져니의 개발 정원 가꾸기

(JavaScript) window.open()로 도메인이 다른 창을 열었을 때 발생하는 CORS 문제 - postMessage를 사용하자. 본문

개발노트/JavaScript | Mustache

(JavaScript) window.open()로 도메인이 다른 창을 열었을 때 발생하는 CORS 문제 - postMessage를 사용하자.

전전쪄니 2024. 3. 17. 23:56

목차

    배경

    현재 개발/운영하고 있는 프로젝트에서 특정 버튼을 클릭하면 다른 도메인 서비스의 팝업을 여는 부분이 있었다. 팝업 사용 후 바로 직전의 부모창으로 다시 데이터를 넘겨줄 때 window.opener라는 객체에 접근하여 부모창의 주소 데이터를 참고하도록 되어 있었는데, 이 때 팝업창에서 부모창의 데이터를 가져오지 못해 opener의 데이터들이 undefined혹은 빈 string 으로 되어 값을 넘겨주지 못하는 상황이 발생했다. 

    원인은 부모창과 팝업창이 다른 도메인 주소를 가지고 있어 window.open()을 하였을 때 CORS문제로 값을 넘겨주지 못했던 것이었다. 해당 CORS 문제는 경고나 익셉션이 뜨지 않고 실제 데이터를 사용할 때 팝업창에서 undefined exception이 났을뿐더러 워낙 사용자가 인터랙션이 적은 팝업이어서 꽤 오랫동안 발견되지 못했던듯 하다. 

    이번 글은 해당 이슈를 해결하면서 다른 도메인끼리 window.open()을 어떻게 사용하는지를 정리할 것이다.

    window.open()

    Window 인터페이스

    • DOM document를 가지고 있는 창이다.

    Window.open() 함수

    • 새로운 혹은 현재 열려있는 브라우저(텝, 창, iframe 등)에 특정 리소스를 로드하는 함수이다. 
    open()
    open(url)
    open(url, target)
    open(url, target, windowFeatures)

     

    파라미터

    • url
      • string type
      • 리소스를 로드할 url
      • 기본값 : blank page
    • target : 로드되는 창의 이름이나 창의 속성
      • string type
      • 특정 키워드로 이름과 속성을 지정할 수 있음
      • ex. "_self" -> 현재 페이지, "_blank" -> 새창, "_parent" -> 부모 프레임, "_top", "_unfecedTop" 등
      • 기본값 : "_blank"
    • windowFeatures
      • string type
      • 리소스가 로드되는 창의 사이즈, 위치, 팝업 여부 등의 속성을 지정. 
      • 여러 속성들을 나열할 때는 컴마로 구분
      • ex. "popup=yes, popup=1, popup=true"

    사용 예시

    // 새 텝으로 열기
    window.open("https://www.test.com/", "testTab");
    
    // 팝업을 열기
    window.open("https://www.test.com", "testWindow", "popup");
    
    // 특정 크기를 가지는 팝업
    const windowFeatures = "left=100,top=100,width=320,height=320";
    const ret = window.open(
      "https://www.test.com", 
      "testWindow",
      windowFeatures,
    );

    다른 도메인 열기 - document.domain vs postMessage

    JavaScript는 보안상 동일 출처 정책(Single Origin Policy)을 두어 다른 도메인의 서버로 요청하는 것을 기본적으로 차단하고 있다.

    여러 서비스들의 기능을 사용해야할 때 부모창과 다른 도메인의 창을 열어야하는 순간이 있기 마련인데, 이 때 크게 두 가지 방법을 사용할 수 있다.

    document.domain 

    과거에 많이 사용하던 방식으로, 서브도메인에 대한 교차 출처의 데이터 통신을 가능하게 된다. 가령 https://11st.co.kr/shopping 에서 https://naver.com/shopping 으로 접근할 때 도메인을 교체해준다.

    ...
        document.domain = 'naver.com'
    ...​

     

    그러나!! 2023.01 일자로 document.domain 의 setter 지원이 중단되면서 depreacted되었다. 그래서 담당하는 서비스의 팝업에서도 문제가 생겼던 것 (지원이 중단된 까닭은 부모창 <-> 자식창 간에 DOM 트리를 탐색할 수 있는 보안상의 이슈 때문이다로 한다.)

     

    이에 postMessage()메서드를 사용하는 것을 권장하고 있다.

    postMessage()

    이에 postMessage()는 교차 출처 window 간에도 안전한 데이터 통신을 가능하게 한다.

    • postMessage(message, targetOrigin [, transfer]);
      • targetOrigin으로 message 라는 이름으로 데이터를 전송한다.
      • 메시지를 수신하는 다른 창에서는 message 이벤트 핸들러를 등록해 메서지를 수신하고 처리한다.
    // 송신 창 - 'http://11st.co.kr'
    const popup = window.open('child'); // 팝업을 연다
    
    popup.postMessage('Hello It's me...', 'http://naver.com'); // 메시지 전송!
    
    window.addEventListener('message', (event) => {
      // 팝업에서 보내온 메시지가 아니라면 아무 작업도 하지 않는다.
      if (event.origin !== 'http://naver.com') {
        return;
      }
      alert(event.data)  // event.data는 popup에서 보낸 데이터인 'Hello~ from the other side ~~~'
    }, false);
    // 수신 창 - http://naver.com
    window.addEventListener('message', (event) => {
      if (event.origin !== 'http://11st.co.kr') { // 부모창이 http://11st.co.kr 아니면 처리 X
        return;
      }
      
      // event.source는 = window.opener(팝업을 연 부모).
      // event.data = 부모에서 보낸 데이터인 'Hello It's me...'
      event.source.postMessage('Hello~ from the other side ~~~', event.origin); // 메시지를 받으면 메시지를 보낸 쪽에 데이터를 보낸다.
    }, false);

     

    결론적으로, 이제부터는 document.domain 쓰지말고 postMessage() 를 사용해서 CORS 문제를 해결하면된다. 사실 postMessage()만으로만 CORS를 해결할 수 있는 것은 아니다. MessageChannel 객체를 사용하거나, (json 데이터의 경우) jsonp 형식으로 요청을 전송할 수도 있다.

    (주의) react dev tools 플러그인 사용할 때!

    chrome 브라우저에 react dev tools플러그인을 사용할 경우 postMessage가 생각한대로 동작하지 않을 수 있다. 이는 react dev tools 내부적으로 component를 제어하기 위해 postMessage()를 사용하기 떄문이다. react 프로젝트에 올라온 Issue를 확인해보자!

     

    링크 ⬇️
    https://github.com/facebook/react/issues/27529

     

    참고

    - https://offbyone.tistory.com/312

    - https://eunplay.tistory.com/205 

    - https://ui.toast.com/posts/ko_20220831