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

··설명 약 17분·작성: 해비·블로그 소개

Expo에서 외부 문서 열기와 로그인 후 앱 복귀는 어떤 WebBrowser API를 사용하나요?

일반 문서는 openBrowserAsync로 열고 로그인 후 앱 복귀는 openAuthSessionAsync와 등록한 복귀 주소로 처리합니다. 브라우저 열림과 로그인 성공을 구분하고 취소·주소 불일치를 처리합니다.

적용 조건·범위: 일반 외부 문서는 openBrowserAsync로 열고, 인증 후 앱 복귀가 필요한 흐름은 openAuthSessionAsync와 등록된 복귀 주소를 함께 구성합니다. 브라우저가 열렸다는 결과는 로그인 성공이 아닙니다. 독립 실습은 공개 문서와 플랫폼별 반환값만 확인하며 실제 OAuth 인증은 별도 서버 조건이 필요합니다.

본문 근거: 해당 기능의 Expo 공식 문서

이 글에서 정리하는 내용

일반 외부 문서는 openBrowserAsync로 열고, 인증 후 앱 복귀가 필요한 흐름은 openAuthSessionAsync와 등록된 복귀 주소를 함께 구성합니다. 브라우저가 열렸다는 결과는 로그인 성공이 아닙니다. 독립 실습은 공개 문서와 플랫폼별 반환값만 확인하며 실제 OAuth 인증은 별도 서버 조건이 필요합니다.

목차

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;
  }
}

버튼 클릭 시 외부 문서를 열지만 Promise가 끝나는 시점은 플랫폼마다 다릅니다. Android의 opened는 브라우저를 열었다는 뜻이며 사용자가 닫았다는 신호가 아닙니다. iOS는 사용자가 닫으면 cancel, dismissBrowser로 닫으면 dismiss를 반환합니다. 이 반환값은 인증 토큰 수신이나 로그인 성공을 뜻하지 않습니다. 인증 복귀는 다음 절의 별도 세션 흐름으로 읽습니다.

상황 우선 선택
약관, 공지, 정책 페이지 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 빌드, 외부 인증 서버 연동을 수행하지 않았습니다. 본문의 기기 동작은 독자가 확인할 기대 결과입니다.

독립 실습: 일반 문서 열기와 호출 결과 표시

React 함수 컴포넌트·props·state와 TypeScript import를 알고 시작합니다. 첫 Expo 앱 환경 준비의 blank-typescript 프로젝트를 별도 연습 폴더에 사용합니다. 생성된 index.ts·package.json·lockfile은 유지하고 아래 App.tsx만 연결합니다. TaskFlow 코드는 필요 없습니다. npx expo start 후 대상 기기에서 확인합니다.

npx expo install expo-web-browser 후 App.tsx를 저장합니다. 일반 HTTPS 문서 호출이므로 OAuth 계정·scheme 설정은 필요 없습니다.

App.tsx 전체 파일

import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
export default function App() {
  const [busy, setBusy] = useState(false);
  const [status, setStatus] = useState('대기');
  async function open() {
    setBusy(true);
    try {
      const result = await WebBrowser.openBrowserAsync('https://expo.dev');
      setStatus(`호출 결과: ${result.type}`);
    } catch { setStatus('문서를 열지 못했습니다.'); }
    finally { setBusy(false); }
  }
  return (
    <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
      <Button title="Expo 문서 열기" disabled={busy} onPress={open} />
      <Text>{status}</Text>
    </View>
  );
}

기대 결과: 플랫폼별 반환 시점과 result.type을 기록합니다. Android의 opened를 사용자가 문서를 닫았다는 신호로 해석하지 않습니다.

자가 과제: 일반 링크를 닫고 버튼을 다시 눌러 호출 가능한지 확인합니다. 열린 문서 URL이 맞다는 사실과 인증 성공은 별개의 조건임을 설명합니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow Mobile은 할 일을 관리하는 모바일 앱입니다. 이 선택 실습에서는 기존 앱에 이번 기능을 이어서 적용합니다.

이번 주제: Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리.

누적 프로젝트: TaskFlow Mobile

시작 위치: 아래 기존 환경·파일 구성에서 시작합니다. 이 글의 완료 ZIP은 결과 확인용입니다.

시작 체크포인트: Expo vector icons 사용법: 탭바와 커스텀 아이콘 적용하기의 누적 파일까지 적용한 상태에서 이어갑니다.

npx expo install expo-web-browser 후 HelpPanel과 설정 화면을 추가·갱신합니다. 일반 공개 문서만 열며 result.type은 플랫폼별 의미로 읽습니다. 문서를 닫아도 다른 탭의 작업 상태가 유지되는지 확인합니다.

전체 파일·코드 보기

src/components/HelpPanel.tsx — 이번 회차 전체 파일

import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
export default function HelpPanel() {
  const [busy, setBusy] = useState(false);
  const [status, setStatus] = useState('대기');
  async function open() {
    setBusy(true);
    try {
      const result = await WebBrowser.openBrowserAsync('https://expo.dev');
      setStatus(`호출 결과: ${result.type}`);
    } catch { setStatus('문서를 열지 못했습니다.'); }
    finally { setBusy(false); }
  }
  return (
    <View style={{ padding: 24 }}>
      <Button title="Expo 문서 열기" disabled={busy} onPress={open} />
      <Text>{status}</Text>
    </View>
  );
}

src/app/(tabs)/settings.tsx — 이번 회차 전체 파일

import { ScrollView, Text } from 'react-native';
import HelpPanel from '../../components/HelpPanel';
export default function Settings() {
  return <ScrollView><Text>TaskFlow 설정</Text><HelpPanel /></ScrollView>;
}

다음 회차로 넘길 상태: 위 추가·수정 파일을 적용한 로컬 연습 폴더입니다. 실제 실행한 플랫폼과 결과를 기록하고 원래 독립 ZIP과 구분해 보관합니다.

아래 원래 과제의 확장 방향은 위 전체 파일이 제공하는 범위와 구분해 읽습니다. 아이콘 액션 확장·실제 세션 인증·background tracking은 추가 구현 과제입니다.

회차별 확장 질문과 확인 기준

독자 질문: 도움말 링크를 열고 앱으로 돌아왔을 때 무엇을 처리하나요?

앱에서 따라 하기

  1. 6회차 설정 화면에 도움말 버튼을 두고 expo-web-browser의 openBrowserAsync로 일반 HTTPS 페이지를 엽니다.
  2. result.type을 플랫폼별 의미로 기록합니다. Android의 opened는 열기 성공이고, iOS의 cancel·dismiss는 닫힘 방식에 따른 값입니다. 문서에서 앱으로 돌아왔는지와 버튼을 다시 누를 수 있는지는 화면에서 따로 확인합니다.
  3. 설정 화면이 그대로 유지되고 버튼을 다시 누를 수 있는지 살핍니다.

기기·빌드별 확인

예상 확인: Android/iOS 시스템 브라우저 표시는 다릅니다. 웹 브라우저와 인증 리디렉션은 별도 경로이며 실제 OAuth 로그인은 이 회차에 구현하지 않습니다.

실행 확인 범위: 위의 예상 동작은 직접 따라 할 때 확인할 기준입니다. Android/iOS 기기·에뮬레이터, 웹 브라우저, development build, EAS 원격 빌드와 APK 설치의 실제 결과는 미확인입니다. 환경별로 실행한 뒤 화면, 오류 로그, 빌드 상태를 각각 확인하세요.

오류 점검과 다음 학습

열리지 않으면 URL 형식과 네트워크를 확인하고, 일반 링크에 openAuthSessionAsync 복귀 주소를 억지로 적용하지 않습니다. URL에 민감 값을 기록하지 않습니다.

다음: 08 SecureStore에서 모의 세션을 저장·복원·삭제합니다.

글 소개·학습 준비·전체 목차

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

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 13개 · 필수 9개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기