
💡 읽기 전 필요한 지식 💡
- 웹뷰(앱 안에 띄우는 브라우저 창)에서 웹 페이지가 돌아간다는 정도의 감 (2편 참고)
- Firebase 로그인, idToken(구글이 서명한 임시 토큰)이라는 개념에 대한 가볍운 이해
- Expo / Expo Go / Development Build라는 단어를 들어본 적이 있으면 더 좋음 (몰라도 본문에서 설명한다)
📖 읽고 나면 얻을 수 있는 것 📖
- 구글이 왜 웹뷰에서의 로그인을 막는지, 그걸 어떻게 우회하는지 이해하게 된다
- 웹뷰와 네이티브(RN)가 JS Bridge로 역할을 나누어 소셜 로그인을 처리하는 구조를 알게 된다
- Expo Go와 Development Build의 차이, 그리고 왜 네이티브 모듈이 필요하면 Development Build를 써야 하는지 알게 된다
일단 이 글을 읽기 전에 이전 글들을 읽으면 도움이 될 것이다.
[tolli #1] 무거운 성경암송 앱이 싫다
💡 읽기 전 필요한 지식 💡- 모바일 앱이 "기획 -> 개발 -> 출시"의 단계를 거친다는 정도의 큰 그림- 듀오링고 같은 학습 앱을 한 번이라도 써본 경험- "타겟 사용자"나 "문제 정의"라는 말이 어떤
bbin-guuuu.tistory.com
[tolli #2] 왜 React Native + WebView 하이브리드였을까?
💡 읽기 전 필요한 지식 💡- React와 React Native(RN)가 서로 다른 것이라는 정도의 구분- SSR과 CSR이라는 말을 들어본 경험- 앱이 웹뷰(앱 안에 띄우는 브라우저 창)로 웹을 띄울 수 있다는 사실📖 읽
bbin-guuuu.tistory.com
[tolli #3] switch로 쪼갠 화면이 협업 단위가 됐다
💡 읽기 전 필요한 지식 💡- 컴포넌트라는 개념에 대한 가벼운 이해- switch문이 값에 따라 분기하는 문법이라는 정도의 지식- 여러 명이 하나의 레포에서 함께 개발한다는 상황에 대한 감📖 읽
bbin-guuuu.tistory.com
[tolli #4] AOS 애플로그인 트러블슈팅
💡 읽기 전 필요한 지식 💡- OAuth 소셜 로그인이 "토큰을 주고받는 과정"이라는 정도의 큰 그림- 딥링크(tolli:// 같은 주소로 앱을 여는 것)와 WebView가 메시지를 주고받는다는 개념- Firebase 로그인
bbin-guuuu.tistory.com
일단 바로 본론으로 들어가보겠다.
필자는 먼저 테스트로 웹뷰에 구글로그인 버튼을 넣어 onClick시에 로그인 페이지로 접근하도록 구현해보았다.

근데 웹뷰에서 로그인 페이지에 접근하려 하자 위와 같이 에러가 났다.
처음엔 코드 자체에 문제가 있나 싶어서 몇줄 안되는 코드를 계속해서 살펴보았는데 이상은 없다고 판단했다.
그래서 GitHub에 있는 issue들이나 reddit과 같은 커뮤니티를 돌아가보며 사실 수집을 좀 해봤다.
그래서 찾아낸건 구글이 웹뷰에서의 로그인을 기본적으로 차단한다고 한다.
이에 대한 이유는 무엇일까?
웹뷰는 외부 코드의 실행과 콘텐츠 표시를 허용하기 때문에 악의적인 공격에 노출될 가능성이 높다고 한다.
그래서 사용자의 데이터 자체를 보호하고 피싱 공격을 예방하기 위해 구글은 웹뷰에서의 로그인을 제한한다는 것을 알게 됐다.
주제에서 벗어난 얘기지만 위의 이유에 대해 좀 더 설명해보고자 한다.
1. 외부 코드의 실행?
// 앱 개발자가 짠 코드
webView.addJavascriptInterface(myApp, "App")
// 이 순간부터 WebView 안의 웹 페이지 JS에서
/ 네이티브 앱 함수 호출
window.App.getUserData()
// 연락처 접근
window.App.getContacts()
// 파일 접근
window.App.readStorage()
위 코드처럼 악성앱의 개발자가 웹뷰 안에서 실행되는 JS코드를 예시의 첫 줄처럼 심어서 네이티브 안드로이드 객체(myApp)을 "App"이라는 이름으로 호출할 수 있게 연결해주는 메서드를 만들 수 있다.
그럼 예시코드와 같이 웹뷰 페이지의 JS가 저렇게 유저 데이터, 연락처, 파일등에 접근해서 네이티브 앱에게 보내고, 네이티브 앱이 서버로 전송하는 구조가 되버려서 탈취가 가능하다.
2. 콘텐츠 표시?
일반 브라우저는 어떤 URL이 열려있는지 주소창에 항상 노출되지만,
웹뷰는 앱이 주소창 없이 아무 URL이나 조용히 띄울 수 있어서 가짜 구글 로그인창을 띄울 수 있다...
첫 번째 시도 (외부 브라우저를 통해)
첫 번째로 외부 브라우저를 통해 해결해보기로 했다.
- 구글로그인 버튼을 눌렀을 때 외부 브라우저를 열어 로그인하고,
- 로그인이 끝나면 딥링크(앱을 열 수 있는 URL)로 토큰과 함께 웹뷰 앱을 다시 열고,
- 웹뷰에 postMessage로 결과를 전달하는 흐름이었다.
그런데 이 방법은 딥링크로 앱을 다시 열었을 때 로그인에 쓰인 외부 브라우저가 그대로 남는다는 단점이 컸다...
사용자 입장에서는 앱과 브라우저가 따로 떠 있는 좋지 않은 경험이라, 이건 아니다 싶었다.
두 번째 시도 (네이티브 로그인과 JS Bridge의 조합)
그래서 방향을 바꿔보았다.
로그인 자체는 네이티브로 구현하되,
그 결과를 JS Bridge를 통해 웹뷰로 넘겨주기로 했다.
- 웹뷰의 구글로그인버튼을 누르면 네이티브에 postMessage로 동작을 알리고,
- 네이티브가 라이브러리로 로그인해서 idToken을 얻은 뒤 postMessage로 다시 웹뷰로 넘겨주고,
- 웹뷰는 message를 수신하고 그 토큰으로 Firebase 로그인을 완성하는 그림!
하지만 여기서 react-native-google-signin이란 네이티브 라이브러리를 사용했는데,
이 라이브러리는 Expo Go와 호환이 되지 않았고, 네이티브 모듈이라 아래와 같이 에러가 났다.

따라서 위와의 방법은 같게하되, expo-auth-session을 쓰기로 결정했다.
(잠시 이전 글에서 언급이 된 라이브러리이지만 다시 설명하자면 웹이 따로 열리는 게 아니라 앱 안에서 브라우저가 열리는 형태라 더 괜찮아 보였다!)
세 번째 시도 (redirect URI와 deprecated된 proxy)
일단 expo-auth-session라이브러리를 통해 로그인은 성공했다.

다음 스텝으로 Google Cloud Console에서 redirect URI를 설정해주고자 했다.
하지만 이번엔 redirect URI가 IP 주소 형식을 허용하지 않아 400 에러가 났다...
이 문제를 해결해보고자 Expo가 운영하는 중간 서버(Expo Auth Proxy)를 써서(Reddit에 많은 선례가 있었음)
정상적인 도메인으로 인식되게 하려 했는데,
이 proxy 방식은 이미 deprecated된 것을 확인했다...
Expo AuthSession Redirect Proxy
Expo AuthSession Redirect Proxy This service is deprecated. Instead, configure your app to navigate to the third-party authentication provider directly instead of using this service. This is both more reliable and secure. Configure the authentication provi
auth.expo.io
이 시점부터 필자는 Expo Go를 과감히 버리고 Development Build를 사용하기로 했다.
즉, 직접 안드로이드, iOS 시뮬레이터 혹은 실기기에서 테스트하고자 했다는 것.
pnpm ios # iOS 시뮬레이터
pnpm android # Android 에뮬레이터
pnpm ios:device # iOS 실기기
pnpm android:device # Android 실기기
위 명령어들은 Development Build를 각 환경에 올려서 테스트하는 스크립트이다.
Expo Go를 포기하는 대신, 네이티브 모듈들을 자유롭게 쓸 수 있게 되었고,
실제로도 더 많은 자유도를 가지고 개발할 수 있었던 것 같다.
최종해결 (네이티브 로그인과 JS Bridge 조합 완성)
최종적으로는 Development Build 환경에서
@react-native-google-signin/google-signin을 써서 네이티브로 Google 로그인을 처리했다.
정리해보자면 다음과 같다.
1. 웹에서 구글로그인 버튼을 클릭하면 네이티브에 요청을 하게된다.
const requestGoogleLogin = () => {
window.ReactNativeWebView?.postMessage(JSON.stringify({ type: 'GOOGLE_LOGIN' }));
};
window.ReactNativeWebView는 React Native WebView가 웹에 주입해주는 브릿지 객체이다!
2. 네이티브에서 메세지를 수신하여 구글 로그인을 실행한다.
const handleMessage = async (e: WebViewMessageEvent) => {
const { type } = JSON.parse(e.nativeEvent.data);
if (type === 'GOOGLE_LOGIN') {
const userInfo = await GoogleSignin.signIn();
const { idToken } = await GoogleSignin.getTokens();
if (idToken) {
webviewRef.current?.postMessage(JSON.stringify({ type: 'AUTH_TOKEN', token: idToken }));
}
}
};
- onMessage핸들러에 위의 handleMessage함수를 넣는다. 즉, 웹에서 보낸 postMessage를 이 곳 App.tsx에서 받는 것.
- GOOGLE_LOGIN 메세지면 GoogleSignin.signIn()으로 네이티브 구글 로그인 UI를 띄우고,
- getToekns()로 idToken(구글이 서명한 JWT)을 가져온다.
- 그러고 나서 다시 postMessage로 idToken을 웹뷰에 보내주는 것을 볼 수 있다.
3. 웹에서 이제 토큰을 수신했으니 Firebase로 인증을 한다.
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
try {
const { type, token } = JSON.parse(e.data);
if (type === 'AUTH_TOKEN' && token) {
signInWithGoogleToken(token).then((user) => {
window.location.href = '/';
});
}
} catch {}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
- 위 코드처럼 useEffect를 사용하여 LoginPage가 렌더링되었을 때, 즉, 컴포넌트가 마운트되었을 때 message 이벤트를 등록한다.
- 그럼 네이티브가 postMessage로 보낸 AUTH_TOKEN 메세지가 있으면 여기서 수신을 하게 되는 것이다.
- 여기에서 만약 postMessage로 온 문자열을 파싱해서 타입이 AUTH_TOKEN이면서 token값이 있다면, signInWithGoogleToken(token)을 호출하게 된다.
- 성공하게 된다면, window.location.href='/', 즉, 홈으로 이동하게 된다.
export const signInWithGoogleToken = async (idToken: string) => {
const credential = GoogleAuthProvider.credential(idToken);
const userCredential = await signInWithCredential(fireAuth, credential);
return userCredential.user;
};
위 코드는 구글 idToken을 GoogleAuthProvider.credential로
- Firebase가 인식하는 Credential 객체로 변환하고,
- signInWIthCredential로 실제 로그인을 요청하는 부분이다.
- 이렇게 Firebase가 구글 서명을 검증하고 자체 세션을 발급하면 로그인이 완성된다.
결국 네이티브는 구글 로그인이라는 네이티브 기능을,
웹뷰는 UI와 Firebase 인증을 맡는 역할 분리가 핵심이었다.
이 트러블슈팅 과정에서 느낀 것
이 문제를 풀면서 크게 세 가지를 배웠다.
- Google은 웹뷰, Expo Go와 같은 제한된 환경에서의 OAuth를 보안상 이유로 차단한다는 것.
- Expo Go는 커스텀 네이티브 모듈을 포함할 수가 없어서 네이티브 OAuth 라이브러리와 호환되지 않으므로 Development Build를 사용해야 한다.
- 네이티브 모듈이 필요한 기능은 처음부터 Development Build 환경을 기준으로 개발하고,
플랫폼 제약이 있는 기능은 환경의 제약을 먼저 파악하기로 다짐했다.
다음 글에서는 인증이나 로그인 관련이 아닌 웹뷰 버전과 플랫폼별 UI에서 고생했던 트러블슈팅들을 모아서 다뤄보고자 한다.
'프로젝트' 카테고리의 다른 글
| [tolli #4] AOS 애플로그인 트러블슈팅 (0) | 2026.06.30 |
|---|---|
| [tolli #3] switch로 쪼갠 화면이 협업 단위가 됐다 (0) | 2026.06.22 |
| [tolli #2] 왜 React Native + WebView 하이브리드였을까? (0) | 2026.06.20 |
| [tolli #1] 무거운 성경암송 앱이 싫다 (0) | 2026.06.16 |