Intro
- 카카오 로그인을 붙이는데 iOS와 Android가 서로 다른 인증 플로우를 요구해 한 번에 정리하기가 쉽지 않았습니다.
- 저는 Expo AuthSession과 Linking을 조합해 두 플랫폼을 하나의 함수에서 처리했습니다.
핵심 아이디어 요약
- 공통으로 사용할 리다이렉트 URI를
AuthSession.makeRedirectUri로 생성했습니다.
- iOS에서는
WebBrowser.openAuthSessionAsync로 웹 기반 인증을 진행하고, Android에서는 Linking 이벤트로 토큰을 추적했습니다.
- Supabase OAuth를 사용해 백엔드 세션 설정까지 한번에 마무리했습니다.
준비와 선택
- Expo Router를 쓰고 있어 로그인 이후 화면 전환은
router.push('/signup/check')로 통일했습니다.
- 딥링크 스킴은 Kakao 콘솔과 일치시키기 위해
schoolmeets:// 형식을 사용했습니다.
- Android 타임아웃은 120초로 제한해 사용자가 앱을 벗어난 뒤 돌아오지 않는 문제를 방지했습니다.
구현 여정
- 리다이렉트 URI 구성: AuthSession이 제공하는 헬퍼로 네이티브 스킴 URI를 만들었습니다.
- Supabase OAuth 호출:
supabase.auth.signInWithOAuth에서 provider를 kakao로 지정하고, scope를 명시했습니다.
- iOS 처리: 인증 성공 시 URL fragment 또는 query에서 access_token/refresh_token을 추출했습니다.
- Android 처리:
Linking.addEventListener('url', ...)로 앱 스킴이 돌아올 때까지 기다리고, 타임아웃 시 에러를 던졌습니다.
- 세션 설정:
supabase.auth.setSession으로 받은 토큰을 저장하고, 후속 라우팅을 이어갔습니다.
const handleKakaoLogin = async () => {
const redirectUri = AuthSession.makeRedirectUri({
scheme: 'schoolmeets',
preferLocalhost: false,
});
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'kakao',
options: {
redirectTo: redirectUri,
scopes:
'profile_image account_email name gender birthday birthyear phone_number',
},
});
if (error || !data.url) throw new Error('카카오 로그인 실패');
if (Platform.OS === 'ios') {
const result = await WebBrowser.openAuthSessionAsync(data.url, redirectUri);
if (result.type !== 'success') return;
const params = new URLSearchParams(result.url.split('#')[1] || result.url.split('?')[1]);
const access_token = params.get('access_token');
const refresh_token = params.get('refresh_token');
if (!access_token || !refresh_token) throw new Error('토큰 없음');
await supabase.auth.setSession({ access_token, refresh_token });
router.push('/signup/check');
return;
}
const urlPromise = new Promise<string>((resolve, reject) => {
const subscription = Linking.addEventListener('url', ({ url }) => {
const lower = url.toLowerCase();
const isOurScheme = lower.startsWith('schoolmeets://');
const isOurRedirect = lower.startsWith(redirectUri.toLowerCase());
if (!isOurScheme && !isOurRedirect) return;
clearTimeout(timeoutId);
subscription.remove();
resolve(url);
});
const timeoutId = setTimeout(() => {
clearTimeout(timeoutId);
subscription.remove();
reject(new Error('로그인 응답 시간 초과'));
}, 120_000);
});
await Linking.openURL(data.url);
const returnedUrl = await urlPromise;
const params = new URLSearchParams(returnedUrl.split('#')[1] || returnedUrl.split('?')[1]);
const access_token = params.get('access_token');
const refresh_token = params.get('refresh_token');
if (!access_token || !refresh_token) throw new Error('토큰 없음');
await supabase.auth.setSession({ access_token, refresh_token });
router.push('/signup/check');
};
결과와 회고
- 두 플랫폼에서 동일한 버튼을 사용하면서 인증 성공률이 높아졌고, KakaoTalk 앱이 설치돼 있어도 문제없이 동작했습니다.
- 타임아웃 처리를 넣은 덕분에 3분 이상 기다리는 QA 이슈도 해결됐습니다.
- 다음에는 에러 메시지를 사용자가 이해하기 쉬운 텍스트로 맵핑하고, Linking 이벤트를 전역 훅으로 옮길 계획입니다.
- 여러분 팀도 소셜 로그인을 붙이고 계신가요? 플랫폼별로 어떤 차이를 경험했는지 공유해 주세요.
Reference