React Native
2026-03-30
React Native 앱-웹뷰 토큰 연동: postMessage로 accessToken 주고받기
앱과 웹뷰 사이에서 토큰을 어떻게 주고 받을까

앱에서 웹뷰를 사용할 때 가장 고민되는 부분 중 하나가 바로 로그인 인증 처리에요.
앱은 AsyncStorage로 토큰을 관리하고, 웹은 쿠키로 토큰을 관리하는데 이 둘을 어떻게 연결할까?
이 글에서는 postMessage를 활용해 앱↔웹뷰 간 토큰을 주고받는 방법과, 토큰 만료 처리까지 어떻게 구현했는지 공유해 볼게요.
전체 흐름 한눈에 보기
[앱 로그인] → accessToken 발급
↓
[WebView 로드] → onLoadEnd로 웹에 postMessage로 토큰 전달
↓
[웹 AuthProvider] → 받은 토큰을 쿠키에 저장
↓
[accessToken 만료] → 웹이 앱에 getRefreshToken 요청
↓
[앱] → 리프레시 API 호출 후 새 토큰을 웹으로 전달
앱은 토큰을 갖고 있고, 웹은 앱에 의존해서 토큰을 받아 사용하는 구조예요.
이 프로젝트에서는 서버에서 refreshToken 쿠키를
Max-Age=30일로 설정했기에, iOS가 이를 영구 쿠키로 인식해 디스크에 보관합니다. 앱을 재 실행해도 쿠키가 그대로 남아있어서 별도의 Cookie 복원 로직이 필요 없어요. (관련 포스트: React Native 앱-웹뷰 로그인 연동: iOS Cookie 유실 문제 해결하기)
로그인 처리
앱 측 (React Native)
로그인 API 응답으로 받은 accessToken은 AsyncStorage에 저장합니다.
웹뷰 페이지 로드 후 토큰 전달
웹뷰가 로드되면(onLoadEnd) 앱이 먼저 웹으로 accessToken을 보내줘요.
const { value: accessToken } = useAppAsyncStorage('accessToken');
const handleOnLoadEnd = async () => {
try {
const cookies =
Platform.OS === 'ios'
? await CookieManager.get('https://myapp.com', true)
: {};
const webviewInitData = {
type: 'webviewInit',
data: {
cookies: { accessToken: cookies?.accessToken?.value || accessToken },
},
};
webviewRef.current?.postMessage(JSON.stringify(webviewInitData));
} catch (error) {
console.error('Failed to webviewInitData:', error);
}
};
<WebView onLoadEnd={handleOnLoadEnd} ... />iOS는 CookieManager로 쿠키에서 토큰을 읽고, Android는 AsyncStorage에 저장한 accessToken을 사용해요.
웹 측에서 토큰 수신 (Next.js)
웹의 AuthProvider에서 message 이벤트를 리스닝하다가 webviewInit 타입 메시지를 받으면 토큰을 쿠키에 세팅해요.
// src/app/_components/AuthProvider.tsx
import { setCookie } from 'cookies-next/client';
useEffect(() => {
const handleWebViewMessage = (event: Event) => {
const rawData = (event as MessageEvent).data;
const { type, data } = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
if (type === 'webviewInit') {
const { cookies: receivedCookies } = data;
if (receivedCookies) {
setCookie('accessToken', receivedCookies?.accessToken, { maxAge });
}
}
};
// Android는 document, iOS는 window에 이벤트 등록
const messageTarget = window.ReactNativeWebView ? (/android/i.test(navigator.userAgent) ? document : window) : null;
if (messageTarget) {
messageTarget.addEventListener('message', handleWebViewMessage);
}
return () => {
if (messageTarget) {
messageTarget.removeEventListener('message', handleWebViewMessage);
}
};
}, []);참고로 Android와 iOS에서 message 이벤트를 등록하는 대상이 달라요.
- Android:
document에 이벤트 등록 - iOS:
window에 이벤트 등록
accessToken 만료 처리
accessToken은 만료되면 401을 반환해요.
웹은 리프레시 토큰을 갖고 있지 않기 때문에, 앱에 토큰 재발급을 요청합니다.
웹 → 앱: 리프레시 요청
공통 API 훅에서 401을 감지하면 앱으로 getRefreshToken 메시지를 보내고, 앱이 응답할 때까지 Promise로 대기해요.
// 토큰 재발급 요청
const requestRefreshToken = (): Promise<string> =>
new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('토큰 요청 타임아웃'));
}, 10000);
const handler = (e: MessageEvent) => {
if (e.data.type !== 'accessToken') return;
clearTimeout(timeout);
window.removeEventListener('message', handler);
resolve(e.data.data);
};
window.addEventListener('message', handler);
rnwPost({ targetFunc: 'getRefreshToken' }); // 앱에 요청
});
// 재발급 토큰 받아서 처리
const newToken = await requestRefreshToken();
setCookie('accessToken', newToken, { maxAge });10초 내에 응답이 없으면 타임아웃 처리해요.
앱 → 웹: 새 토큰 전달
앱에서 getRefreshToken 메시지를 받으면 리프레시 API를 호출하고, 받은 토큰을 injectJavaScript로 웹에 전달합니다.
case 'getRefreshToken': {
const newToken = await getRefreshToken();
const script = `
window.postMessage({ type: 'accessToken', data: '${newToken}' }, '*');
`;
webviewRef.current?.injectJavaScript(script);
break;
}마무리
웹뷰와 앱 사이의 토큰 연동은 처음 설계할 때 어떻게 흐름을 잡을지가 제일 중요한 것 같아요.
이 구조의 핵심은 웹은 토큰을 직접 관리하지 않고 앱에 위임한다는 것이에요. 웹에서 토큰이 필요한 모든 상황(토큰만료, 세션종료)을 앱으로 위임하니 관리 포인트가 앱 한 곳으로 집중되어 일관성 있게 처리할 수 있었어요.
비슷한 구조를 설계하고 있다면 참고가 되길 바랍니다!
Tags: