Next.js 웹서비스를 React Native WebView 앱으로 전환하기 — Expo부터 스토어 출시까지 2026 완전 가이드
Next.js 웹서비스를 React Native WebView 앱으로 전환하기 — Expo부터 스토어 출시까지 2026 완전 가이드
이미 Next.js로 웹서비스를 운영 중인데, 어느 날 이런 요구가 들어옵니다.
“앱스토어/플레이스토어에도 올려야 해요.”
“푸시 알림도 필요해요.”
“모바일에서 더 ‘앱 같은’ 경험이었으면 좋겠어요.”
그때 가장 먼저 떠오르는 접근이 React Native로 앱을 새로 만드는 것이지만, 실제로는 “처음부터 네이티브 UI를 전부 다시”가 아니라 웹을 WebView로 감싸는 래핑 앱으로 충분한 케이스가 많습니다.
이 글은 Next.js 웹서비스를 React Native(WebView) 앱으로 전환하는 과정을, “앱을 실제로 출시해서 운영”하는 관점에서 아주 상세히 정리합니다.
이미 비슷한 목적의 글로 Android는 TWA/Capacitor 중심, iOS는 Capacitor 중심으로 다뤘습니다. 먼저 큰 그림에서 방법 선택이 필요하다면 아래 글도 함께 보세요.
0. 전제: WebView 앱이 “가장 좋은 선택”인 경우
React Native WebView 래핑은 개발 속도와 웹 재사용률이 장점입니다. 대신, WebView가 포함된 앱은 스토어 정책·성능·UX에서 종종 불리합니다.
0.1 WebView 래핑이 잘 맞는 경우
- 이미 웹에서 핵심 UX가 완성되어 있고, 모바일은 “스토어 유통 + 푸시 + 딥링크”가 주 목적
- 화면 전환/애니메이션보다 콘텐츠/업무 흐름이 중요한 서비스
- SSR/서버 기능이 많아 정적 번들 방식이 부담되고, 앱은 URL 로딩으로 운영하고 싶음
0.2 WebView 래핑이 맞지 않는 경우(처음부터 RN/네이티브 고려)
- 스크롤/제스처가 많은 고성능 피드형 UI(대형 커뮤니티, 쇼츠/릴스류)
- 카메라·BLE·백그라운드 작업 등 네이티브 API가 핵심 가치
- 결제/구독이 앱스토어 IAP 정책에 강하게 묶이는 상품(특히 디지털 콘텐츠/기능 언락)
0.3 방법 선택 한 장 요약 (Android/iOS 공통)
| 방식 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
| React Native + WebView | JS/TS로 iOS+Android 한 번에, 네이티브 기능을 필요만큼만 추가 가능 | 정책/성능/UX에서 “웹뷰 앱” 티가 나면 심사 리스크 | 푸시/딥링크/로그인만 얹고 빠르게 출시 |
| Capacitor | 웹 중심(Next.js) + 플러그인, 설정이 단순한 편 | iOS/Android 각 빌드 파이프라인 세팅 필요 | “웹앱을 앱으로”에 집중, RN 생태계는 불필요 |
| Android TWA | Chrome 엔진, PWA 강점(오프라인/푸시), 웹과 동일 | iOS 불가(대체가 없음) | Android만 빠르게 스토어 유통 |
| 완전 RN/네이티브 | 최고 UX/성능, 스토어 친화적 | 개발 비용 큼 | 모바일이 핵심 제품(웹은 보조) |
이 글은 React Native + WebView를 다루며, Capacitor/TWA 대비 장단점과 “스토어 출시 관점”에서 막히는 포인트를 중심으로 설명합니다.
1. 아키텍처 결정: WebView는 “원격 URL” 로딩이 정답(대부분)
Next.js 웹서비스가 이미 운영 중이라면, 앱은 웹을 원격 URL로 로드하는 방식이 가장 현실적입니다.
1.1 원격 URL 방식 (대부분의 서비스에서 추천)
- 앱은
https://app.example.com을 WebView로 로딩 - 웹 배포 = 앱 UI 즉시 반영(스토어 심사 없이 수정 가능)
- Next.js의 SSR/Server Actions/API Route 등 서버 기능 그대로 유지
1.2 번들(정적) 방식은 “앱 오프라인”이 핵심일 때만
정적 번들(Next.js export)을 앱에 넣는 방식은, “오프라인에서도 화면이 떠야 한다” 같은 목적이 있어야만 고려할 만합니다. 대부분의 SaaS/콘텐츠 서비스는 원격 URL이 더 단순하고 운영 비용이 낮습니다.
2. 프로젝트 시작: Expo로 빠르게 WebView 앱 뼈대 만들기
React Native는 크게 Expo(관리형) vs Bare(네이티브 직접) 두 길이 있습니다. WebView 래핑 앱은 Expo가 시작점으로 매우 좋습니다.
2.1 Expo로 프로젝트 생성
npx create-expo-app myapp
cd myapp
2.2 필수 라이브러리 설치
npm i react-native-webview
npm i @react-navigation/native @react-navigation/native-stack
npm i react-native-safe-area-context react-native-screens
Expo 환경에서 react-native-screens 등이 요구하는 설정은 템플릿이 대부분 해결해줍니다.
2.2.1 (권장) EAS 빌드 준비: 실제 스토어 배포용 파이프라인
개발 단계는 Expo Go로도 되지만, 푸시/딥링크/권한이 들어가는 순간부터는 Dev Build(EAS) 로 가는 편이 깔끔합니다.
npm i -g eas-cli
eas login
eas build:configure
그 다음 app.json(또는 app.config.ts)에 최소 메타데이터를 정리합니다.
{
"expo": {
"name": "내서비스",
"slug": "myapp",
"scheme": "myapp",
"ios": { "bundleIdentifier": "com.example.myapp" },
"android": { "package": "com.example.myapp" }
}
}
딥링크를 제대로 하려면
scheme은 초기에 확정하는 편이 안전합니다(나중에 바꾸면 링크/마케팅 자산/설정이 다 깨집니다).
2.3 가장 단순한 WebView 화면부터
// app/App.tsx (또는 expo-router를 쓰면 구조가 다를 수 있음)
import React from "react";
import { SafeAreaView, Platform } from "react-native";
import { WebView } from "react-native-webview";
const APP_URL = "https://app.example.com";
export default function App() {
return (
<SafeAreaView style={{ flex: 1 }}>
<WebView
source={{ uri: APP_URL }}
originWhitelist={["*"]}
javaScriptEnabled
domStorageEnabled
setSupportMultipleWindows={false}
allowsBackForwardNavigationGestures={Platform.OS === "ios"}
startInLoadingState
/>
</SafeAreaView>
);
}
여기까지는 “진짜로” 30분이면 됩니다. 문제는 이제부터입니다. 실제 앱이 되려면 아래 기능들이 필요합니다.
3. WebView 앱이 반드시 갖춰야 하는 10가지(실전 체크리스트)
WebView 래핑은 “그냥 띄우면 끝”이 아닙니다. 서비스마다 편차는 있지만, 운영 가능한 앱이 되려면 보통 아래가 필요합니다.
| 항목 | 왜 필요한가 | 실패하면 |
|---|---|---|
| 네비게이션(뒤로가기/딥링크) | 앱의 기본 UX | 로그인 후 화면 복귀 실패 |
| 외부 링크 처리 | 결제/로그인/OAuth | WebView에서 깨지거나 심사 리젝 |
| 로그인 세션(쿠키/토큰) | 웹과 앱의 상태 동기화 | 로그인이 매번 풀림 |
| 브릿지(postMessage) | 앱에서 푸시 토큰 등 전달 | 푸시 등록 불가 |
| 푸시 알림 + 딥링크 | 앱을 ‘앱’으로 만드는 핵심 | 푸시 눌러도 홈으로만 감 |
| 파일 업로드/다운로드 | 프로필/첨부 | 업로드 버튼이 안 됨 |
| 에러/오프라인 화면 | 네트워크 품질 대응 | 빈 흰 화면(white screen) |
| 로딩/스플래시 | 첫 인상, 심사 스크린샷 | “웹 로딩 앱”처럼 보임 |
| 보안(인앱 브라우저 정책) | 민감 기능 보호 | 세션 탈취/URL 스푸핑 |
| 스토어 정책/메타데이터 | 심사 통과 | “웹뷰만 있는 앱” 리젝 |
이제 각 항목을 구현합니다.
4. 내비게이션: 뒤로가기/새 창/외부 링크 처리
4.1 Android 하드웨어 뒤로가기
WebView는 기본적으로 히스토리를 갖지만, RN에서 하드웨어 back을 연결해야 합니다.
import React, { useEffect, useRef, useState } from "react";
import { BackHandler, SafeAreaView } from "react-native";
import { WebView } from "react-native-webview";
const APP_URL = "https://app.example.com";
export default function App() {
const webviewRef = useRef<WebView>(null);
const [canGoBack, setCanGoBack] = useState(false);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
if (canGoBack) {
webviewRef.current?.goBack();
return true;
}
return false;
});
return () => sub.remove();
}, [canGoBack]);
return (
<SafeAreaView style={{ flex: 1 }}>
<WebView
ref={webviewRef}
source={{ uri: APP_URL }}
onNavigationStateChange={(nav) => setCanGoBack(nav.canGoBack)}
/>
</SafeAreaView>
);
}
4.2 외부 링크는 시스템 브라우저로 보내기(기본 원칙)
결제, 약관, OAuth 등은 WebView 안에서 깨지거나 정책 리스크가 생길 수 있어, 특정 도메인/경로는 외부 브라우저로 여는 편이 안전합니다.
import * as Linking from "expo-linking";
function shouldOpenExternally(url: string) {
try {
const u = new URL(url);
if (u.hostname.endsWith("example.com")) return false;
return true;
} catch {
return false;
}
}
그리고 WebView의 onShouldStartLoadWithRequest에서 차단하고 외부로 보냅니다.
<WebView
source={{ uri: APP_URL }}
onShouldStartLoadWithRequest={(req) => {
if (shouldOpenExternally(req.url)) {
Linking.openURL(req.url);
return false;
}
return true;
}}
/>;
결제/구독이 걸린 앱은 특히 정책이 민감합니다. iOS/Android 배포 글의 “결제/디지털 상품 정책 위반” 섹션은 꼭 참고하세요.
4.3 target=_blank / window.open / tel: 같은 스킴 처리
실서비스에서는 아래 같은 링크가 섞입니다.
target="_blank"링크(결제/약관/외부 도움말)mailto:support@...,tel:+82...intent://(Android) / 커스텀 스킴
원칙은 동일합니다.
- 내 도메인(앱 본문): WebView에서 열기
- 외부/특수 스킴: 시스템 핸들러로 열기
shouldOpenExternally에 스킴 기준도 추가해 두면 운영 중 사고가 줄어듭니다.
function isSpecialScheme(url: string) {
return (
url.startsWith("mailto:") ||
url.startsWith("tel:") ||
url.startsWith("sms:")
);
}
그리고 onShouldStartLoadWithRequest에서 우선 처리합니다.
onShouldStartLoadWithRequest={(req) => {
if (isSpecialScheme(req.url)) {
Linking.openURL(req.url);
return false;
}
if (shouldOpenExternally(req.url)) {
Linking.openURL(req.url);
return false;
}
return true;
}}
5. WebView 브릿지: 웹(Next.js) ↔ 앱(React Native) 연결하기
앱이 “앱다워지려면” WebView 내부의 웹앱이 네이티브 정보를 받아야 합니다. 대표적으로:
- 푸시 토큰
- 앱 버전/플랫폼
- 딥링크로 들어온 경로
5.1 RN → Web: postMessage 보내기
const injectedJS = `
window.__APP__ = { platform: "rn-webview" };
true;
`;
<WebView
injectedJavaScript={injectedJS}
onLoadEnd={() => {
const payload = JSON.stringify({
type: "APP_READY",
platform: "ios_or_android",
appVersion: "1.0.0",
});
webviewRef.current?.postMessage(payload);
}}
/>;
구현 포인트: “첫 로드 이후”에 보내야 웹이 수신 준비가 되어 있습니다.
5.2 Web → RN: 웹에서 window.ReactNativeWebView.postMessage
Next.js 쪽에서 아래처럼 보내면 RN의 onMessage로 들어옵니다.
// Next.js (클라이언트)
function postToApp(message: any) {
const payload = JSON.stringify(message);
// @ts-expect-error: injected by RN WebView
window.ReactNativeWebView?.postMessage(payload);
}
postToApp({ type: "OPEN_NATIVE_SETTINGS" });
RN에서 수신:
<WebView
onMessage={(event) => {
try {
const msg = JSON.parse(event.nativeEvent.data);
if (msg.type === "OPEN_NATIVE_SETTINGS") {
// 필요 시 네이티브 화면으로 이동
}
} catch {
// ignore
}
}}
/>;
5.3 “브릿지 프로토콜”을 문서화하라
운영이 시작되면 웹과 앱의 릴리즈 속도가 다릅니다. 그래서 브릿지 메시지는 아래 원칙으로 고정해두는 게 중요합니다.
- 메시지는
{ type: string, payload?: object, version?: number }형태 고정 - 앱이 못 알아듣는 메시지는 무시(크래시 금지)
- 웹이 앱을 못 찾으면(브라우저 접속) 대체 UX 제공
6. 로그인/세션: “쿠키가 풀리는 앱”이 가장 흔한 실패
WebView 앱에서 자주 터지는 이슈는 다음입니다.
- 로그인 후 새로고침하면 풀림
- 결제/OAuth 후 돌아오면 세션이 끊김
- iOS에서 특히 쿠키 정책이 까다로움
6.1 원칙: 인증은 가능하면 “웹”이 담당하고 앱은 래핑만
가장 단순한 전략은:
- 로그인은 웹에서 진행
- 앱은 쿠키/세션을 WebView에 유지
- 앱이 별도 토큰을 저장해서 API를 직접 치지 않는다(초기 단계)
6.1.1 WebView 쿠키 옵션(특히 iOS)
서비스에 따라 아래 옵션이 필요할 수 있습니다.
sharedCookiesEnabled: 시스템 쿠키 저장소 공유(iOS에서 도움이 되는 경우가 있음)thirdPartyCookiesEnabled: (Android) 서드파티 쿠키가 필요한 구조라면 고려(가능하면 회피)
<WebView
source={{ uri: APP_URL }}
sharedCookiesEnabled
thirdPartyCookiesEnabled={false}
/>
최선은 “서드파티 쿠키가 필요 없는 도메인 설계”입니다.
app.example.com과api.example.com처럼 분리하더라도, 인증은 가능하면app.example.com의 1st-party 컨텍스트에서 끝내세요.
6.1.2 Next.js 인증 쿠키 설정(실전 체크)
앱(WebView)에서 로그인 유지가 불안정하다면, 웹 쪽에서 아래를 우선 점검합니다.
- HTTPS +
Secure쿠키 SameSite정책 (OAuth/리디렉션이 걸리면 영향)- 여러 서브도메인에서 공유해야 하면
Domain=.example.com을 명확히(필요할 때만)
프레임/임베드 구조를 쓰면 정책이 급격히 어려워지므로, WebView 앱에서는 가능한 한 정상 top-level 네비게이션으로 인증 흐름을 설계하는 편이 안전합니다.
6.2 리디렉션/콜백 URL을 정리하라
OAuth/결제 콜백은 “앱 스킴으로 돌아오기”를 꿈꾸기 전에, 먼저 웹에서 완결되게 하는 것이 운영적으로 안정적입니다.
- 좋은 패턴:
https://app.example.com/auth/callback에서 모든 처리를 끝냄 - 그 다음에 “앱 내부로 돌아오기”는 딥링크로 개선
7. 푸시 알림 + 딥링크: WebView 앱의 핵심 가치
WebView 앱이 웹과 다르게 “설치할 이유”를 만들어주는 가장 강력한 기능은 보통 푸시 알림입니다.
여기서는 “구현 가능한 수준”으로 흐름을 잡습니다. (서버 발송은 사용하는 푸시 공급자/백엔드에 따라 달라집니다.)
7.1 흐름(개념)
1) 앱이 푸시 토큰을 획득
2) WebView 브릿지로 토큰을 웹에 전달
3) 웹이 서버 API로 토큰을 저장
4) 서버가 이벤트 발생 시 푸시 발송
5) 사용자가 알림을 탭하면 특정 URL로 딥링크 → WebView가 해당 경로 로드
7.1.1 Expo 푸시 토큰 획득(앱)
npm i expo-notifications expo-device
// push.ts (개념 예시)
import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
export async function getPushToken(): Promise<string | null> {
if (!Device.isDevice) return null; // 시뮬레이터는 제한이 있음
const { status: existing } = await Notifications.getPermissionsAsync();
const final =
existing === "granted"
? existing
: (await Notifications.requestPermissionsAsync()).status;
if (final !== "granted") return null;
const token = await Notifications.getExpoPushTokenAsync();
return token.data;
}
앱에서 토큰을 얻으면 WebView 브릿지로 웹에 넘깁니다.
const token = await getPushToken();
if (token) {
webviewRef.current?.postMessage(
JSON.stringify({ type: "PUSH_TOKEN", provider: "expo", token })
);
}
웹(Next.js)은 이 메시지를 받아 서버 API로 저장합니다.
7.2 “딥링크 URL”을 제품의 URL로 통일
딥링크를 설계할 때, 내부 화면을 myapp:// 같은 전용 스킴으로만 만들면 웹과 분기돼서 운영 비용이 급증합니다.
- 추천: 딥링크는 결국
https://app.example.com/notifications/123같은 웹 URL로 귀결 - 앱은 “그 URL을 열어준다”만 책임
7.2.1 딥링크를 WebView로 전달하는 구현 패턴
딥링크/알림 탭으로 들어온 URL을 앱이 받으면, 결국 WebView를 그 경로로 보내야 합니다.
- 이미 WebView가 떠 있으면:
webviewRef.current?.injectJavaScript(...)또는postMessage로 “이 URL로 이동” 이벤트 전달 - 아직 WebView가 안 떠 있으면: 앱 초기
initialUrl로 보관했다가 첫 로드 후 전달
개념 예시:
import * as Linking from "expo-linking";
export async function getInitialIncomingUrl(): Promise<string | null> {
const url = await Linking.getInitialURL();
return url;
}
그리고 들어온 URL을 서비스 URL로 매핑합니다.
function mapIncomingToAppUrl(incoming: string) {
// 예: myapp://open?path=/notifications/123
const parsed = Linking.parse(incoming);
const path = typeof parsed.queryParams?.path === "string" ? parsed.queryParams.path : "/";
return `https://app.example.com${path}`;
}
“앱 안의 화면 ID”로 이동시키기보다, 웹 URL(path) 로 귀결시키면 운영이 쉬워집니다. 웹과 앱이 같은 라우팅을 공유할 수 있기 때문입니다.
8. 파일 업로드/다운로드: 심사 직전에 터지는 지뢰
WebView 래핑에서 사용자들이 제일 먼저 발견하는 버그가 “업로드 버튼이 안 눌린다” 입니다.
8.1 업로드는 서비스 요구사항을 미리 정의
- 프로필 사진 업로드가 필요한가?
- 첨부파일이 필요한가?
- 카메라 캡처가 필요한가?
필요한 경우, WebView가 파일 선택/카메라 권한을 제대로 처리하는지 iOS/Android 각각 확인해야 합니다.
8.2 다운로드는 “외부 브라우저로 보내기”가 가장 안전
PDF/CSV 다운로드를 WebView 내부에서 처리하려 하면 플랫폼별 이슈가 많습니다. 초기에는 다운로드 링크를 외부로 보내는 것이 안정적입니다.
9. 성능/UX: “웹뷰 앱 같다”는 느낌을 줄이는 방법
스토어 심사에서 “웹뷰만 감싼 앱”은 리젝 사유가 될 수 있습니다. 실제로는 WebView를 쓰더라도, 아래를 챙기면 앱 퀄리티가 확 달라집니다.
- 첫 진입 스플래시 + 로딩 상태(화이트 스크린 금지)
- 오프라인/서버 장애 시 에러 화면(재시도 버튼)
- 상단 Safe Area/노치 처리
- 앱 아이콘·런치 스크린·권한 설명 문구
9.1 화이트 스크린 방지(최소 UX)
최소한 아래는 기본으로 넣는 편이 안전합니다.
startInLoadingState- 타임아웃 기반의 “네트워크 문제” 안내
onError/onHttpError로 사용자 메시지 출력
이걸 빼면, 장애 상황에서 사용자는 그냥 “하얀 화면”만 보고 앱을 지웁니다.
10. 스토어 출시 체크리스트 (WebView 앱 전용)
10.1 iOS(App Store)에서 자주 보는 리젝 포인트
- 기능이 “웹사이트와 동일”인데, 앱만의 가치가 설명되지 않음
- 결제/구독이 정책을 우회하거나(외부 결제 유도), 설명이 불명확
- 로그인/콘텐츠 접근이 리뷰어에게 제공되지 않음(데모 계정 필요)
- 개인정보 수집 고지(Privacy Policy) 누락
관련 포인트는 iOS 배포 글의 “심사 체크리스트”가 큰 도움이 됩니다.
10.2 Android(Google Play)에서 자주 보는 리젝/경고 포인트
- WebView 기반인데 앱 설명이 과장됨
- 권한을 과도하게 요청(필요 최소로)
- 개인정보/데이터 안전 섹션 미기재
Android 배포 글의 스토어 제출 파트도 함께 확인하세요.
11. 배포/업데이트 전략: 앱은 느리고 웹은 빠르다
WebView 앱의 장점은 “웹을 배포하면 앱 화면이 바로 바뀐다”입니다. 그래서 앱 배포는 아래에 집중하는 것이 효율적입니다.
- WebView 컨테이너 안정성
- 푸시/딥링크/권한/브릿지
- 크래시/로그 수집
UI/콘텐츠/대부분의 기능은 웹에서 빠르게 개선합니다.
11.1 EAS로 스토어 빌드 만들기(실전 흐름)
eas build -p android
eas build -p ios
권장 운영 패턴:
- WebView 컨테이너가 바뀌는 경우만 앱 업데이트
- 기능/화면/카피는 웹 배포로 빠르게 개선
12. 자주 터지는 문제(운영 트러블슈팅)
12.1 “로그인이 자꾸 풀려요”
- 웹 쿠키 속성(
Secure,SameSite,Domain) 점검 - 리디렉션/OAuth가 “외부 브라우저”로 빠졌다가 돌아오는지 점검
- WebView 쿠키 옵션(
sharedCookiesEnabled) 적용 여부 점검
12.2 “결제 페이지가 깨져요”
- 결제/인증 플로우는 외부 브라우저로 분리하는 전략을 우선 고려
- 스토어 정책(특히 iOS)에서 우회로 보이지 않도록, 앱 메타데이터/문구 정리
12.3 “푸시 토큰은 받는데, 눌러도 원하는 화면으로 안 가요”
- 딥링크 → URL 매핑 규칙을 한 군데에서 관리
- 알림 payload에는 웹 URL(path) 을 넣고 앱은 그대로 로드
13. 정리 — WebView 앱은 “앱의 최소 기능 + 웹의 속도” 조합
Next.js 웹서비스를 React Native(WebView)로 전환할 때 성공 확률을 올리는 핵심은 단순합니다.
- 원격 URL 로딩으로 운영을 단순화하고
- 앱은 푸시·딥링크·외부 링크 처리·브릿지만 정확히 완성하며
- 심사에서 문제가 되는 포인트(결제/정책/가치 설명)를 문서와 메타데이터로 선제 대응한다
WebView는 “어중간한 타협”이 아니라, 올바르게 설계하면 1인/소팀이 글로벌 스토어 유통까지 뚫는 가장 빠른 경로가 될 수 있습니다.