WebView에서 인증 팝업이 조용히 차단되는 이유 (2): 팝업 대신 리다이렉트로 우회하기
SDK popup vs redirect 패턴 / UA 기반 환경 분기
난이도: ★★★☆☆
연관 노트: WebView에서 인증 팝업이 조용히 차단되는 이유 (1): window.open() 차단 메커니즘
배경
(1) 편에서 원인을 파악했다 — Toss 인증 SDK의 팝업 방식이 특정 인앱 브라우저에서 조용히 차단된다. 이제 이걸 서버 수정 없이 프론트엔드만으로 우회하는 방법을 찾아야 했다.
핵심 요약
팝업 방식과 리다이렉트 방식은 같은 인증 결과를 다른 UX 경로로 전달한다.
| 팝업 방식 (Case 1) | 리다이렉트 방식 (Case 2) | |
|---|---|---|
| 흐름 | 현재 탭 유지 + 팝업 창 | 현재 탭이 인증 페이지로 이동 후 복귀 |
| 콜백 | JS 함수 (onSuccess) | URL 이동 (successUrl) |
| WebView 호환 | ❌ window.open() 차단 | ✅ 탭 이동이라 차단 없음 |
| Toss SDK 인자 | onSuccess, onFail | type: 'redirect', successUrl, failUrl |
WebView에서 팝업이 막히면 리다이렉트 방식으로 전환한다. 단, successUrl에 콜백 페이지가 필요한 파라미터를 직접 임베드해야 기존 처리 로직을 재사용할 수 있다.
왜 헷갈렸나
“리다이렉트 방식”이라는 말에서 서버가 HTTP 301/302 응답을 보내는 것으로 오해하기 쉽다. 실제로는 Toss Cert SDK가 클라이언트에서 현재 탭의 URL을 교체하는 방식이다. 서버는 관여하지 않는다.
또한 팝업 방식에서 인증 완료 후 JS 콜백(onSuccess)이 실행될 때 클로저로 request_id를 참조하던 구조를, 리다이렉트 방식에서는 URL 파라미터로 넘겨야 한다는 점을 놓치기 쉽다.
메커니즘
팝업 방식 (Case 1 — tossCert.start({ onSuccess, onFail }))
사용자 클릭
│
▼
tossCert.preparePopup() → window.open("") → 빈 팝업
│
API 호출 → request_id 획득 (클로저에 저장)
│
▼
tossCert.start({ authUrl, txId, onSuccess, onFail })
└─ 팝업에 authUrl 로드 → 인증 완료
│
▼
onSuccess() 콜백 실행
└─ request_id (클로저) → 결과 조회 → 다음 페이지
Toss 공식 예시:
var tossCert = TossCert();
tossCert.preparePopup();
getTxId().then(function (response) {
tossCert.start({
authUrl: response.authUrl,
txId: response.txId,
onSuccess: function () { /* 인증 완료 처리 */ },
onFail: function (error) { /* 실패 처리 */ },
});
});
리다이렉트 방식 (Case 2 — tossCert.start({ type: 'redirect', successUrl, failUrl }))
사용자 클릭
│
API 호출 → request_id 획득
│
▼
successUrl 생성: /auth-callback/?rid={request_id}&...
│
▼
tossCert.start({ type: 'redirect', authUrl, txId, successUrl, failUrl })
└─ 현재 탭이 authUrl로 이동 → 인증 완료
│
▼
탭이 successUrl로 이동
└─ URL에서 rid 읽기 → 결과 조회 → 다음 페이지
Toss 공식 예시:
function startTossCertWithRedirect() {
var tossCert = TossCert();
getTxId().then(function (response) {
tossCert.start({
type: 'redirect', // ← 이 한 줄이 팝업/리다이렉트 분기
authUrl: response.authUrl,
txId: response.txId,
successUrl: 'https://example.com/auth/callback', // 인증 완료 후 이동
failUrl: 'https://example.com/auth/fail', // 실패 시 이동
});
});
}
핵심 차이: request_id를 클로저로 전달하던 것을 URL 파라미터(successUrl)에 임베드해 전달한다.
해결 패턴
환경 감지 + 분기
var tossCert = TossCert();
// Toss 인앱 WebView 감지
const isTossInApp = /TossApp/i.test(navigator.userAgent);
const { data } = await api.post("/auth/request", {
success_callback_url: `${origin}/auth/callback/`,
// ...
});
if (isTossInApp) {
// Case 2: 리다이렉트 — request_id를 successUrl에 임베드
const successUrl = new URL(`${origin}/auth/callback/`);
successUrl.searchParams.set("rid", data.request_id);
// 탭 이동 후에도 필요한 값은 모두 URL에 포함 (세션 스토리지는 불확실)
successUrl.searchParams.set("client_id", client_id);
tossCert.start({
type: "redirect",
authUrl: data.auth_url,
txId: data.tx_id,
successUrl: successUrl.toString(),
failUrl: `${origin}/auth/failure/`,
});
} else {
// Case 1: 팝업 — preparePopup() 먼저, request_id는 클로저로 전달
tossCert.preparePopup();
tossCert.start({
authUrl: data.auth_url,
txId: data.tx_id,
onSuccess: async () => {
const url = await getAuthResultUrl(data.request_id); // 클로저
router.push(url);
},
onFail: (error) => {
router.push("/auth/failure/");
},
});
}
auth-callback 페이지 (기존 재사용)
리다이렉트 방식으로 도달해도 URL에 rid가 있으면 동일하게 처리된다.
// /auth/callback/ 페이지
const { rid } = router.query;
if (!rid) {
router.push("/auth/failure/");
return;
}
const url = await getAuthResultUrl(rid as string); // URL 파라미터에서 읽음
router.replace(url);
UA 분기를 전체 적용하지 않는 이유
Toss 공식 문서에서는 리다이렉트 방식을 Case 2로 소개하며 범용으로 쓸 수 있다고 명시되어 있다. 그럼에도 UA 분기를 선택한 이유:
리다이렉트 방식은 현재 탭이 이동했다가 돌아오기 때문에, 일반 브라우저에서는 팝업 방식보다 UX가 덜 자연스럽다. 그리고 팝업 방식이 이미 동작 중인 환경에서 교체하면 검증 안 된 코드가 모든 유저에게 영향을 준다.
영향 범위를 “Toss 인앱 WebView 문제”로 격리하기 위해 UA 분기를 선택했다. 리다이렉트 방식 동작이 충분히 검증되면 팝업 방식을 제거하고 전체를 단일화하는 게 더 단순하다.
다음에 이 상황을 만나면
트리거 조건: 특정 WebView에서만 팝업 인증이 안 되고, SDK가 팝업/리다이렉트 두 방식 모두 지원할 때
- SDK 문서에서
type: 'redirect'또는 리다이렉트 방식 지원 확인 (Toss: Case 2) -
successUrl에 필요한 파라미터를 모두 임베드 (세션 스토리지는 탭 이동 후 접근 보장 불확실) - 기존 콜백 페이지가 URL 파라미터로도 동작하는지 확인
- UA 분기로 영향 범위 격리 → 검증 후 전체 통일 고려
커넥팅 닷
← 선행 개념 (이걸 알아야 이해된다)
- WebView에서 인증 팝업이 조용히 차단되는 이유 (1): window.open() 차단 메커니즘 — 팝업이 왜 안 되는지, window.open() silent blocking 메커니즘
- sessionStorage 대신 URL에 상태를 담는 이유 — 리다이렉트 방식에서 request_id를 URL에 임베드하는 이유의 근거
→ 확장 개념 (여기서 더 나아가면)
- OAuth2 redirect_uri 패턴 — 동일한 구조. 인증 완료 후 state 파라미터를 redirect_uri에 포함해 CSRF 방지 및 컨텍스트 복원
- Feature Detection — UA 감지 대신
window.open()결과를 직접 체크해 환경을 분기하는 방식. UA 문자열보다 견고하지만 비동기 타이밍 문제를 별도 처리해야 함
↔ 같은 원리가 적용되는 곳
- 결제 SDK (PG사) — 팝업/리다이렉트 이중 지원하는 구조가 동일. WebView 환경에서 같은 분기 문제 발생
- 광고 픽셀은 UTM이 사라져도 어떻게 전환을 잡는가 — 브라우저 이동 전후로 컨텍스트를 URL 파라미터로 넘기는 같은 패턴
참고
- Toss 인증 공식 문서 — Case 2. 리다이렉트 방식 —
type: 'redirect',successUrl,failUrl명세 - MDN: URL.searchParams — URL 파라미터 안전하게 다루기