출처가 다른 윈도우 간에는 데이터를 어떻게 통신할까?


계기

최근 document.domain의 setter 지원 중단이 확정 되어 유관 부서에서 윈도우 간 통신 문의가 있었다. 일례로, 이 중 부모 윈도우에서 출처가 다른 팝업을 띄우고 나서 팝업에서 window.opener.location.reload()가 동작하지 않아 대안으로 window.opener.top.location.href를 사용하며 의견을 구한 적이 있었다.

이 이슈를 간략하게 정리하면 다음과 같다.

A라는 출처를 가진 윈도우A에서 B라는 출처를 가진 팝업B를 띄우고 팝업B에서 윈도우A를 조작한다.

브라우저에서는 CORS 등과 같은 보안 이슈에 대해 민감하게 대응하는데 장기적인 관점에서 이런 이슈들을 해소하기 위해 출처가 다른 윈도우 간의 통신 방법에 대한 글을 작성하게 되었다.

동일 출처(Same origin)와 교차 출처(Cross origin)

개념은 간단하다. 출처가 같으면 동일 출처(Same origin)고 다르다면 교차 출처(Cross origin)다.

출처가 같은지 판별하는 방법도 간단하다. 출처의 스펙에 의하면 출처 A와 출처 B가 동일한 프로토콜, 포트, 호스트가 모두 동일한 경우에 두 출처는 동일 출처가 된다.

다음 표는 http://store.company.com/dir/page.html URL에 대해 표의 URL들이 동일 출처인지 판별한 결과와 그 이유에 대한 표다.

URL 결과 이유
http://store.company.com/dir2/other.html o 경로만 다름
http://store.company.com/dir/inner/another.html o 경로만 다름
https://store.company.com/page.html x 프로토콜이 다름
http://store.company.com:81/dir/page.html x 포트가 다름(http는 80포트가 기본)
http://news.company.com/dir/page.html x 호스트가 다름

동일 출처와 비슷하지만 조건이 조금 다른 동일 출처-도메인(Same origin-domain)라는 개념도 있다. 동일 출처-도메인은 두 출처의 도메인을 검사하며 둘의 프로토콜이 같으면서 도메인이 null이 아니면서 같거나, 둘이 동일 출처면서 도메인이 같을 때(null이여도 무방) 두 출처는 동일 출처-도메인이 된다.

다음 표는 두 출처 A, B가 동일 출처인지 동일 출처-도메인인지, 혹은 둘 다 해당되는지 보여준다. 괄호 안의 인자의 순서는 (프로토콜, 호스트, 포트, 도메인) 순서다.

A B 동일 출처 동일 출처-도메인
("https", "example.org", null, null) ("https", "example.org", null, null) O O
("https", "example.org", 314, null) ("https", "example.org", 420, null) X X
("https", "example.org", 314, "example.org") ("https", "example.org", 420, "example.org") X O
("https", "example.org", null, null) ("https", "example.org", null, "example.org") O X
("https", "example.org", null, "example.org") ("http", "example.org", null, "example.org") X X

교차 출처일 때 제한되는 메서드와 속성

iframe.contentWindow, window.parent, window.open, window.opener 같은 자바스크립트 API는 두 문서가 서로를 직접적으로 참조할 수 있게 도와준다. 하지만 두 문서가 교차 출처인 경우 참조는 Window, Location 객체에 대한 매우 제한된 접근만을 제공한다.

제한된 접근을 제공하는 Window, Location 객체

다음 Window 속성에 대한 교차 출처 접근이 허용된다.

메서드 및 속성
window.blur
window.close
window.focus
window.postMessage
window.closed 읽기 전용
window.frames 읽기 전용
window.length 읽기 전용
window.location 읽기/쓰기 전용
window.opener 읽기 전용
window.parent 읽기 전용
window.self 읽기 전용
window.top 읽기 전용
window.window 읽기 전용

다음 Location 속성에 대한 교차 출처 접근이 허용된다.

메서드
location.replace 쓰기 전용

앞선 이슈의 원인

앞선 예제에서 window.opener.location.reload()는 동작하지 않지만 window.opener.top.location.href는 동작하는 예시를 보았다. 이 문제도 윈도우와 팝업이 교차 출처인 것이 원인이다.

href 스펙reload() 스펙을 비교하면 원인에 대해 더 자세히 알 수 있다.

  • href의 경우 값을 변경 시 별도로 출처를 검사하지 않는다.
  • reload()의 경우 동일 출처-도메인인지 검사해서 출처가 다르다면 "SecurityError"라는 DOMException 에러를 던지며 새로고침을 수행하지 않는다.

교차 출처에 대한 검사를 하느냐 안하느냐의 차이에 의한 문제인 것이다.

교차 출처 문서간의 데이터 통신

그렇다면 교차 출처 문서 간에는 서로 통신이 불가능한 것일까? postMessage()MessageChannel을 이용하면 교차 출처 문서 간의 데이터를 안전하게 주고 받을 수 있다.

document.domain을 변경하는 방법은 지원 중단 예정이니 이 글에서 소개하지 않는다. 해당 방식을 사용하고 있다면 postMessage()MessageChannel을 이용한 방식으로 변경하는 것을 권장한다.

postMessage()

postMessage()을 이용하면 교차 출처 window 객체 간에도 안전한 데이터 통신이 가능하다. 다음과 같은 인터페이스를 가지고 있다.

postMessage(message, targetOrigin [, transfer]);

targetOrigin으로 message라는 데이터를 전송한다고 생각하면 이해하기 쉽다. 메시지를 전송(postMessage)하면 받는 쪽에서 'message' 이벤트 핸들러를 등록해 놓고 메시지를 받아서 처리할 수 있다. 'message' 이벤트 핸들러의 event 파라미터는 보낸 데이터인 data, 보낸 쪽의 출처인 origin, 보낸 쪽의 window객체의 참조인 source로 구성되어 있다.

다음은 http://example.com:8080에서 http://example.com 출처를 가진 팝업을 띄우고 postMessage()를 사용해 데이터를 주고 받는 예제다.

// http://example.com 출처를 가진 팝업을 띄우는 스크립트
const popup = window.open(/* */) // 팝업을 띄운다.

popup.postMessage('Hello', 'http://example.com'); // 'http://example.com' 출처의 팝업으로 'Hello' 라는 데이터를 전송한다.

window.addEventListener('message', (event) => {
  // 팝업에서 온 메시지가 아니라면 아무 작업도 하지 않는다.
  if (event.origin !== 'http://example.com') {
    return;
  }
  
  // event.source 는 popup이 된다.
  // event.data는 popup에서 보낸 데이터인 'World!'가 된다.
}, false);
// http://example.com인 팝업의 스크립트
window.addEventListener('message', (event) => {
  // event.origin을 통해 출처를 안전하게 검사할 수 있다.
  if (event.origin !== 'http://example.com') {
    return;
  }
  
  // event.source 는 window.opener(팝업을 연 부모)가 된다.
  // event.data는 부모에서 보낸 데이터인 'Hello'가 된다.
  event.source.postMessage('World!', event.origin); // 메시지를 받으면 메시지를 보낸 쪽에 'World!' 데이터를 보낸다.
}, false);

event.origin을 통해 어떤 출처에서 보냈는지 검사하여 안전하게 데이터를 주고 받을 수 있으며 event.dataStructured clone 알고리즘을 이용한 직렬화를 통해 별도의 직렬화 과정 없이 메서드를 사용할 수 있다.

MessageChannel

MessageChannel은 메세지 채널을 만들어서 2개의 포트를 통해 데이터를 보내는 방식을 사용한다. postMessage와 조금 다르게 중간에 이 MessageChannel을 통해 데이터를 주고 받는다. 웹 워커에서도 사용할 수 있으며 지속적으로 통신 가능하다는 장점이 있으며 자체적으로 버퍼가 내장되어 있어 연결되지 않은 상태에서 데이터를 전송해도 연결된 이후에 순차적으로 전송된다는 특징이 있다.

다음은 MessageChannel을 이용해 부모 윈도우인 http://example.com:8080에서 자식 윈도우인 http://example.com iframe과 데이터를 주고받는 예제다.

// http://example.com:8080에서 iframe을 사용하는 부모의 스크립트
const channel = new MessageChannel();
const iframe = document.querySelector('iframe'); // 자식 iframe을 가져온다. querySelector가 아닌 다른 방법으로 가져와도 무방하다.

iframe.contentWindow.postMessage('Hello', 'http://example.com', [channel.port2]); // port2를 iframe과 연결하기 위해 전송한다.
channel.port1.onmessage = (e) => console.log(e.data); // 자식에게 메시지가 오면 출력한다. 'World!'가 출력된다.
// http://example.com인 iframe의 스크립트
window.addEventListener('message', (e) => {
  // 마찬가지로 e.origin을 확인해 출처를 확인한다.
  if (e.origin !== 'http://example.com') {
    return;
  }

  const [port2] = e.ports;
  // e.ports에 3번째 인자로 넘겨준 port2가 들어있다.
  port2.postMessage('World!'); // 전송된 port2를 사용해 부모에게 'World!' 데이터를 보낸다.
  // 이후 MessageChannel을 통해 오는 데이터를 처리하는 핸들러를 등록한다.
  port2.onmessage = (e) => console.log(e.data);
});

window.postMessage()를 사용해서 의아할 수 있지만 port2의 정보를 다른 윈도우에 넘겨주기 위해서 사용해야한다. 이후에는 port1, port2가 연결되어 window.postMessage() 없이 MessageChannelport.postMessage()를 이용해 두 윈도우 간 통신이 가능하다.

결론

크롬 브라우저 M109(23년 1/10 배포 예정)에서 document.domain 변경을 통한 교차 출처 문서 간에 데이터를 전송하는 방법이 지원 중단될 예정이다. document.domain 말고 postMessage()MessageChannel을 통해 교차 출처 문서 간에도 쉽고 안전하게 데이터를 주고 받을 수 있다.

References

임재언2022.08.31
Back to list