본문 바로가기
🔥React 뽀개기

React Native에 React Query 적용하기

by 짱돌보리 2025. 2. 8.
728x90

React Native에 React Query 적용하기

React Query란?

React Query는 서버 상태 관리 라이브러리로 아래와 같은 기능을 제공한다!

  • 데이터 페칭, 캐싱, 동기화, 업데이트 관리
  • 네트워크 요청 상태(로딩, 에러, 성공) 추적
  • 서버 데이터 캐싱 및 리패칭 최적화

이번 프로젝트에서 회원가입 기능을 구현하면서, React Query를 도입했는데 연습용으로 사용해봤다. React Query는 서버와의 비동기 통신을 효율적으로 관리하고, 상태 관리가 복잡하지 않게 해주는 도구로 잘 알려져 있다. 기존에는 서버와의 비동기 통신을 처리하기 위해 fetch나 axios를 사용했었지만, React Query는 비동기 상태를 자동으로 관리해줘서 상태 변화에 따른 처리 로직을 더욱 깔끔하게 작성할 수 있었던 것 같다.

 

이제 react query로 firebase api를 연동해 작업해보자..

1. React Native에 React Query 설치

pnpm add @tanstack/react-query

 

설치 후 queryClient를 설정해야 한다. (queryClient.ts를 생성해 프로젝트 폴더 하위 바로 넣어줌.!)

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient()

2. QueryClientProvider 설정

React Query를 사용하기 위해 최상위 컴포넌트에 QueryClientProvider를 추가해야 한다.

난 _layout 파일에 넣어주었돠.

  • QueryClientProvider는 React Query의 모든 기능을 하위 컴포넌트에서 사용할 수 있도록 한다.
  • queryClient는 서버 상태를 관리한다.

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      <Stack
        screenOptions={{
          headerStyle: {
            backgroundColor: '#fff',
          },
          headerTintColor: '#000',
          headerTitleStyle: {
            fontWeight: 'bold',
          },
          headerTitle: 'Fitty',
          headerLeft: () => (
            <Image
              source={require('../assets/images/faceLogo_bgRemoved.png')}
              style={{ width: 40, height: 40, marginLeft: 130 }}
            />
          ),
        }}
      >
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      </Stack>
    </QueryClientProvider>
  )
}

3. API 요청 구현

난 firebase를 사용 중이라 firebaseApi.ts 파일을 만들고 api 로직을 분리해줬다.

// 회원가입
export const signupUser = async (
  userData: FinalSignupFormData
): Promise<void> => {
  try {
    // Firebase Authentication에 유저 등록
    const userCredential = await createUserWithEmailAndPassword(
      auth,
      userData.email,
      userData.password
    )
    const user = userCredential.user

    // Firestore에 유저 정보 저장
    await addDoc(collection(db, 'users'), {
      uid: user.uid,
      nickname: userData.nickname,
      email: userData.email,
    })
  } catch (error) {
    console.error('회원가입 실패:', error)
    throw error
  }
}

 

이제 signup.tsx 에서 회원가입 요청을 보내준다.

 

회원가입 폼을 제출할 때, 서버에 요청을 보내고 그에 대한 성공/실패 결과를 처리하는 방식으로 useMutation을 사용했다. React Query의 useMutation 훅은 주로 데이터 생성, 수정, 삭제와 같은 서버에 데이터를 전송하는 작업에 적합한 훅이다!

const mutation = useMutation<void, Error, FinalSignupFormData>({
    mutationFn: signupUser,
    onSuccess: () => {
      Alert.alert('회원가입 성공!', '이제 Fitty를 사용할 수 있어요.')
      router.push('/login')
    },
    onError: (error: Error) => {
      console.error(error)
      Alert.alert('회원가입 실패!', '다시 시도해주세요.')
    },
  })

  const onSubmit = (data: SignupFormData) => {
    const { confirmPassword, ...userData } = data
    mutation.mutate(userData)
  }

 

위와 같은 방식으로 성공실패 시에 각각 적절한 처리를 할 수 있돠!!!

 

난 zod + react-hook-form으로 회원가입 폼을 만들어 함께 통합해주면 된다!

type SignupFormData = z.infer<typeof signupSchema>
export type FinalSignupFormData = Omit<SignupFormData, 'confirmPassword'>

export default function Signup() {
  const {
    register,
    handleSubmit,
    setValue,
    formState: { errors },
  } = useForm<SignupFormData>({
    resolver: zodResolver(signupSchema),
    mode: 'all',
  })

  const mutation = useMutation<void, Error, FinalSignupFormData>({
    mutationFn: signupUser,
    onSuccess: () => {
      Alert.alert('회원가입 성공!', '이제 Fitty를 사용할 수 있어요.')
      router.push('/login')
    },
    onError: (error: Error) => {
      console.error(error)
      Alert.alert('회원가입 실패!', '다시 시도해주세요.')
    },
  })

  const onSubmit = (data: SignupFormData) => {
    const { confirmPassword, ...userData } = data
    mutation.mutate(userData)
  }

  return (
    <View style={commonStyles.container}>
      <Link href="/">
        <Image
          source={require('../../assets/images/fullLogo_bgRemoved.png')}
          style={styles.image}
        />
      </Link>
      <Text style={styles.title}>회원가입</Text>
      <TextInput
        placeholder="닉네임"
        style={styles.input}
        keyboardType="email-address"
        {...register('nickname')}
        onChangeText={(text) => setValue('nickname', text)}
      />
      {errors.nickname && (
        <Text style={styles.errorText}>{errors.nickname.message}</Text>
      )}
      <TextInput
        placeholder="이메일"
        style={styles.input}
        keyboardType="email-address"
        {...register('email')}
        onChangeText={(text) => setValue('email', text)}
      />
      {errors.email && (
        <Text style={styles.errorText}>{errors.email.message}</Text>
      )}

      <TextInput
        placeholder="비밀번호"
        secureTextEntry
        style={styles.input}
        {...register('password')}
        onChangeText={(text) => setValue('password', text)}
      />
      {errors.password && (
        <Text style={styles.errorText}>{errors.password.message}</Text>
      )}
      <TextInput
        placeholder="비밀번호 확인"
        secureTextEntry
        style={styles.input}
        {...register('confirmPassword')}
        onChangeText={(text) => setValue('confirmPassword', text)}
      />
      {errors.confirmPassword && (
        <Text style={styles.errorText}>{errors.confirmPassword.message}</Text>
      )}

      <Button title="회원가입" onPress={handleSubmit(onSubmit)} />

      <Text style={styles.footerText}>
        이미 계정이 있으신가요?
        <Text style={styles.linkText} onPress={() => router.push('/login')}>
          로그인하러 가기
        </Text>
      </Text>
    </View>
  )
}

 

위와 마찬가지로 로그인도 구현해주면 된다.

// 로그인
export const loginUser = async (
  userData: LoginFormData
): Promise<UserCredential> => {
  try {
    const userCredential = await signInWithEmailAndPassword(
      auth,
      userData.email,
      userData.password
    )
    return userCredential
  } catch (error) {
    console.error('로그인 실패:', error)
    throw error
  }
}
const mutation = useMutation<UserCredential, Error, LoginFormData>({
    mutationFn: loginUser,
    onSuccess: () => {
      Alert.alert('로그인 성공!', '오늘 운동을 완료하세요.')
      router.push('/(tabs)')
    },
    onError: (error: Error) => {
      console.error(error)
      Alert.alert('로그인 실패!', '이메일 또는 비밀번호를 확인해주세요.')
    },
  })

  const onSubmit = (data: LoginFormData) => {
    mutation.mutate(data)
  }

 

 

실행을 해보면...

 

실행도 잘 되고 firebase에도 회원이 잘 추가가 된다.!! 🥳🥳