네이티브 앱을 React Native로 바꾸면 기존 사용자가 전부 로그아웃되는 이유
네이티브 앱을 React Native로 전환할 때 업데이트를 받은 기존 사용자가 로그인 상태를 잃습니다. Keychain·Keystore 저장 구조가 달라서 생기는 문제이고, 1회성 토큰 이관으로 막을 수 있습니다.
운영 중인 네이티브 앱을 React Native로 다시 만드는 프로젝트에서 가장 늦게 발견되는 문제가 있습니다. 개발과 테스트를 모두 마치고 스토어에 업데이트를 올린 뒤에야 드러납니다. 기존 사용자가 앱을 업데이트하면 로그인이 풀립니다.
새로 설치한 사용자에게는 아무 문제가 없기 때문에 개발 단계에서 잡히지 않습니다. 테스트 기기에도 대부분 새 빌드를 새로 깔아 쓰므로 재현되지 않습니다. 그래서 출시 직후 문의가 몰리고 나서야 원인을 찾게 됩니다.
왜 로그인이 풀리는가
로그인 상태는 보통 서버가 발급한 토큰을 기기에 저장해 유지합니다. 앱을 다시 켰을 때 저장된 토큰을 읽어 서버에 확인하면 다시 로그인할 필요가 없습니다. 문제는 그 토큰을 어디에, 어떻게 저장했느냐입니다.
iOS
네이티브 앱은 보통 Keychain 에 토큰을 넣습니다. Keychain 항목에는 kSecAttrService, kSecAttrAccount, kSecAttrAccessGroup 같은 속성이 붙고, 저장한 쪽과 읽는 쪽이 이 속성을 똑같이 맞춰야 값을 꺼낼 수 있습니다.
React Native 에서 흔히 쓰는 react-native-keychain 은 자체 규칙으로 service 값을 정합니다. 기본값은 앱의 번들 ID 이지만, 네이티브 앱이 쓰던 service 문자열과 일치한다는 보장이 없습니다. 게다가 라이브러리가 값을 저장할 때 자체 형식으로 감싸는 경우가 있어, service 가 우연히 맞더라도 읽어낸 값을 그대로 토큰으로 쓰지 못할 수 있습니다.
즉 같은 기기의 같은 앱인데도 새 코드가 옛 토큰을 찾지 못합니다. 토큰이 사라진 것이 아니라 열쇠가 다른 것입니다.
Android
Android 는 사정이 조금 다릅니다. 네이티브 앱이 SharedPreferences 나 EncryptedSharedPreferences 에 저장했다면, 파일 이름과 키 이름을 알면 접근할 수 있습니다. 다만 EncryptedSharedPreferences 는 Android Keystore 의 마스터 키로 암호화하므로, 그 마스터 키 별칭(alias)까지 같아야 복호화됩니다.
React Native 쪽 라이브러리는 대개 자기만의 파일과 별칭을 씁니다. 결과는 iOS 와 같습니다.
웹뷰 쿠키는 또 다른 문제입니다
웹뷰를 함께 쓰는 앱이라면 문제가 하나 더 있습니다. 네이티브 앱의 웹뷰(WKWebView, Android WebView)에 남아 있던 세션 쿠키가 React Native 웹뷰로 자동으로 넘어가지 않습니다.
특히 iOS 는 WKWebView 의 쿠키 저장소가 앱의 다른 네트워크 계층(URLSession)과 분리되어 있습니다. 네이티브 코드에서 로그인해 받은 세션을 웹뷰가 그대로 쓰던 구조였다면, 전환 후에는 그 연결이 끊어집니다. 앱은 로그인 상태인데 웹뷰 안 페이지만 로그아웃되어 보이는 증상이 나타납니다.
얼마나 심각한가
로그아웃 자체보다 그 뒤에 일어나는 일이 문제입니다.
- 소셜 로그인만 지원하던 앱이라면 사용자가 어떤 계정으로 가입했는지 기억하지 못합니다. 카카오로 가입했는지 애플로 가입했는지 모른 채 다른 방식으로 다시 로그인하면 계정이 갈라집니다
- 본인인증을 거친 서비스는 재인증을 다시 요구하게 되어 이탈이 큽니다
- 결제 수단이 등록된 서비스는 재로그인 과정에서 이탈하면 정기 결제가 끊깁니다
- 스토어 리뷰에 낮은 평점이 몰리고, 이건 되돌리기 어렵습니다
해결 방법: 1회성 토큰 이관
원리는 단순합니다. 새 앱이 첫 실행 때 옛 저장소를 직접 읽어 새 저장소로 옮기고, 옮겼다는 표시를 남깁니다.
1단계 — 옛 저장 위치를 정확히 파악합니다
기존 네이티브 앱의 소스에서 확인해야 할 값입니다.
| 플랫폼 | 확인할 것 |
|---|---|
| iOS | Keychain 항목의 kSecAttrService, kSecAttrAccount, 접근 그룹(kSecAttrAccessGroup), 저장한 값의 형식(문자열인지 JSON인지 아카이빙된 객체인지) |
| Android | SharedPreferences 파일명과 키 이름, EncryptedSharedPreferences 사용 여부, 마스터 키 별칭 |
기존 앱 소스가 없다면 이 단계에서 막힙니다. 인수인계 자료에 저장소 정보가 남아 있는지 프로젝트 시작 전에 반드시 확인하십시오. 이건 개발 난이도의 문제가 아니라 정보 확보의 문제입니다.
2단계 — 네이티브 모듈로 옛 값을 읽습니다
JavaScript 만으로는 옛 Keychain 항목에 접근할 수 없습니다. iOS 는 Swift·Objective-C, Android 는 Kotlin·Java 로 작은 네이티브 모듈을 만들어 옛 속성 그대로 조회하는 함수를 노출시킵니다. 하는 일은 "읽어서 문자열로 돌려주기" 하나뿐이라 코드는 길지 않습니다.
3단계 — 읽은 토큰을 검증하고 새 저장소에 씁니다
읽어낸 토큰을 그대로 믿지 말고 서버에 한 번 확인하십시오. 이미 만료되었거나 서버에서 폐기된 토큰일 수 있습니다. 유효하면 새 저장소에 저장하고, 유효하지 않으면 조용히 로그인 화면으로 보냅니다.
이 시점에 리프레시 토큰으로 새 토큰을 발급받아 저장하는 방식이 더 안전합니다. 옛 토큰의 수명이 얼마 남지 않았을 수 있기 때문입니다.
4단계 — 이관 완료 표시를 남깁니다
migration_v1_done 같은 표시를 새 저장소에 남겨, 두 번째 실행부터는 이관 로직을 건너뛰게 합니다. 이 표시가 없으면 앱을 켤 때마다 옛 저장소를 뒤지게 됩니다.
5단계 — 웹뷰 쿠키도 함께 옮깁니다
웹뷰를 쓰는 구조라면 이관한 토큰으로 웹뷰 쿠키를 직접 설정해 주어야 합니다. React Native 웹뷰에 쿠키를 심는 방법은 두 가지입니다. 네이티브 쪽 쿠키 저장소에 직접 넣거나, 웹뷰가 처음 로드될 때 토큰을 담은 요청을 보내 서버가 쿠키를 내려주게 하는 것입니다. 후자가 관리하기 편합니다.
검증은 이렇게 합니다
이관 코드는 옛 앱이 설치된 상태에서 새 앱으로 덮어썼을 때만 동작합니다. 그래서 테스트도 그 조건을 그대로 만들어야 합니다.
- 스토어에 올라가 있는 현재 버전 앱을 설치합니다
- 실제로 로그인해서 토큰을 저장시킵니다
- 그 위에 새 빌드를 덮어씁니다 (지우고 새로 깔면 재현되지 않습니다)
- 로그인 상태가 유지되는지, 웹뷰 안쪽도 로그인 상태인지 확인합니다
iOS 는 TestFlight, Android 는 내부 테스트 트랙으로 이 절차를 밟을 수 있습니다. 소셜 로그인 방식별로(카카오·애플·구글) 각각 확인해야 합니다.
일정에 반영해야 할 분량
전환 프로젝트 견적에서 이 항목이 누락되는 일이 많습니다. 실제로는 다음 정도가 듭니다.
| 작업 | 대략의 기간 |
|---|---|
| 옛 저장 구조 파악 (소스 확인 포함) | 1~2일 |
| iOS·Android 네이티브 모듈 작성 | 2~3일 |
| 이관 로직과 서버 검증 연결 | 1~2일 |
| 웹뷰 쿠키 이관 | 1~2일 |
| 덮어쓰기 시나리오 테스트 (로그인 방식별) | 2~3일 |
기존 앱 소스가 없거나 저장 구조를 알 수 없으면 이 계획이 성립하지 않습니다. 그때는 전 사용자에게 재로그인을 요청하는 것을 전제로 공지와 안내 화면을 설계하는 편이 낫습니다. 숨기려다 문의가 몰리는 것보다, 업데이트 화면에서 미리 알리고 재로그인 절차를 짧게 만드는 쪽이 이탈이 적습니다.
정리
네이티브 앱을 React Native로 전환할 때 로그아웃 문제는 버그가 아니라 저장소가 바뀌면 당연히 생기는 결과입니다. 미리 알고 계획에 넣으면 대응할 수 있고, 모르고 출시하면 되돌리기 어렵습니다.
전환을 검토하고 계시다면 기존 앱의 소스와 저장 구조 자료가 남아 있는지부터 확인해 보십시오. 그 자료 유무에 따라 전환 방식과 일정이 달라집니다. 저희 회사에 문의를 주시면 지금 가진 자료로 어디까지 이관이 가능한지 먼저 확인해 드리겠습니다.