[iOS] 아이폰 에서만 팝업이 안뜬다고? window.open과 iOS의 사용자 제스처 정책
🖐️ Intro: 잊을 만하면 찾아오는 그 녀석
개발자라면 누구나 "어? 아까 로컬에선 됐는데?" 하는 순간을 마주하곤 합니다. 이번 세금계산서 발행 기능 구현이 바로 그런 순간이었습니다.
PC Chrome과 Android 기기에서 기능 테스트를 완벽하게 마친 뒤 배포를 진행했습니다. 그런데 배포 직후 QA팀에서 아이폰 앱에서 "버튼을 눌러도 반응이 없다"는 제보가 들어왔습니다. 로그를 확인해 보니 API는 정상적으로 호출되었고, 에러 로그조차 남아있지 않았습니다.
그 순간, 전 직장에서 겪었던 동일한 이슈가 뇌리를 스쳤습니다. "아, 맞다. iOS는 window.open이 달랐는데..."
당시에도 "다신 안 잊어버려야지" 하고 다짐했건만, 같은 실수를 반복하고 말았습니다. 미래의 저를 위해, 그리고 저와 같은 삽질을 하고 계실 분들을 위해 이 문제를 명확히 문서화해 두려 합니다.
🚨 문제 상황: 세금계산서 조회 팝업 실종 사건
상황은 간단합니다. 사용자가 버튼을 클릭하면 서버에 데이터를 전송하고 응답으로 받은 URL을 새 창(팝업)으로 띄워야 합니다.
[의도한 플로우]
-
'세금계산서 확인' 버튼 클릭
-
API 호출 (POST /api/tax-invoice) → 비동기(Async/Await)
-
응답 성공 (URL 수신)
-
window.open(url) 실행 → 팝업 오픈
하지만 iOS 환경에서는(사파리 / 웹뷰 / 기타 브라우저) 4번 단계에서 침묵합니다. 팝업 차단 알림조차 뜨지 않고 조용히 무시되는 경우가 많습니다.
❌ 예를 들어 아래의 코드가 있습니다.
const handleClickTaxInvoiceButton = async () => {
try {
const { url } = await requestTaxInvoice();
window.open(url, '_blank');
} catch (error) {
console.error(error);
}
};🧠 원인: Apple의 엄격한 "사용자 제스처" 정책
이 문제는 특정 브라우저 버그가 아니라 WebKit의 사용자 제스처 보안 정책에 의해 발생합니다.
브라우저는 무분별한 광고 팝업을 막기 위해 window.open이 "사용자의 명시적인 상호작용(Trusted Event: 클릭, 탭 등)"과 동기적(Synchronous)으로 연결된 경우에만 실행을 허용합니다. 비동기 로직을 거쳤을 때 차단되는 기준은 같은 WebKit이라도 브라우저별로 실제 구현이 다를 수 있습니다.
🔧 해결책:
1. 빈 창 우선 열기
-
브라우저가 안심할 수 있도록 창을 여는 행위를 사용자 클릭 직후(동기)에 수행하는 것입니다.
-
동기 실행: 클릭 이벤트 발생 즉시 빈 창을 엽니다.
-
비동기 실행: 데이터를 요청합니다.
-
URL 교체: 데이터를 받으면 아까 열어둔 창의 주소(location.href)를 바꿔치기합니다.
✅ 성공하는 코드
const handleClickSampleButton = async ({ onSuccess, onError }) => {
const popup = window.open('', '_blank');
if (!popup) {
alert('팝업 차단을 해제해주세요.');
return;
}
try {
const { url } = await requestAsync();
// 비동기 완료 후, 확보해둔 창의 주소만 교체
popup.location.href = url;
onSuccess?.();
} catch (error) {
popup.close();
onError?.(error);
}
};2. 웹뷰 앱에서는 네이티브 모듈 사용
- 웹뷰에서는 네이티브 모듈을 사용할 수 있습니다. 앱 환경에서는 앱 기능을 사용하시기를 권장드립니다.
// rn 앱 onMessage 핸들러
if (type === WEBVIEW_BRIDGE.OPEN_EXTERNAL_URL) {
Linking.openURL(url);
}// 웹
webViewBridge.send({
type: WEBVIEW_BRIDGE.OPEN_EXTERNAL_URL,
data: { url },
});📖 참고
-
아이폰 특정 버전(확인한 버전으로는 17.4)이상 기준 사파리 브라우저에서는 async 로직이 있어도 정상 동작 한다고 합니다. 단 유저 인터렉션 후속 동작으로 인정되는 시간 범위가 제한적이기 때문에, 환경에 따라 여전히 차단될 수 있습니다.
-
아이폰 최신 버전 기준이라도 웹뷰에서는 여전히 차단된다고 합니다. 이는 팝업 차단이 safari의 정책이 아닌 웹킷 엔진 차원의 정책이라서 그렇습니다.
🧭 끝으로
iOS 에서는 팝업 오픈 기준이 엄격하기에 유저 인터렉션으로 핸들링해야합니다. 만약 비동기 로직이 불가피하다면 window.open으로 빈 창을 먼저 열어서 리다이렉션 하는 방식을 권장드립니다.
웹 프론트엔드 개발자는 PC 브라우저 위주로 테스트하다 보니 놓치기 쉬운 부분입니다. 특히 결제나 본인인증, 세금계산서처럼 외부 연동이 필수적인 기능에서는 반드시 모바일 Safari 테스트를 선행해야 함을 다시 한번 되새깁니다. (다음에 또 까먹으면 이 글 보러 와야지...)
참고 자료 (References)
-
Apple Developer Forums: window.open not working in async function: 비동기 함수 내에서 팝업이 차단되는 현상에 대한 개발자 논의입니다.
-
MDN Web Docs window.open() - Popup blocking: "대부분의 브라우저는 사용자 상호작용 컨텍스트 밖에서 호출된 팝업을 차단한다"는 표준 스펙을 명시하고 있습니다. Safari는 이를 가장 엄격하게 준수합니다.