window.postMessage()는 서로 다른 origin을 가진 window 객체 사이에서 안전하게 메시지를 주고받을 수 있는 Web API입니다.
브라우저의 동일 출처 정책(Same-Origin Policy)은 iframe, 팝업, 다른 탭의 JavaScript가 서로 직접 접근하는 것을 막습니다. postMessage는 이 정책을 우회하지 않고, 명시적으로 허용된 출처 간에만 안전하게 통신하는 채널을 제공합니다.
event.origin이 "null"인 경우: file:// 프로토콜, sandbox된 iframe, 일부 리다이렉트 상황에서 발생합니다. 이 경우 검증이 어려우므로 신뢰하지 않는 것이 안전합니다.
3. 보안: origin 검증
postMessage를 쓸 때 가장 중요한 원칙은 수신 측에서 반드시 origin을 검증하는 것입니다.
잘못된 코드 (취약)
// 위험: origin 검증 없이 모든 메시지를 처리 window.addEventListener('message', (event) => { document.getElementById('output').innerHTML = event.data; // XSS 위험! eval(event.data); // 절대 안 됨 });
window.addEventListener('message', (event) => { // 1. origin 화이트리스트 검사 if (!ALLOWED_ORIGINS.has(event.origin)) { console.warn(`허용되지 않은 origin: ${event.origin}`); return; }
// 2. 데이터 구조 검증 (타입/스키마 확인) if (typeof event.data !== 'object' || !event.data.type) return;
const { type, payload } = event.data;
// 3. 메시지 타입별 처리 (허용된 타입만) switch (type) { case'UPDATE_THEME': applyTheme(payload.theme); // 안전한 처리 break; default: console.warn(`알 수 없는 메시지 타입: ${type}`); } });
보내는 쪽에서도 targetOrigin 지정
// 나쁜 예: '*'는 어느 사이트에서도 메시지를 가로챌 수 있음 iframe.contentWindow.postMessage(sensitiveData, '*');
// 좋은 예: 특정 origin만 수신 가능 iframe.contentWindow.postMessage(sensitiveData, 'https://trusted-child.com');
4. 활용 예제
예제 1: iframe ↔ 부모 페이지 통신
마이크로 프론트엔드나 써드파티 위젯을 iframe으로 임베드하고 통신하는 패턴입니다.
<!-- 부모 페이지 (https://parent.com) --> <iframeid="widget"src="https://widget.com/embed"></iframe>
<script> const iframe = document.getElementById('widget'); // iframe이 로드된 후 메시지 전송 iframe.addEventListener('load', () => { iframe.contentWindow.postMessage( { type: 'INIT', userId: 'user-123', theme: 'dark' }, 'https://widget.com' ); }); // iframe으로부터 이벤트 수신 window.addEventListener('message', (event) => { if (event.origin !== 'https://widget.com') return; const { type, payload } = event.data; if (type === 'PURCHASE_COMPLETE') { // 위젯에서 결제가 완료되었을 때 부모가 처리 updateOrderSummary(payload.orderId); trackAnalyticsEvent('purchase', payload); } }); </script>
// iframe 내부 (https://widget.com/embed) window.addEventListener('message', (event) => { if (event.origin !== 'https://parent.com') return;
const { type, payload } = event.data;
if (type === 'INIT') { initializeWidget(payload.userId, payload.theme); } });
returnnewPromise((resolve, reject) => { consthandler = (event) => { if (event.origin !== 'https://auth.myapp.com') return; if (event.source !== popup) return; // 이 팝업에서 온 메시지만 처리
// Worker에 작업 요청 asyncfunctionprocessImage(file) { // ArrayBuffer로 변환 후 Worker에 전달 const buffer = await file.arrayBuffer();
// transfer: 복사 없이 소유권을 Worker로 이전 (성능 최적화) worker.postMessage( { type: 'PROCESS_IMAGE', buffer, filter: 'grayscale' }, [buffer] // transfer 목록: 이후 메인 스레드에서 buffer 접근 불가 ); }
// 복사 전송: 메모리 사용량 2배, 시간 오래 걸림 worker.postMessage({ buffer: largeBuffer });
// 이전 전송: 메모리 복사 없음, 거의 즉시 완료 // 단, 이전 후 largeBuffer는 byteLength === 0 이 됨 (무효화) worker.postMessage({ buffer: largeBuffer }, [largeBuffer]); console.log(largeBuffer.byteLength); // 0
6. 주의사항 및 트러블슈팅
origin이 null인 경우
event.origin === 'null'(문자열) 인 경우가 있습니다. null(값)과 다르므로 주의하세요.
window.addEventListener('message', (event) => { // null origin은 file://, sandbox iframe, 일부 리다이렉트에서 발생 if (event.origin === 'null') { // 신뢰할 수 없으므로 무시 return; } // ... });
// 잘못된 예: iframe 로드 전에 메시지를 보내면 수신되지 않음 iframe.contentWindow.postMessage({ type: 'INIT' }, '*');
// 올바른 예: load 이벤트 이후 또는 준비 완료 메시지를 기다림 iframe.addEventListener('load', () => { iframe.contentWindow.postMessage({ type: 'INIT' }, 'https://child.com'); });
// 또는 iframe 측에서 준비됐을 때 먼저 알림을 보내는 방식 window.addEventListener('message', (event) => { if (event.origin !== 'https://child.com') return;
if (event.data.type === 'IFRAME_READY') { // iframe이 준비됐다고 알려줬을 때 초기화 메시지 전송 event.source.postMessage({ type: 'INIT', config }, event.origin); } });
// 올바른 예 1: 플래그로 중복 방지 let listenerRegistered = false; functionsetupListener() { if (listenerRegistered) return; listenerRegistered = true; window.addEventListener('message', handler); }
// 올바른 예 2: 기존 리스너를 먼저 제거 후 등록 functionsetupListener() { window.removeEventListener('message', handler); window.addEventListener('message', handler); }