React를 WebView에 띄우고 백엔드는 Spring Boot API로 받는 하이브리드 앱을 만들다 보면, 소셜 로그인 단계에서 한 번은 반드시 부딪히는 문제가 있다. 구글 로그인 화면이 WebView 안에서 아예 열리지 않거나 403 disallowed_useragent 로 막히는 것이다.
처음 이 상황을 만나면 “앱 전체를 네이티브로 다시 짜야 하나?” 하는 생각이 들 수 있는데, 결론부터 말하면 그럴 필요 없다. 바꿔야 하는 건 로그인 화면을 띄우는 방식 한 군데뿐이다.
왜 막히는가
구글은 임베디드 브라우저(WebView) 안에서 OAuth 로그인을 수행하는 것을 명시적으로 차단하고 있다. 이유는 보안이다.
- WebView는 앱이 완전히 통제하는 브라우저다. 앱 개발자가 마음만 먹으면 사용자가 입력하는 아이디·비밀번호를 가로챌 수 있다.
- 사용자는 지금 보고 있는 로그인 화면이 진짜 구글 화면인지, 앱이 흉내 낸 피싱 화면인지 구분할 방법이 없다.
- 주소창이 없으니 URL 확인도 불가능하다.
그래서 구글은 WebView의 User-Agent를 감지해서 OAuth 요청을 거부한다. 이건 우회할 대상이 아니라 정책이다. User-Agent를 바꿔서 뚫는 방법이 인터넷에 돌아다니지만, 정책 위반이고 언제 막혀도 이상하지 않다.
다른 소셜 로그인은?
같은 문제를 겪으면서 Kakao, Naver, Apple도 같이 확인해봤다.
| 제공자 | WebView OAuth |
|---|---|
| 명시적으로 차단 (가장 엄격) | |
| Apple | 정책상 시스템 브라우저 권장. Sign in with Apple은 네이티브 SDK 경로가 따로 있음 |
| Kakao | 명시적 차단은 없으나 SDK/브라우저 방식 권장 |
| Naver | 명시적 차단은 없음 |
정리하면 지금 당장 막히는 건 구글이지만, 업계 전체 방향은 “OAuth는 앱이 통제하지 않는 브라우저에서 하라”는 쪽으로 일관되게 가고 있다. 구글 때문에 어차피 구조를 바꿔야 한다면, 나머지도 같은 방식으로 맞춰두는 게 나중에 두 번 일하지 않는 길이다.
대응: 로그인 화면만 시스템 브라우저로
핵심은 로그인 페이지를 여는 순간만 WebView 밖으로 빼는 것이다.
- Android: Chrome Custom Tabs
- iOS: SFSafariViewController (또는 ASWebAuthenticationSession)
이 둘은 시스템 브라우저를 앱 안에 오버레이 형태로 띄워준다. 사용자 입장에서는 앱을 벗어난 것처럼 보이지 않지만, 실제로는 앱이 통제할 수 없는 브라우저이므로 구글 정책을 만족한다. 게다가 브라우저의 기존 로그인 세션을 공유하기 때문에 이미 구글에 로그인돼 있으면 비밀번호를 다시 안 쳐도 된다. 사용자 경험은 오히려 좋아진다.
흐름
[WebView 안 React]
└─ "구글로 로그인" 버튼 클릭
└─ JS Bridge로 네이티브 호출
└─ Chrome Custom Tabs / SFSafariViewController 오픈
└─ 사용자 구글 인증
└─ redirect_uri 로 콜백 (커스텀 스킴 또는 App Link)
└─ 네이티브가 code 수신
└─ WebView / 백엔드로 전달
└─ Spring Boot가 code → token 교환
바뀌는 곳은 딱 하나, “버튼 클릭 → 로그인 페이지 열기” 부분이 window.location 이나 iframe 대신 브릿지 호출로 바뀌는 것이다. 나머지 화면, API, 세션 관리는 그대로다.
콜백 받기
redirect_uri를 앱이 받을 수 있는 주소로 등록해야 한다.
- 커스텀 URL 스킴:
myapp://oauth/callback - Android App Links / iOS Universal Links:
https://api.example.com/oauth/callback(더 안전, 설정은 더 번거로움)
구글 클라우드 콘솔에서는 Android/iOS 클라이언트 타입으로 등록하면 패키지명·번들ID 기준으로 검증되므로, 웹 클라이언트 타입으로 등록해놓고 WebView에서 쓰려던 방식과는 애초에 다르다는 점을 기억해야 한다.
백엔드 쪽
Spring Boot에서 code → token 교환을 하는 부분은 WebView 시절과 동일하다. 다만 PKCE를 쓰고 있다면 code_verifier를 만든 주체(네이티브 또는 WebView)와 교환 요청을 보내는 주체가 일치하도록 한 곳에서 관리해야 한다. BFF 패턴이라면 백엔드가 code_verifier를 세션에 들고 있는 게 가장 깔끔하다.
“전면 네이티브 전환”을 선택하지 않은 이유
이 문제를 처음 만났을 때 검토했던 선택지는 세 가지였다.
- 로그인 호출부만 시스템 브라우저로 교체 (선택)
- 로그인 화면 전체를 네이티브로 구현
- 앱 전체를 React Native 등으로 재작성
문제의 범위는 “구글이 WebView 안의 OAuth를 거부한다”는 것 하나다. 이 범위에 맞는 최소 변경은 1번이고, 2·3번은 문제 크기에 비해 변경 범위가 너무 크다. 정책 이슈의 범위가 좁을 때는 그 범위만큼만 손대는 게 맞다. 아키텍처를 갈아엎을 이유는 “구조가 근본적으로 잘못됐다”는 판단이 섰을 때 생기는 것이지, 정책 하나 때문에 생기는 게 아니다.
정리
- 구글은 WebView 안의 OAuth를 정책으로 차단한다. 우회하지 말 것.
- Kakao/Naver는 아직 명시적 차단이 없지만, 방향은 같다.
- 로그인 페이지를 여는 부분만 Chrome Custom Tabs / SFSafariViewController로 바꾸면 된다.
- 하이브리드 구조, Spring Boot 백엔드, 세션 관리는 그대로 둔다.