블로그 목록
apps

expo-web-browser로 인앱 브라우저·OAuth 로그인 붙이기 — 시스템 브라우저로 튕기지 말자

소셜 로그인을 붙이는데 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도 있지만, 콜백을 직접 다뤄야 하는 경우엔 이 조합이 제일 단순했다.