ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 서로 다른 Window 통신하기 PostMessage
    개발 2022. 11. 29. 02:11

     

     

     

     

    Window.postMessage()

     

    postMessage() 메서드는 Window 객체 간의 안전한 통신방법을 제공한다.

    대표적인 사용 예로 새창으로 띄워진 팝업창, 페이지 안에 포함된 iframe 을 말한다.

    또 데이터를 보내는 HTTP 요청을 생성하지 않으며, DOM 기반 통신에 사용된다.

     

     


     

    ⌨️  기본 사용법

     

     

    메시지 보내기

    targetWindow.postMessage(message, targetOrigin, [transfer]);

     

     

    메시지 받기

    window.addEventListener("message", receiveMessage, false);
    
    function receiveMessage(message)
    {
    	console.log(message)
    }

     

     


     

    📮 데이터 전달하기

    보내고 받는법을 발견했다.

    postMessage() 를 사용해 데이터를 전달해보자.

    아래 간단한 카운터 예제로 코드와 함께 살펴본다면 누구나 이해 할 수 있는 방법이다.

     

    카운터는 서로 다른 window 상황을 만들기 위해 새창을 띄워 숫자를 표시하고

    부모창에서는 그 숫자를 증감 할 수 있는 컨트롤러 역할을 하는 예제이다.

    (김치 맛있겠다)

     

     

     

     

    🍿 준비물

    카운팅될 숫자를 갱신하는 컨트롤러 + 갱신된 숫자를 표시하는 디스플레이어

     

     

    [컨트롤러 코드]

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Open Window - Controller</title>
    </head>
    <body>
        <button onclick="openWindow()">Open</button>
        <button onclick="add()">Add</button>
        <button onclick="subtract()">Subtract</button>
    
        <script>
            let target
            function openWindow() {
                target = open('./display.html', 'Display', 'popup')
            }
    
            function add() {
                target.postMessage('add')
            }
    
            function subtract() {
                target.postMessage('subtract')
            }
        </script>
    </body>
    </html>

     

     

    [디스플레이어 코드]

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Open Window - Display</title>
    </head>
    <body>
        <h1>0</h1>
    
        <script>
            const viewEl = document.querySelector('h1')
            let count = 0
            function updateCount(count) {
                viewEl.textContent = count
            }
    
            window.addEventListener('message', (message) => {
                if (message.data === 'add') updateCount(++count)
                else updateCount(--count)
            })
        </script>
    </body>
    </html>

     

    컨트롤러와 디스플레이어는 서로 다른 window가 열려있다.

    컨트롤러에서 openWindow 함수를 통해 디스플레이어가 자식창으로 팝업된다.

    open 함수가 반환한 window의 참조정보를 통해 targetWindow를 저장한다.

    let target
    function openWindow() {
        target = open('./display.html', 'Display', 'popup')
    }

     

    이 targetWindow를 사용해 postMessage를 보낸다.

    각각의 메시지 데이터는 'add' 와 'subtract' 문자열을 보내고있다.

    function add() {
        target.postMessage('add')
    }
    
    function subtract() {
        target.postMessage('subtract')
    }

     

    그럼 팝업창으로 열린 디스플레이어에서는 window 객체에 'message' 이벤트 리스너를 등록해 해당 postMessage를

    호출받을 수 있도록 작성한다.

    window.addEventListener('message', (message) => {
        console.log(message)
        if (message.data === 'add') updateCount(++count)
        else updateCount(--count)
    })

     

    add() 와 subtract() 함수가 호출되면 디스플레이어에 Message 이벤트에서 받은 내용은 어떻게 나올까

     

    MessageEvent 라는 이름의 객체가 출력되는데

    그 내용을 보면 data 라는 키값에 우리가 전달한 'add'와 'subtract'가 확인된다.

     

    카운터 예제의 경우 실제 카운팅된 값을 전달하지 않고 현재 덧셈 호출인지 뺄셈 호출인지를 문자열로 구분하기 때문에

    자식창에서 그 내용을 분기하여 자식창에서 선언된 count 값을 증감연산하여 화면에 렌더링한다.

     

    사실 실무에서는 부모에서 자식창으로 데이터를 전달하는것보다 반대의 경우가 더 많을것 같다.

    한번 코드를 작성해보자.

     

    🍯 자식창에서 부모창으로 데이터 전달하기

    단순하게 문자열만 넘긴 메시지는 실제 서비스에서 다양하게 활용되기 어려워 보인다.

    현재 어떠한 동작을 위한 메시지가 전달되었고 그 데이터는 무엇인지 명확하게 통신해야 한다면

    객체를 통해서 그 구분값을 넣어줄수 있을것이다.

     

     

    우선 요구사항

    • 자식창에서 부모창으로 메시지를 전달한다.
    • 메시지의 내용을 구분하는 값이 있어야한다.
    • 다양한 자료형을 사용해 많은 정보를 담아 전달해야 한다.

     

    해결 방법

    • window.open 함수를 통해 띄워진 팝업창(자식창)은 window.opener 객체를 참조하여 접근 할 수 있다.
    • postMessage() 메서드에 전달될 메시지 내용으로 객체를 활용한다.
      객체의 구성은 { type: '', payload: [] } 형식으로하여 메시지의 형태와 전달값을 구분한다.
    • payload에 배열을 사용해여 리스트 정보를 전달한다.

     

    위 카운터 예제에 약간의 코드만 변경하여 코드를 실행 해보자.

     

     

     

     

    [디스플레이어 코드]

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Open Window - Display</title>
    </head>
    <body>
        <h1>자식창 -> 부모창</h1>
    
        <button onclick="applyResult()">등록</button>
    
        <script>
    
            function applyResult() {
                const caller = window.opener
                console.log('[Child Window]', caller)
                caller.postMessage({type: 'FOUND_ITEM', payload: [{name: '김밥'}, {name: '떡볶이'}, {name: '오뎅'}]})
            }
    
        </script>
    </body>
    </html>

     

     

     

     

    [컨트롤러 코드]

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Open Window - Controller</title>
    </head>
    <body>
        <button onclick="openWindow()">Open</button>
    
        <div id="result"></div>
    
        <script>
            const viewEl = document.getElementById('result')
    
            window.addEventListener('message', (message) => {
                const {type, payload} = message.data
                if (type !== 'FOUND_ITEM') return
                console.log('[Parent Window]', message)
                print(payload)
            })
    
            function openWindow() {
                open('./display.html', 'Display', 'popup')
            }
    
            function print(payload) {
                viewEl.textContent = JSON.stringify(payload, null , 2)
            }
        </script>
    </body>
    </html>

     

    시나리오는 이렇다, 자식창에서 검색을 한다고 가정하고 음식을 검색하여 찾은 결과를 등록 버튼을 눌렀을때

    부모창으로 전달하기를 기대한다.

     

    자식창에서는 부모창에 접근하기 위해서 window.opener 를 caller 변수에 저장하고

    const caller = window.opener

     

    caller를 사용해 postMessage를 날린다. 이때 type, payload 키값을 통해 전달받은 메시지가 무엇이며 어떤내용인지 구분하도록 데이터를 전달하였다.

     

      caller.postMessage({type: 'FOUND_ITEM', payload: [{name: '김밥'}, {name: '떡볶이'}, {name: '오뎅'}]})

     

     

     

     

     

     

     

    [디스플레이어 코드]

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Open Window - Controller</title>
    </head>
    <body>
        <button onclick="openWindow()">Open</button>
    
        <div id="result"></div>
    
        <script>
            const viewEl = document.getElementById('result')
    
            window.addEventListener('message', (message) => {
                const {type, payload} = message.data
                if (type !== 'FOUND_ITEM') return
                console.log('[Parent Window]', message)
                print(payload)
            })
    
            function openWindow() {
                open('./display.html', 'Display', 'popup')
            }
    
            function print(payload) {
                viewEl.textContent = JSON.stringify(payload, null , 2)
            }
        </script>
    </body>
    </html>

     

    전달받은 데이터를 부모창에서는 message 이벤트를 통해 확인 할 수 있고 type를 확인하여 원하는 정보가 넘어왔을 경우만

    화면에 렌더링 할 수 있도록 분기한다.

     

    const {type, payload} = message.data
    if (type !== 'FOUND_ITEM') return
    print(payload)

     

    자식창에서 부모창으로 전달된 데이터

     

     

    부모창에서 결과를 확인해보니 의도한대로 데이터가 잘 확인된다.

    간단히 알아본 postMessage() 메서드의 사용법이다.

     

    postMessage() 메서드가 서로 다른 window 혹은 iframe 간의 통신을 손쉽게 도와주지만

    실제 서비스에서는 보안에 취약 할 수 있기때문에 몇가지 유의하여 코드를 작성해야 한다.

     

     

     


     

    🫵🏻  보안에 신경쓰기

    참고한 타 포스팅 내용에 의하면 이렇게 언급하고 있다.

     

     

    window.postMessage()는 SOP 제한을 안전하게 우회하는 제어된 메커니즘을 제공합니다
    (제대로 사용되는 경우라면)

     

     

    제대로 사용되지 않는 경우가 어떤 경우 일까?

    • 인증 없이 웹사이트에 표시되는 위험한 HTML 코드를 보내는 XSS 공격에 노출될 수 있다.
    • 로그인한 사용자 모르기 쿠키를 탈취 할 가능성이 있다.
    • 그럴일은 없겠지만 eval() 함수를 실행되는 경우 심각한 스크립트 공격이 될 수 있다. 

     

    위와 같은 공격을 방어하는 방법은 무엇이 있을까?

    • 전달받은 메시지의 Origin 체크하기
      postMessage 로 전달받은 메시지에는 보내진 출처에 대한 정보가 담겨있다. 이것을 사용해 메시지의 정보를 허용할수 있는 도메인의 경우에만 코드가 실행되도록 예외처리 하는것이 중요하다.
    window.addEventListener('message', (message) => {
    	if (message.origin === 'http://www.my-domain.com') {
    		// 실행 로직
    	}
    })

     

    • postMessage() 메서드 두번째 인자로 사용되는 targetOrigin을 명시하기

    MDN에서 명시되어있는 내용을 살펴보자. targetOrigin을 명시하지 않을 경우 "*"  (별도지정 없음) 옵션으로 설정되어 모든 경우의 메시지를 허용하게 된다. 때문에 안전한 메시지 통신을 위해서 targetOrigin을 명시하는것이 좋겠다.

     

    • 적절한 X-Frame-Options 및 Content-Security-Policy 헤더를 설정하면 페이지가 iframe에 로드되지 않는다.
    • 필요에 따라 postMessage()를 통해 전송된 데이터를 적절하게 검증해라.

    메모

     

    이렇게 postMessage() 메서드에 알아본 내용이다.

    사실 예전에 뭣모르고 사용해본적이 있었다. 역시 필요에 의해서 공부하지만 문서화해서 기록해두지 않으면 말짱 도루묵스

     

    요즘은 이런 통신방법이 얼마나 많이 쓰이겟냐만

    어디에나 존재하는 레거시 무덤에서 단번에 모던한 프로젝트로 탈바꿈하기는 쉽지 않기에

    항상 레거시에 대응해야 하는 방법은 알아두는것이 좋으니 이런저런 기술에 대한 옛날의 기술과 요즘의 기술을 나눌 필요가 없어보인다.

     

    필요할때 쓰면 그게 나에게 맞는 기술 아닁갸

     

     

     


    참고자료

    https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage

    https://www.youtube.com/results?search_query=js+postmessage 

    https://medium.com/@chiragrai3666/exploiting-postmessage-e2b01349c205

    댓글

onul-hoi front-end developer limchansoo