웹 서비스를 개발하다 보면 사용자가 여러 개의 탭을 동시에 켜두고 작업할 때 특정 탭에서 로그아웃을 하거나 장바구니 목록을 변경해도 다른 탭에 즉시 반영되지 않는 문제를 경험해봤을 것이다. 다만 대부분의 개발자들은 localStorage의 storage 이벤트를 편법으로 활용하거나 주기적인 폴링(Polling)으로 서버와 브라우저 자원을 낭비하는 경우가 많다. 이번에는 BroadcastChannel API가 정확히 무엇인지, 왜 필요하며, 다중 탭 간 실시간 상태 동기화를 어떻게 깔끔하게 구현하는지 완벽하게 정리해서 소개하겠다.
BroadcastChannel API는 동일한 출처(Same-Origin)를 공유하는 브라우저 탭, 창, iframe, 서비스 워커 간에 메시지를 양방향으로 주고받을 수 있게 해주는 웹 표준 API다. Complex한 백엔드 통신이나 서드파티 라이브러리 없이 순수 자바스크립트만으로 탭 간 상태 통신을 처리한다.
과거에는 localStorage의 변수 변경을 감지하는 storage 이벤트를 주로 썼다. 하지만 storage 이벤트는 동일한 탭 내부에서는 발생하지 않고 타 탭에서만 감지되며, 반드시 DOM Storage에 저장되어 데이터가 디스크나 메모리에 남아있어야 한다는 명확한 단점이 있었다.
| 구분 | BroadcastChannel API | localStorage (storage 이벤트) | WebSocket / SSE |
|---|---|---|---|
| 동작 영역 | 동일 출처(Same-Origin) 브라우저 Context | 동일 출처 브라우저 Storage | 클라이언트-서버 전 영역 |
| 서버 부하 | 전혀 없음 (브라우저 내부 통신) | 전혀 없음 | 서버 커넥션 및 트래픽 발생 |
| 데이터 휘발성 | 휘발성 (메시지 전달 후 즉시 소멸) | 비휘발성 (직접 삭제 필요) | 휘발성 / 서버 구현에 종속 |
| 구현 난이도 | 매우 쉬움 (간단한 객체 생성 후 사용) | 보통 (JSON 직렬화/파싱 필요) | 높음 (백엔드 인프라 필요) |
BroadcastChannel을 사용하는 절차는 아주 단순하다. 채널 이름을 지정하여 객체를 생성하고, 메시지를 전송하며, 이벤트 리스너를 통해 수신하면 된다. 작업이 끝나면 메모리 누수를 방지하기 위해 채널을 닫아주는 관리가 필수적이다.
1. 채널 생성: 동일한 채널 이름을 공유해야 상호간에 메시지가 수신된다.
2. 메시지 전송: postMessage() 메서드로 객체, 배열, 문자열 등 다양한 데이터 타입 전달.
3. 메시지 수신: onmessage 이벤트 핸들러 등록.
4. 채널 닫기: close() 메서드로 연결 해제.
실무에서 자주 발생하는 탭 간 인증 상태 및 실시간 데이터 변경을 올바르게 구현해보자.
✗ storage 이벤트와 setInterval 폴링을 혼용한 잘못된 구현이다. 불필요한 스토리지 입출력이 발생하고 메모리 정리가 누락된다.
// ✗ 잘못된 방식: localStorage에 의존한 탭 간 상태 동기화
window.addEventListener('storage', (event) => {
if (event.key === 'user_session') {
// JSON 파싱 에러 위험 및 동일 탭 감지 불가능
const session = JSON.parse(event.newValue);
if (!session) {
alert('로그아웃 되었습니다.');
location.reload();
}
}
});
// 탭 간 상태 확인을 위해 주기적으로 폴링 진행 (자원 낭비)
setInterval(() => {
const token = localStorage.getItem('access_token');
if (!token) console.log('세션만료 체크 중...');
}, 3000);
✓ BroadcastChannel을 도입해 불필요한 스토리지 접근 없이 메모리 단에서 즉시 탭 간 이벤트를 동기화하는 코드다.
// ✓ 올바른 방식: BroadcastChannel 기반 싱글톤 관리 모듈
class AuthSyncManager {
constructor() {
// 'auth_channel'이라는 공유 통로 개설
this.channel = new BroadcastChannel('auth_channel');
this.initListener();
}
initListener() {
this.channel.onmessage = (event) => {
const { action, payload } = event.data;
console.log(`[수신된 이벤트]: ${action}`, payload);
switch (action) {
case 'LOGOUT':
this.handleLogout();
break;
case 'CART_UPDATED':
this.updateCartUI(payload.count);
break;
default:
break;
}
};
}
// 다른 탭으로 이벤트 전파
broadcast(action, payload = {}) {
this.channel.postMessage({ action, payload });
}
handleLogout() {
alert('다른 탭에서 로그아웃 되었습니다. 로그인 페이지로 이동합니다.');
window.location.href = '/login';
}
updateCartUI(count) {
const badge = document.querySelector('#cart-count');
if (badge) badge.textContent = count;
}
// 탭이 닫히거나 SPA 컴포넌트 해제 시 호출
destroy() {
this.channel.close();
}
}
// 사용 예시
const authSync = new AuthSyncManager();
// 사용자가 탭 A에서 로그아웃 버튼을 눌렀을 때 실행
document.querySelector('#logout-btn').addEventListener('click', () => {
// 1. 현재 탭 로그아웃 처리
localStorage.removeItem('access_token');
// 2. 다른 모든 탭에 로그아웃 사실을 알림
authSync.broadcast('LOGOUT');
window.location.href = '/login';
});위 코드를 실행하고 여러 탭을 연 상태에서 한 탭의 로그아웃 버튼을 클릭하면, 다른 탭에 즉각 수신 이벤트가 전달되어 화면 전환이 이뤄진다.
첫째, 동일 출처 정책(Same-Origin Policy) 제한을 간과하는 실수다. 프로토콜, 도메인, 포트 중 하나라도 다르면 BroadcastChannel로 메시지를 주고받을 수 없다. 예를 들어 http://localhost:3000과 http://localhost:8080은 전혀 다른 채널로 취급된다.
둘째, 채널 닫기(close) 누락이다. Single Page Application(SPA) 환경에서 React, Vue 컴포넌트가 마운트될 때 채널을 생성하고 언마운트 시 close()를 호출하지 않으면 핸들러가 이중 등록되거나 메모리 누수가 발생한다.
셋째, 복사 불가능한 데이터 객체 전달 시도다. postMessage는 구조화된 복사 알고리즘(Structured Clone Algorithm)을 사용하므로 DOM 엘리먼트, 함수, Symbol 등은 전송할 수 없으며 DataCloneError 예외가 발생한다.
BroadcastChannel API는 다중 탭 환경에서 최적의 실시간 데이터 동기화를 제공하는 강력한 브라우저 표준이다. 작은 브라우저 API 하나를 올바르게 채택하는 습관이 모여서 불필요한 네트워크 요청과 메모리 낭비를 줄이는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 실전 예제와 채널 라이프사이클 관리 코드를 참고해 탭 간 상태 관리 최적화를 진행하면, 매끄러운 사용자 경험과 깔끔한 아키텍처를 얻을 수 있을 것이다.