Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리

2026.04.21·수정 2026.09.13·약 19분·작성: 해비·블로그 소개

학습 목표·선수 지식: 일반 외부 링크와 인증 세션을 구분하고 취소·성공·복귀 주소 불일치를 처리합니다. Promise와 URL, 앱 scheme 개념을 알아야 합니다.

이 글에서 정리하는 내용

이 글에서는 expo-web-browser를 사용할 때 무엇을 위해 쓰는지부터 openBrowserAsync와 openAuthSessionAsync의 차이, 로그인 후 앱으로 돌아오는 딥링크 흐름, Expo Router와 함께 쓸 때의 포인트, Android 최적화 함수까지 한 번에 정리합니다. 끝까지 읽으면 외부 링크 열기와 인증 흐름을 같은 브라우저 기능으로 보지 않고 상황에 따라 정확히 나눠서 적용할 수 있게 됩니다.

expo-web-browser는 무엇이고 왜 쓰는가

Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리 핵심 개념을 설명하는 첫 번째 본문 이미지

expo-web-browser는 앱 안에서 웹 콘텐츠를 보여주되, 일반적인 WebView처럼 앱 내부에 웹 페이지를 직접 렌더링하는 방식이 아니라 운영체제가 제공하는 시스템 브라우저 경험을 활용하는 도구로 이해하면 훨씬 정리가 쉽습니다. 실무에서는 보통 개인정보처리방침, 이용약관, 고객센터, 외부 결제 안내, 로그인 페이지처럼 앱 바깥 웹 문서를 잠깐 열어야 할 때 먼저 떠올리게 됩니다.

핵심은 이 패키지가 단순히 URL을 여는 함수 모음이 아니라는 점입니다. 어떤 메서드를 고르느냐에 따라 일반 웹페이지 열기인지, 인증 후 앱으로 다시 돌아와야 하는 흐름인지가 갈립니다. 그래서 처음부터 expo-web-browser를 WebView 대체재로만 보면 실무에서 가장 많이 막히는 인증 복귀 구조를 놓치기 쉽습니다.

WebView와 구분해서 보는 기준

코드 위치: src/auth/browser.ts. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.

// 일반 링크 열기와 인앱 렌더링은 목적이 다릅니다.
const useCases = {
  privacyPolicy: 'expo-web-browser',
  termsOfService: 'expo-web-browser',
  oauthLogin: 'expo-web-browser 또는 expo-auth-session',
  internalCustomPage: 'react-native-webview',
};

기준은 단순합니다. 외부 웹페이지를 시스템 브라우저 경험으로 안전하게 열고 닫는 것이 목적이면 expo-web-browser가 먼저입니다. 반대로 앱 내부 화면처럼 웹 콘텐츠를 깊게 제어해야 한다면 WebView가 더 맞습니다. 즉, 화면을 임베드할 것인지, 브라우저를 잠깐 호출할 것인지부터 나눠서 생각하면 선택이 쉬워집니다.

플랫폼 관점에서도 차이가 있습니다. Android에서는 Custom Tabs를 사용하고, iOS에서는 호출 방식에 따라 SFSafariViewController 또는 ASWebAuthenticationSession이 사용됩니다. 이 차이 때문에 같은 브라우저 호출처럼 보여도 실제 동작과 인증 적합성이 달라집니다.

openBrowserAsync와 일반 링크 열기

가장 먼저 익혀야 하는 메서드는 openBrowserAsync입니다. 이 메서드는 로그인 인증용이라기보다 일반적인 외부 링크 열기에 가깝습니다. 예를 들어 개인정보처리방침, 이벤트 안내, 공지사항, 고객센터처럼 사용자가 내용을 보고 돌아오면 되는 경우라면 이쪽이 더 자연스럽습니다.

특히 iOS에서는 openBrowserAsync가 SFSafariViewController를 사용하므로 시스템 Safari와 쿠키를 공유하는 인증 세션을 기대하면 안 됩니다. 그래서 로그인 화면도 브라우저니까 같은 함수로 열면 되겠지라고 접근하면, 나중에 인증 복귀나 세션 유지에서 어긋나는 경우가 생깁니다.

기본 사용 예시

코드 위치: src/links/openPrivacyPolicy.ts. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.

import * as WebBrowser from 'expo-web-browser';

export async function openPrivacyPolicy() {
  try {
    const result = await WebBrowser.openBrowserAsync('https://expo.dev/privacy');
    return { kind: result.type } as const;
  } catch {
    return { kind: 'error' } as const;
  }
}

이 코드는 가장 단순하지만 실무에서 자주 쓰는 형태입니다. 버튼 클릭 시 외부 페이지를 열고, 사용자가 닫거나 흐름이 끝나면 결과를 받을 수 있습니다. 여기서 중요한 점은 이 방식이 로그인 성공 후 토큰을 앱으로 다시 받아오는 인증 세션용 기본값은 아니라는 점입니다. 단순 열람 목적이라면 충분하지만, 인증 결과를 안전하게 복귀시켜야 하는 시나리오라면 다음 섹션의 메서드로 넘어가야 합니다.

상황 우선 선택
약관, 공지, 정책 페이지 openBrowserAsync
OAuth 로그인, 인증 후 앱 복귀 openAuthSessionAsync

openAuthSessionAsync와 로그인 인증 흐름

인증은 일반 링크 열기와 완전히 같은 문제로 보면 안 됩니다. 로그인 페이지를 연 뒤 인증이 끝나면 앱으로 다시 돌아와야 하고, 그 복귀 주소가 프로젝트에 등록되어 있어야 합니다. 이 흐름 때문에 expo-web-browser를 배울 때 가장 중요한 분기점이 openBrowserAsync와 openAuthSessionAsync의 차이입니다.

실무에서는 소셜 로그인이나 외부 인증 제공자를 붙일 때 openAuthSessionAsync를 먼저 검토합니다. 특히 iOS에서는 일반 브라우저 방식과 인증 세션 방식의 차이를 모르면 로그인은 됐는데 앱으로 결과를 못 넘겨받는 상황을 만나기 쉽습니다. 인증 제공자 쪽에서도 URI 허용 목록을 관리하는 경우가 많아서, 앱이 보내는 주소와 서버에 등록된 주소가 정확히 일치해야 합니다.

인증 세션 기본 흐름

src/auth/openLoginSession.ts 전체 모듈이지만 완성 OAuth 로그인은 아닙니다. authorizationUrl은 신뢰한 공급자의 authorize 주소에 client ID·redirect URI·state·PKCE challenge를 넣어 만든 값이어야 합니다. 호출부에서 공급자 호스트를 허용 목록으로 고정하세요. callback은 브라우저 복귀만 의미하며 state 검증, 공급자 error 처리, code와 PKCE verifier 교환이 성공해야 로그인 완료입니다. 이 글은 실제 인증 서버·client ID·토큰 교환 코드를 제공하지 않습니다. 인증 URL과 결과 URL에는 민감 값이 있을 수 있어 console.log로 출력하지 않습니다.

import * as WebBrowser from 'expo-web-browser';

export async function openLoginSession(authorizationUrl: string, redirectUrl: string) {
  const parsed = new URL(authorizationUrl);
  if (parsed.protocol !== 'https:') throw new Error('HTTPS 인증 주소가 필요합니다.');
  try {
    const result = await WebBrowser.openAuthSessionAsync(authorizationUrl, redirectUrl);
    if (result.type !== 'success') return { kind: 'cancelled' } as const;
    const actual = new URL(result.url);
    const expected = new URL(redirectUrl);
    if (
      actual.protocol !== expected.protocol ||
      actual.host !== expected.host ||
      actual.pathname !== expected.pathname
    ) {
      return { kind: 'invalid-redirect' } as const;
    }
    return { kind: 'callback', url: result.url } as const;
  } catch {
    return { kind: 'error' } as const;
  }
}

여기서는 redirectUrl이 핵심입니다. 인증 제공자는 로그인 성공 후 이 주소로 다시 보내주고, 앱은 그 주소를 받아 결과를 이어서 처리합니다. 그래서 이 코드는 단순히 브라우저를 연다기보다 브라우저 기반 인증을 앱 흐름으로 되돌리는 출발점에 가깝습니다. 또한 URI는 인증 서버의 허용 목록에도 등록되어 있어야 하므로, 앱 코드와 인증 콘솔 설정을 따로따로 보면 안 됩니다.

scheme 설정과 테스트 방법

app.json의 expo 객체에 병합할 부분 설정입니다. 새 development build를 설치한 뒤 등록한 blogflowdemo://auth/callback과 공급자 허용 목록을 정확히 맞추세요. Expo Go는 앱 전용 scheme 인증 검증 환경이 아닙니다. npx uri-scheme open blogflowdemo://auth/callback --android는 앱 복귀 경로만 검사하며 인증 성공을 증명하지 않습니다.

설정 예시: app.json의 해당 객체에 병합합니다. 기존 설정은 보존합니다.

{
  "expo": { "scheme": "blogflowdemo" }
}

scheme는 내 앱을 여는 고유 주소의 앞부분입니다. 이 값이 없으면 인증 흐름은 끝나더라도 앱이 결과를 돌려받지 못할 수 있습니다. 예를 들어 blogflowdemo://auth/callback 같은 주소가 앱으로 연결되어야 로그인 완료 후 자연스럽게 돌아올 수 있습니다. 따라서 인증 기능을 붙일 때는 브라우저 호출 코드보다 scheme 설정이 먼저 맞는지부터 확인하는 편이 안전합니다.

추가로 iOS에서는 preferEphemeralSession 옵션으로 사적인 인증 세션을 요청할 수 있습니다. 이 옵션은 브라우저의 일반 쿠키를 공유하지 않도록 요청하는 성격이므로, 기존 로그인 상태를 재사용하는 흐름보다 매번 분리된 인증 경험이 필요할 때 검토하면 됩니다.

딥링크, Expo Router, 실무 연결 포인트

Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리 적용 흐름을 설명하는 두 번째 본문 이미지

expo-web-browser를 실무에서 어렵게 만드는 이유는 브라우저를 여는 코드보다 브라우저를 닫고 다음 화면으로 이어지는 처리에 있습니다. 인증 완료 후 콜백 URL이 들어오면 그 주소를 해석해서 필요한 화면으로 보내야 하고, 경우에 따라 브라우저를 직접 닫아주는 흐름도 함께 생각해야 합니다.

Expo Router를 쓰는 프로젝트라면 딥링크 자체는 자동으로 처리됩니다. 다만 자동 처리와 인증 설계는 같은 말이 아닙니다. URL 경로를 어떤 화면에 연결할지, 성공 후 어디로 이동할지, 실패 시 어떤 화면을 보여줄지는 여전히 직접 정리해야 합니다.

Linking 이벤트로 직접 처리하는 예시

src/hooks/useBrowserReturn.ts 전체 훅은 일반 openBrowserAsync 창을 썼고 Router 없이 직접 복귀를 관리하는 별도 흐름에서만 사용합니다. openAuthSessionAsync와 함께 등록하지 않습니다. 수신 URL이 기대한 경로인지 먼저 검사하고 dismissBrowser는 iOS에서만 시도합니다. 이미 닫힌 창의 정리 오류만 무시하며 인증 결과 오류를 삼키는 코드는 아닙니다.

import { useEffect } from 'react';
import { Platform } from 'react-native';
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';

export function useBrowserReturn(expectedUrl: string) {
  useEffect(() => {
    const subscription = Linking.addEventListener('url', ({ url }) => {
      const actual = Linking.parse(url);
      const expected = Linking.parse(expectedUrl);
      if (
        actual.scheme !== expected.scheme ||
        actual.hostname !== expected.hostname ||
        actual.path !== expected.path
      )
        return;
      if (Platform.OS === 'ios') {
        void WebBrowser.dismissBrowser().catch(() => {});
      }
    });
    return () => subscription.remove();
  }, [expectedUrl]);
}

직접 Linking 이벤트를 다룰 때는 브라우저가 자동으로 닫히지 않는 상황까지 같이 고려하는 편이 좋습니다. 그래서 콜백 URL을 받았을 때 dismissBrowser를 호출해 흐름을 마무리하는 패턴이 자주 나옵니다. 반대로 Expo Router를 쓰고 있다면 라우트가 URL과 자연스럽게 연결되기 때문에 딥링크 구조를 이해하기가 한결 편해집니다. 파일 기반 라우팅을 쓰는 프로젝트라면 auth/callback 같은 경로를 앱 라우트 구조 안에서 함께 설계해두면 읽는 사람도 흐름을 따라가기 쉽습니다.

다만 이 부분에서 한 가지를 더 구분해야 합니다. openBrowserAsync나 일반 창으로 딥링크를 되돌리는 패턴에서는 Linking 이벤트와 dismissBrowser가 유효하지만, openAuthSessionAsync를 iOS 인증 세션으로 사용할 때는 수동 Linking 이벤트를 같이 붙이지 않는 편이 안전합니다. 인증 세션은 자체적인 복귀 처리 방식이 있고, 여기에 별도 이벤트를 겹치면 오히려 흐름이 꼬일 수 있습니다.

Android에서 알아두면 좋은 준비 함수

코드 위치: src/hooks/useWarmUpBrowser.ts. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.

import { useEffect } from 'react';
import { Platform } from 'react-native';
import * as WebBrowser from 'expo-web-browser';

export function useWarmUpBrowser() {
  useEffect(() => {
    if (Platform.OS !== 'android') return;
    const warming = WebBrowser.warmUpAsync().catch(() => undefined);
    return () => {
      void warming.then(() => WebBrowser.coolDownAsync()).catch(() => {});
    };
  }, []);
}

이 부분은 모든 화면에 무조건 넣는 기능은 아니지만, 자주 브라우저를 호출하는 인증 화면이나 외부 링크 중심 화면이라면 열리는 체감 속도를 다듬는 데 도움이 됩니다. mayInitWithUrlAsync는 첫 로딩 가능성이 높은 URL을 미리 알려주는 용도에 가깝고, coolDownAsync는 warmUpAsync나 mayInitWithUrlAsync로 만든 서비스 바인딩을 정리하는 역할을 합니다. 즉, 성능 최적화와 정리 코드를 한 세트로 보는 편이 맞습니다.

정리

expo-web-browser는 외부 웹페이지를 여는 도구처럼 보이지만, 실무에서는 일반 링크 열기와 인증 세션 열기를 구분해서 쓰는 것이 핵심입니다. 정책 페이지처럼 보고 돌아오면 되는 문서는 openBrowserAsync가 잘 맞고, 로그인처럼 인증 후 앱으로 다시 돌아와야 하는 흐름은 openAuthSessionAsync와 딥링크 설정을 함께 봐야 합니다. 여기에 Expo Router를 사용 중이라면 URL 기반 화면 연결을 더 자연스럽게 정리할 수 있고, Android에서는 warmUpAsync와 coolDownAsync로 호출 경험까지 다듬을 수 있습니다.

로그인 인증 흐름을 구성할 때 토큰 저장은 Expo SecureStore 토큰 저장, 인증 후 화면 복귀 구조는 Expo Router app 폴더 구조 글과 함께 보면 흐름이 더 분명해집니다.

많이 받는 질문

Q. expo-web-browser와 react-native-webview는 같은 용도로 봐도 되나요?
같게 보면 정리가 잘 안 됩니다. expo-web-browser는 시스템 브라우저 경험을 호출하는 쪽에 가깝고, react-native-webview는 앱 내부에 웹 콘텐츠를 직접 렌더링하는 쪽에 가깝습니다. 외부 정책 페이지나 인증 창은 전자가, 앱 안에서 깊게 제어할 웹 화면은 후자가 더 맞습니다.

Q. 로그인 버튼도 openBrowserAsync로 열면 안 되나요?
열리는 것 자체는 가능할 수 있지만, 인증 후 앱으로 돌아오는 흐름까지 생각하면 openAuthSessionAsync가 더 적절한 경우가 많습니다. 특히 리디렉션과 인증 결과 복귀가 필요한 경우에는 인증 세션용 메서드 기준으로 보는 편이 안전합니다.

Q. Expo Router를 쓰면 딥링크 설정을 아예 신경 쓰지 않아도 되나요?
라우팅 연결은 많이 편해지지만, 앱 스킴과 인증 제공자에 등록할 URI까지 자동으로 해결되는 것은 아닙니다. Router는 URL과 화면 연결을 단순하게 만들어주고, 인증 복귀를 성립시키는 기본 전제는 여전히 scheme와 올바른 URL 설정입니다.

문서·검증 기준 (2026-09-13): 해당 기능의 Expo 공식 문서와 코드 구조를 대조했습니다. 설치 버전은 프로젝트 Expo SDK에 맞는 npx expo install 결과와 lockfile을 기준으로 합니다. 이번 개정에서는 실제 Android·iOS 기기 실행, 원격 EAS 빌드, 외부 인증 서버 연동을 수행하지 않았습니다. 본문의 기기 동작은 독자가 확인할 기대 결과입니다.

이 글이 도움이 되었나요?

조회 중

Expo 학습 순서

필수 9개 · 전체 13개

읽음 기록 관리

전체 과정 목차 (13개)
  1. 필수 학습 · Expo 첫 앱 만들기: 설치·프로젝트 생성부터 첫 화면 실행까지
  2. 필수 학습 · Expo Safe Area 사용법: 화면 여백을 안전하게 잡는 방법
  3. 필수 학습 · Expo Status Bar 사용법: Safe Area와 화면별 설정
  4. 필수 학습 · Expo vector icons 사용법: 탭바와 커스텀 아이콘 적용하기
  5. 필수 학습 · Expo KeyboardAvoidingView 사용법: 키보드가 화면을 가릴 때 해결하기
  6. 필수 학습 · Expo Router 사용법: app 폴더와 Stack Tabs 구조 잡기
  7. 필수 학습 · Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리 현재 글
  8. 필수 학습 · Expo SecureStore 사용법: 토큰 저장과 생체 인증 처리
  9. 선택 참고 · Expo Location 사용법: 현재 위치와 백그라운드 추적 처리
  10. 선택 참고 · Expo React Native Web 사용법: 앱을 웹으로 확장하기
  11. 선택 참고 · Expo Metro unable to resolve module 오류 해결: 경로와 캐시
  12. 필수 학습 · Expo EAS Build 시작하기: Android APK 빌드부터 설치 확인까지
  13. 선택 참고 · Expo EAS Build 오류 해결: 로컬은 되는데 원격 빌드만 실패할 때

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기