[React Native] 앱 접근시 필요한 동작을 구현해보자
시대생 앱은 최초 앱 접근시 로그인 유무에 따라 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>
);
- 본 글에서 설명할 앱 접근 시 로직과 앱 내 인증 인가 처리를 전체 구조를 그려보았습니다. 아래 이미지는 해당 구조도입니다.
앱 접근시 실행 로직
앱 접근시 처리해야 하는 과정은 아래와 같습니다.
- notification permission을 가져온다.
- 알림권한에 동의했다면 FCM Token을 가져오고, Device Storage에 저장한다.
- 로그인 여부를 확인하기 위해 userInfo API를 서버에 요청한다.
- 응답이 401이라면 로그인 상태가 아니기 때문에, 앱의 로그인 상태를 false로 만든 후 Account Screen을 보여준다.
- 응답이 200이라면 앱의 로그인 상태를 true로 만든다.
- 이후 응답받은 userInfo를 Device Storage에 저장한다.
- 현재 Device의 정보(fcm token, app version 등)가 서버와 다르다면 최신 정보로 서버에 업데이트한다.
- 로그인 상태를 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로 변경합니다.
앱에서 로그인 상태가 변경되는 경우
로그인 / 회원가입
로그인 또는 회원가입하는 경우 아래와 같은 작업을 수행합니다.
- 로그인 / 회원가입 API의 응답에서 얻은 JWT token을 device storage에 저장
- deviceInfo를 서버에 저장
- userInfo를 받아와 device storage에 저장
- 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