소셜 로그인을 붙이는데 Linking.openURL(authUrl)로 처리하니까 사파리가 통째로 떠버렸다. 로그인 끝나고 앱으로 못 돌아오는 경우도 있고, 사용자가 브라우저를 그냥 닫아버리면 앱은 계속 로딩 스피너만 돌았다. 흐름이 앱 밖으로 나가버리는 게 문제였다.
expo-web-browser는 이걸 인앱 브라우저(iOS의 SFSafariViewController, Android의 Custom Tabs)로 처리한다. 세션이 앱 안에 갇혀 있으니까 로그인이 끝나면 자동으로 닫히고 결과가 앱으로 바로 돌아온다.
npx expo install expo-web-browser
그냥 링크 여는 용도
외부 링크(약관, 공지 같은 것)를 열 때는 openBrowserAsync 하나면 된다. 시스템 브라우저로 튕기지 않고 앱 위에 시트처럼 뜬다.
import * as WebBrowser from 'expo-web-browser';
<Button title="이용약관" onPress={() => WebBrowser.openBrowserAsync('https://example.com/terms')} />
OAuth는 openAuthSessionAsync
로그인은 openAuthSessionAsync를 써야 한다. openBrowserAsync와 달리 리다이렉트 URL을 감지하면 창을 알아서 닫고 결과를 돌려준다. 우리 앱 scheme으로 돌아오는 콜백을 잡아채는 게 핵심이다.
import * as WebBrowser from 'expo-web-browser';
import * as Linking from 'expo-linking';
WebBrowser.maybeCompleteAuthSession();
async function login() {
const redirectUrl = Linking.createURL('auth-callback');
const authUrl = `https://your-auth.com/authorize?redirect_uri=${encodeURIComponent(redirectUrl)}`;
const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl);
if (result.type === 'success') {
const { queryParams } = Linking.parse(result.url);
console.log('토큰:', queryParams?.code);
} else {
// 'cancel' | 'dismiss' — 사용자가 그냥 닫음
console.log('로그인 취소됨');
}
}
redirectUrl을 두 번째 인자로 넘겨줘야 그 URL로 돌아왔을 때 세션을 닫는다. 이걸 빼먹으면 콜백이 와도 브라우저가 안 닫혀서 처음에 한참 헤맸다.
두 가지 빼먹기 쉬운 것
- 앱 최상단에서
WebBrowser.maybeCompleteAuthSession()을 한 번 호출해야 웹 환경과 팝업 흐름에서 세션이 제대로 마무리된다. result.type을 꼭 확인하자.success가 아니면 사용자가 취소한 거니까 로딩 상태를 반드시 풀어줘야 한다. 이 처리를 안 해서 스피너 무한 로딩이 나던 게 원래 문제였다.
redirect_uri는 인증 서버 콘솔에도 똑같이 등록해둬야 한다. 개발 중엔 exp:// scheme, 배포 빌드에선 앱 커스텀 scheme으로 값이 달라지니 둘 다 넣어두는 게 편하다.
외부 브라우저로 튕기던 흐름을 인앱 세션으로 바꿨더니 로그인 이탈이 눈에 띄게 줄었다. AuthSession 같은 상위 API도 있지만, 콜백을 직접 다뤄야 하는 경우엔 이 조합이 제일 단순했다.