[React Native] 앱 접근시 필요한 동작을 구현해보자

profile image pIutos 2023. 11. 21. 07:52

시대생 첫화면, Account / Main

시대생 앱은 최초 앱 접근시 로그인 유무에 따라 AccountScreen을 보여주거나, MainScreen을 보여줘야 합니다.

또한 로그인이 되어있다면 device 정보를 서버에 업데이트 시키거나, 알림을 위한 firebasePushToken 정보를 얻어오는 과정이 필요하기 때문에 이와 관련된 로직이 앱 접근시 실행되어야합니다.

본 글은 앱 접근시 어떤 로직이 실행되도록 구현했는지, 그리고 어떻게 앱의 전체적인 회원 로그인 관련 처리를 구현했는지를 설명합니다.

시작하기 앞서

  • 시대생 앱의 사용자 인증 인가 방식은 JWT 방식을 사용합니다.
  • React Native의 Device Storage로는 mmkv를 이용합니다.
  • 앱 화면을 보여주는 RootStackNavigator는 아래와 같습니다.
const [isLoading, setIsLoading] = useState(true);
const [isLoggedIn, setIsLoggedIn] = useState(false);

useEffect(() => {
  if (isLoading) return;
  (async () => await SplashScreen.hide())();
}, [isLoading]);

return (
  <Stack.Navigator
    initialRouteName="Main"
    {isLoggedIn ? (
      <>
        <Stack.Screen
          name="Main"
          component={RootBottomTapNavigator}
          options={{animationEnabled: false}}
        />
        // ...
      </>
    ) : (
      <Stack.Screen name="Account" component={AccountStackNavigator} />
    )}
  </Stack.Navigator>
);
  • 본 글에서 설명할 앱 접근 시 로직과 앱 내 인증 인가 처리를 전체 구조를 그려보았습니다. 아래 이미지는 해당 구조도입니다.

앱 접근시 실행 로직

앱 접근시 처리해야 하는 과정은 아래와 같습니다.

  1. notification permission을 가져온다.
  2. 알림권한에 동의했다면 FCM Token을 가져오고, Device Storage에 저장한다.
  3. 로그인 여부를 확인하기 위해 userInfo API를 서버에 요청한다.
  4. 응답이 401이라면 로그인 상태가 아니기 때문에, 앱의 로그인 상태를 false로 만든 후 Account Screen을 보여준다.
  5. 응답이 200이라면 앱의 로그인 상태를 true로 만든다.
  6. 이후 응답받은 userInfo를 Device Storage에 저장한다.
  7. 현재 Device의 정보(fcm token, app version 등)가 서버와 다르다면 최신 정보로 서버에 업데이트한다.
  8. 로그인 상태를 true로 만든 후 Main Screen을 보여준다.

이를 코드로 아래와 같이 구현했습니다. 이를 하나하나 자세히 살펴보겠습니다.

  useEffect(() => {
    (async () => {
      await NotificationService.requestNotificationPermissions();
      await NotificationService.handleFirebasePushToken();

      const hasRefreshToken = UserService.getHasRefreshToken();
      if (!hasRefreshToken) {
        setLoadingFinish();
        return;
      }

      const userInfo = await UserService.getUserInfoFromServer();
      if (!userInfo) {
        setLoadingFinish();
        return;
      }
      UserService.setUserInfoToDevice(userInfo);

      await DeviceService.updateDeviceInfo();
      setAuthenticationSuccess();
    })();
  }, []);

FCM Token 관련 로직

await NotificationService.requestNotificationPermissions();
await NotificationService.handleFirebasePushToken();

 

FCM Token은 한 device마다 생성됩니다. token을 가져오기 위해 알림 권한을 받아오고, 권한이 없다면 토큰을 받아오지 않습니다.

static async requestNotificationPermissions(): Promise<FirebaseMessagingTypes.AuthorizationStatus> {
  if (Platform.OS === 'android') {
    await Promise.all([
      // 생략
      // permission을 받고, notifee channel을 생성
    ]);
  }
  return messaging().requestPermission();
}

iOS에서는 토큰을 가져오는 messaging.getToken() 함수는 실행 이전에 requestPermission이 필요합니다.

FCM token을 가져오는 방법에 대해서는 양이 많기 때문에 따로 포스팅 하겠습니다.

static async handleFirebasePushToken(): Promise<void> {
  const isPermissionAuthorized =
    await this.checkPermissionIsAuthorizedStatus(); // messaging.hasPermission을 이용
  if (!isPermissionAuthorized) return;

  const token = await this.getFirebasePushToken();
  this.setFirebasePushToken(token);
}

권한이 없다면 FCM 토큰을 가져오지 않고, 있다면 토큰을 가져온 다음 device storage에 해당 토큰을 저장합니다. 각 메서드들은 class 내부에 추상화하여 사용했습니다.

Storage의 RefreshToken 유무 확인

Device Storage에 refreshToken이 존재하지 않는다면 로딩을 종료합니다. 이 과정을 추가함으로써 refreshToken 유무만 확인하면 되므로 초기에 빠른 로딩이 가능해졌습니다.

const hasRefreshToken = UserService.getHasRefreshToken();
if (!hasRefreshToken) {
  setLoadingFinish();
  return;
}

UserInfo API 요청 및 저장

JWT token을 이용하여 로그인 여부를 확인하기 위해 유저의 정보(닉네임, id등)를 가져오는 API를 요청합니다.

응답이 400, 즉 유저 정보가 없다면 마찬가지로 로딩을 종료합니다.
응답이 200으로 정상적으로 온다면 device storage에 응답받은 유저 정보를 저장합니다.

만약 accessToken이 만료되어 refreshToken을 이용해 재발급 받는 로직은 여기서 다루지 않고, 따로 포스팅하겠습니다.

const userInfo = await UserService.getUserInfoFromServer();
if (!userInfo) {
  setLoadingFinish();
  return;
}
UserService.setUserInfoToDevice(userInfo);

디바이스 정보 업데이트

해당 코드는 다음과 같습니다.

await DeviceService.updateDeviceInfo();
setAuthenticationSuccess();
static async updateDeviceInfo(): Promise<void> {
  const localDeviceInfo = this.getDeviceInfoFromLocal();
  const serverDeviceInfo = await this.getDeviceInfoFromServer();
  if (
    localDeviceInfo.appVersion !== serverDeviceInfo.appVersion ||
    localDeviceInfo.codePushVersion !== serverDeviceInfo.codePushVersion ||
    localDeviceInfo.firebasePushToken !==
      serverDeviceInfo.firebasePushToken ||
    // ...
  ) {
    await CoreAPI.patchDeviceInfo(localDeviceInfo);
  }
}

유저가 앱 또는 OS를 업데이트 하거나 FCM token이 업데이트 되는 상황같아 device 정보가 수정 될 수 있습니다.

따라서 만약 현재 device 정보가 서버와 다르다면 서버로 patch시키도록 합니다.

const setAuthenticationSuccess = () => {
  storage.set('isLoggedIn', true);
  setIsLoggedIn(true);
  setLoadingFinish();
};

이후 모든 과정이 종료되었다면 isLoggedIn과 isLoading상태를 true로 변경합니다.

앱에서 로그인 상태가 변경되는 경우

로그인 / 회원가입

로그인 또는 회원가입하는 경우 아래와 같은 작업을 수행합니다.

  1. 로그인 / 회원가입 API의 응답에서 얻은 JWT token을 device storage에 저장
  2. deviceInfo를 서버에 저장
  3. userInfo를 받아와 device storage에 저장
  4. isLoggedIn 상태를 true로 변경
/** SingIn 또는 SingUp시 실행되는 함수입니다. */
static async onRegister(params: OnRegisterParamsType): Promise<void> {
  storeToken(params.accessToken, params.refreshToken);
  await DeviceService.setDeviceInfo();
  await UserService.handleUserInfo();
  storage.set('isLoggedIn', true);
}

로그아웃

device storage에 저장된 JWT token과 userInfo, isLoggedIn 상태를 삭제합니다.

static deleteUserInfo(): void {
  storage.delete('accessToken');
  storage.delete('refreshToken');
  storage.delete('user');
  storage.set('isLoggedIn', false);
};
  
static logout(): void {
  this.deleteUserInfo();
}

주의점: 직접적으로 Screen을 navigate하면 안됩니다.

공식문서에서 설명하듯이 직접적으로 useNavigation 훅 등을 이용하여 스크린으로 navigate 시키면 안됩니다. 대신에 isSignedIn, isLoggedIn과 같은 state를 이용하여 Account / Home Screen을 렌더링해야 합니다.

// 로그인 완료시
const navigation = useNavigation();
navigation.navigate('Main') // x

setIsLoggedIn(true) // o

 

참고링크

https://reactnavigation.org/docs/auth-flow/