Tempo Di Valse

[JS] window.open 에서 postMessage 와 Promise 를 이용하여 콜백 받기 본문

개발/Web

[JS] window.open 에서 postMessage 와 Promise 를 이용하여 콜백 받기

TempoDiValse 2022. 10. 4. 16:29

예전부터 열려진 팝업에서 콜백을 받기위해서 내가 알고 있는 것 2가지를 사용하곤 했다.

 

1. window.opener 를 통하여 팝업을 연 주체에 접근하여 직접 메소드 실행

2. postMessage 를 이용하여 메세지 전달

 

그런데 이런 경우에는 어떻게 깔끔하게 처리 해야 하는 가에 대해서 고민을 하게 되었다.

 

1. 메소드를 실행하여 팝업을 연다

2. 아이템을 선택한다.

3. 팝업이 닫힌다.

4. 값을 받아서 리턴을 시킨다.

 

C# 의 경우에는 OpenFileDialog 같은 것을 예를 들자면, dialog 를 열고 파일을 선택하게 되면 files 변수에 내가 선택한 값들이 들어가서 해당 변수를 콜백 삼아 사용하면 되는데... JS 에서는 2번 postMessage 를 사용하는 방식에 대해 먼저 예를 들어보자면, 일단 데이터를 받아서 처리 할 'onmessage' 를 addEventListener 를 통해 등록을 해야하며, 리스너 내부로 로직이 분리되어버렸기 때문에 window.open 을 실행한 메소드와 콜백 이후의 로직이 분리되게 되는 것이 있다. 그리고 1번 방법인 팝업을 연 주체에 접근하여 직접 메소드를 실행하는 것도 마찬가지로 따로 메소드를 만들어 실행 시켜주어야 하기 때문에  로직이 분리되는 것은 어쩔 수 없다.

 

하지만 나는 저 1~4번의 루틴을 한 메소드에서 처리 하고 싶은 것이었다. 그래서 생각한 것이 postMessage 방식을 선택하되 onmessage 이벤트를 일회성으로 만들어보자는 것이었다.

 

Android 에서는 ViewTreeObserver 라는 객체를 통해서 View 가 렌더된 후의 크기를 구하기 위해서 addOnGlobalLayoutListener 를 사용하는 경우가 있는데, 해당 메소드는 변화되는 View 의 크기를 계속 받아오기 떄문에 최초 1회만 가져오고자 할 떄 addOnGlobalLayoutListener 안에 removeOnGlobalLayoutListener 를 사용하여 더 불러오지 않도록 만들기도 하는데, 그 방식을 생각하여 다음과 같이 로직을 정리하게 되었다.

 

1. 팝업을 연다.

2. addEventListener 를 통해서 onmessage 이벤트를 등록한다.

3. 팝업에서 postMessage 를 호출하여 데이터를 전달한다.

4. 팝업을 닫는다.

5. removeEventListener 로 onmessage 이벤트를 삭제한다.

 

대략적인 로직의 순서는 이렇게 되는데, 여기서 하나 더 짚어야 될 것은, 리스너 조차도 메소드이기 떄문에 로직이 분리되는 것과 다르지 않는가 이다. window.addEventListener 는 단순히 리스너를 등록하는 용도이지 변수를 통해 콜백을 받을 수 있는 용도가 아니기 떄문이다.

 

그래서 사용하는 것이 Promise 이다. Promise 를 통해서 리스너를 감싸고 받은 데이터를 resolve 를 통해서 올려주면 메소드 자체가 Promise 형으로 묶일 수 있으며, async/await 을 통해서 Promise 형으로 체이닝을 하는 방식이 아니라 값을 끄집어내어 처리할 수 있기 떄문에 현재 방식에서는 대체할 것이 없는 좋은 기능이라고 할 수 있다.

 

그래서 로직을 한번 코드로 작성해보도록 하자.

    function openPopup(){
        // 1. 팝업을 열었다.
        window.open(POPUP_URL, POPUP_NAME, "window=640,height=480")
        
        return new Promise(resolve => {
            const listener = (e) => {
                // 3. 팝업으로부터 리턴받은 데이터를 전달한다.
                resolve(e.data)
                
                // 4. omnessage 이벤트를 삭제한다.
                window.removeEventListener('message', listener)                
            }
            
            // 2. onmessage 이벤트에 리스너를 등록한다.
            window.addEventListener('message', listener)
        })
    }

이런 식으로 팝업을 열어주면 될 것이다. Event 를 삭제시키는 window.removeEventListener 는 로직의 마지막에서 실행이 되도록 했다. postMessage 는 팝업이 열리는 곳에서 window.opener.postMessage("보낼 데이터") 만 호출하면 되기 때문에 어렵지 않게 사용할 수 있으며, 호출 후에는 Promise 내의 listener 의 e.data 안에 팝업에서 전달한 데이터가 꽂힐 것이다.

 

사용할 때의 주의할 점은,

 

첫번째로는 EventListener 가 Set 이 아닌 Add 이기 때문에 같은 이벤트가 중첩이 될 수 있다. 그렇기 때문에 EventListener 가 다른 곳에서 Add 되는 경우에는 postMessage 로 전송된 데이터도 등록된 모든 곳에 던져주기 때문에 메세지 이벤트를 바탕으로 등록하고 있는 경우에는 AddEventListener 를 인스턴트로 사용을 피하는 게 좋다. 만약 하게 된다면 다른 곳에서 작동하지 못하도록 처리를 해주어야 한다.

 

두번째로는 팝업이 열려진 상태에서 다른 곳에서 postMesage가 불리는 경우이다. 아무래도 팝업에서는 opener 객체를 통해 상위 윈도우의 postMessage 를 호출하는 것이기 때문에 만약에 개발자 콘솔에서 postMessage 를 호출하게 되면 팝업에서 일이 처리되지 않았는데도 불구하고 다음 스텝으로 작동이 되지 않는 불상사가 발생하게 된다. 그래서 그러한 예외사항도 잘 처리하는 것을 고민해봐야 할 것이다.

 

세번째로는 팝업에서 아무 데이터도 떨어트려주지 않고 꺼지는 경우가 있다. 예를 들면, 어떤 것을 선택해야 되는데 닫기 버튼을 누른다거나 윈도우를 닫아버리는 경우가 있는데, 이 경우에는 removeEventListener 를 부르지 않았기 때문에 제대로 이벤트가 해제되지 않았을 것이다. 그래서 윈도우가 닫아질 때에는 어떻게 처리해야 할 지도 고민해보아야 할 것이다.


해당 패턴 자체는 팝업이 아니라도 콜백받고자 하는 로직이 있으면 충분히 사용할 수 있는 로직이기 때문에 초보자들은 잘 기억해 놓으면 피와 살이 될 것이다

반응형
Comments