한 윈도우에서 다른 윈도우를 참조하고, 데이터를 주고 받는 등의 통신이 필요할 때가 있다. 대표적으로 소셜 로그인을 위해 OAuth 처리를 해주는 팝업을 새 윈도우로 띄우고, 인증이 완료되면 다시 본래 창으로 돌아와야 할 때. 인증 관련 정보를 탭 간에 공유하여 로그인 처리를 일관성있게 유지해야 할 때. 어플리케이션의 상태를 윈도우/탭/iframe 사이에 맞춰야 하는 경우가 있을 수 있겠다. 따라서 이번에는 페이지에서 팝업을 열어서 데이터를 주고받아보며, 서로 다른 브라우징 컨텍스트 간에 통신하는 몇 가지 방법들을 살펴보고자 한다.
window.postMessage
Window 오브젝트 사이에서 cross-origin 통신을 하기 위해 가장 많이 쓰이던 API다. 페이지와 생성된 팝업 간의 통신, 또는 페이지와 iframe 간의 통신도 구현할 수 있다.
메세지 발신
메세지를 전송하는 방법은 다음과 같다.
targetWindow.postMessage(message, targetOrigin, [transfer]);
// Page
let popup = window.open('/sample-popup', '_blank', ' width=500, height=700')
const data = { message: "Hello from parent!" };
// 실행할 타겟 도메인을 명시적으로 제공해야 한다.
const targetOrigin = "https://sample-target-origin.com";
popup.postMessage(data, targetOrigin);
우선 팝업 Window를 참조할 수 있는 변수를 만들어야 한다. popup 이라는 변수를 통해 Window 를 생성하고 나서, 해당 윈도우의 postMessage API를 사용하여 전달하고자 하는 데이터를 첫 번째 파라미터로 넣어주면 끝이다. 이때 데이터는 structured clone 알고리즘을 통해 자동으로 직렬화되므로, 객체를 전송할 때에도 데이터 직렬화/역직렬화에 대한 걱정 없이 자유롭게 데이터를 넣으면 된다. (일일이 JSON.stringfy, JSON.parse 해주지 않아도 된다는 것!)
두 번째 인자로는 타겟할 윈도우의 url origin을 명시해줘야 한다. 와일드카드("*")를 통해 모든 url을 허용해줄 수도 있지만, 이 경우 악의적인 사이트가 윈도우의 위치를 변경해 전송된 데이터를 가로챌 수 있는 위험이 있다. 따라서 다른 윈도우로 데이터를 전송할 땐 보안을 위해 정확한 타켓 origin을 지정하는 것이 중요하다.
메세지 수신
이렇게 전송한 데이터를 받기 위해서는 다음과 같이 Event를 디스페치해줘야 한다.
// Popup window
window.addEventListener("message", receiveMessage);
const receiveMessage = (event) => {
if (event.origin !== "http://example.com") {
console.error("Received message from unauthorized origin:", event.origin);
return; // 출처가 예상과 다르면 처리하지 않는다.
}
if (event && event.data && event.data.message) {
console.log(event.data.message)
// Hello from parent!
}
}
message 이벤트 리스너를 통해 다른 윈도우로부터 전송되는 모든 메세지를 받을 수 있다. 이때 단순히 모든 message에 대한 이벤트 리스너를 추가하는 것은 크로스 사이트 스크립팅 공격을 열어두는 셈이다. 따라서 이 경우 알 수 없는 발신자가 악의적인 메세지를 보내는 걸 받지 않도록, 항상 전송하는 측의 신원을 확인할 필요가 있다. 다행히도 이때 전달되는 MessageEvent 는 전송된 메세지 데이터 외에도 origin, source 속성을 가진다. 따라서 해당 속성이 우리가 받고자 하는 페이지의 도메인과 일치하는지 검사하는 조건문을 통해 보안 문제를 다소 우회할 수 있겠다.
Broadcast Channel
Broadcast Channel 은 동일한 출처의 서로 다른 Window 간에 통신할 수 있는 또다른 Web API다. Window 레퍼런스를 직접 가지고 있지 않아도 자유롭게 사용할 수 있다는 장점이 있다. 사용 방법은 다음과 같다.
메세지 발신
const channel = new BroadcastChannel("test_channel");
// Page
window.open('/sample-popup2', '_blank', ' width=500, height=700')
const data = { message: "Hello from parent!" };
channel.postMessage(data);
우선 new 생성자를 통해 BroadcastChannel 인스턴스를 생성한다. 이렇게 싱글톤 객체를 메세지를 전송하려는 페이지에서 참조하여, postMessage 메서드를 통해 데이터를 BroadcaseChannel 객체에 전송해주면 된다. window.postMessage와 동일하게 직렬화에 대한 걱정 없이 자유롭게 데이터를 전송할 수 있다.
메세지 수신
// Popup
channel.addEventListener("message", receiveMessage);
const receiveMessage = (event) => {
if (event.origin !== expectedOrigin) {
console.error("Received message from unauthorized origin:", event.origin);
return; // 출처가 예상과 다르면 처리하지 않는다.
}
if (event && event.data && event.data.message) {
console.log(event.data.message)
// Hello from parent!
}
}
window 의 message 이벤트와 유사하게 메세지 이벤트를 감지할 수 있다. 체널에 메시지가 도착할 경우 이벤트가 발생하며, 마찬가지로 MessageEvent 타입의 객체를 수신함으로써 데이터 외에도 origin, source 등을 검증할 수 있다.
브라우저 컨텍스트 간의 통신을 손쉽게 구현할 수 있는 방법이지만, 안타깝게도 2022년에 생겨난 새로운 API기 때문에 브라우저 호환성이 아직 쓸 수 없는 수준이라는 게 걸린다. (2024.3월 기준 사파리의 가장 최신 버전인 15부터 지원을 시작했다고 한다.)
localStorage
마지막으로 가장 친숙한 Web API인 로컬 스토리지를 사용하는 방법이 있다. 이것은 사실 Window 별로 발생하는 이벤트라기보다는 브라우저 스토리지를 통해 데이터를 우회해서 전달하는 방식이다.
메세지 발신
모두가 알고 있듯이...그저 document 출처의 localStorage에 데이터를 저장하기만 하면 된다. 이렇게 저장한 데이터는 브라우저 세션 간에 공유된다. 사실 메세지를 발신한다기보다는 그냥 스토리지라는 싱글톤 객체에 저장하는 것이다.
// Page
const data = { message: "Hello from parent!" }
// 다음과 같이 DOMString 형태로 데이터를 직렬화해준다.
localStorage.setItem("test-data", JSON.stringify(data))
메세지 수신
메세지를 수신받고자 하는 Window에서 Timer로 인터벌을 주면서, 로컬 스토리지가 변경되는 것을 감지할 수도 있을 것이다. 그런데 그보다 간편한 방법이 있다. 바로 로컬스토리지에도 이벤트 리스너를 거는 것이다.
이런 케이스가 꽤나 빈번했는지, 이미 Web Storage의 변경사항을 감지하는 이벤트 핸들러가 이미 있다고 한다. 해당 이벤트는 동일한 탭 또는 동일한 윈도우 안에서는 발생하지 않고, 오직 서로 다른 탭, 윈도우에서만 발생한다고 하니, 우리의 목적에 부합하다. 이벤트를 걸어 메세지를 수신받는 방법은 다음과 같다.
// Popup
window.addEventListener('storage', () => {
const recievedData = JSON.parse(window.localStorage.getItem('test-data'))
console.log(recievedData.message);
// Hello from parent!
});
결론
서로 다른 Window 간에 이벤트 기반으로 메세지를 주고받는 대표적인 3가지 방법들을 알아보았다. 윈도우/탭 간 동기화를 지원하게 되면 여러 모로 개선된 사용자경험을 제공할 수 있다. 여러 가지 윈도우를 다뤄볼 일이 많지 않아서 막막했는데, 앞으로 필요에 따라 적절한 방법을 선택하면 되겠다.
Reference
https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage
https://developer.mozilla.org/ko/docs/Web/API/BroadcastChannel
https://developer.mozilla.org/ko/docs/Web/API/Window/storage_event
'Web' 카테고리의 다른 글
구글 스프레드 시트로 i18n 메세지 관리 자동화하기 (3) | 2024.11.10 |
---|---|
다국어 프로젝트 시작해보기 (feat. Next.js, next-intl) (0) | 2024.10.13 |
Axios 인터셉터로 JWT 토큰 로테이션 구현하기 (0) | 2024.02.18 |
헷갈리는 줄바꿈, 올바르게 제어해보자 (15) | 2023.12.10 |
JavaScript로 이미지 파일 데이터 다루기 (0) | 2023.08.31 |