BroadcastChannel이란?
BroadcastChannel은 브라우저 창, 탭 간 메시지 통신을 할 수 있는 Web API입니다.
핵심 특징
동일 출처 정책 (Same-Origin Policy):
- 본 기능을 이용해 메시지 통신을 하기 위해서는
- Browsing Context 간 동일한 출처여야 한다는 엄격한 규약
동일한 출처 조건:
- 콘텐츠의 **스킴(Scheme)**이 일치
- **도메인(Domain)**이 일치
- **포트(Port)**가 일치
✅ 동일 출처
https://example.com:443/page1
https://example.com:443/page2
❌ 다른 출처
https://example.com:443 (HTTPS)
http://example.com:80 (HTTP - 스킴 다름)
https://example.com (example.com)
https://sub.example.com (서브도메인 - 도메인 다름)
https://example.com:443 (포트 443)
https://example.com:8080 (포트 8080 - 포트 다름)
왜 BroadcastChannel을 사용하는가?
기존 방법의 한계
별도의 서버 통신 필요:
탭 1 ←→ WebSocket Server ←→ 탭 2
탭 1 ←→ MQTT Broker ←→ 탭 2
탭 1 ←→ Firebase ←→ 탭 2
문제점:
- 서버 인프라 필요
- 네트워크 비용
- 지연 시간 (Latency)
- 추가 복잡성
BroadcastChannel의 장점
서버 없이 직접 통신:
탭 1 ←→ BroadcastChannel ←→ 탭 2
(브라우저 내부)
이점:
- ✅ 서버 불필요
- ✅ 무료
- ✅ 빠른 속도
- ✅ 간단한 구현
- ✅ 동일 기기 내 완벽한 동기화
사용 사례
BroadcastChannel을 이용한다면, 동일한 서비스의 [창/탭] 간에 동일한 데이터(Data Sync)를 보여줘야 한다면 해결할 수 있습니다.
실제 활용 예시:
-
사용자 로그인/로그아웃 동기화
탭 1에서 로그아웃 → 모든 탭에서 자동 로그아웃 -
장바구니 동기화
탭 1에서 상품 추가 → 탭 2의 장바구니에도 즉시 반영 -
알림 읽음 상태 동기화
탭 1에서 알림 읽음 → 탭 2의 알림 배지 숫자 감소 -
다크 모드 설정 동기화
탭 1에서 다크 모드 켜기 → 모든 탭이 다크 모드로 전환
브라우저 지원 현황 (Spec)
Can I Use
주요 브라우저 지원:
- ✅ Chrome 54+
- ✅ Firefox 38+
- ✅ Edge 79+
- ✅ Safari 15.4+
- ✅ Opera 41+
미지원:
- ❌ Internet Explorer (전체)
- ❌ Safari 15.3 이하
모바일 지원:
- ✅ Chrome Android
- ✅ Firefox Android
- ✅ Safari iOS 15.4+
폴리필 고려
구형 브라우저 지원이 필요하다면:
- LocalStorage + Storage Event 사용
- SharedWorker 활용
- 폴리필 라이브러리 사용
기본 사용법
1. 채널 생성
// 같은 이름의 채널은 모든 탭에서 공유됩니다
const channel = new BroadcastChannel('my-channel');2. 메시지 보내기
// 메시지 전송 (현재 탭 제외한 모든 탭에 전달)
channel.postMessage('Hello from Tab 1!');
// 객체도 전송 가능
channel.postMessage({
type: 'UPDATE_USER',
payload: {
userId: 123,
name: 'John Doe'
}
});
// 배열도 전송 가능
channel.postMessage(['item1', 'item2', 'item3']);3. 메시지 받기
// 메시지 수신
channel.onmessage = (event) => {
console.log('받은 메시지:', event.data);
// 메시지 타입에 따라 처리
if (event.data.type === 'UPDATE_USER') {
updateUserUI(event.data.payload);
}
};
// 또는 addEventListener 사용
channel.addEventListener('message', (event) => {
console.log('받은 메시지:', event.data);
});4. 채널 닫기
// 더 이상 사용하지 않을 때 채널 닫기
channel.close();실전 예제
예제 1: 로그인/로그아웃 동기화
// auth-sync.js
const authChannel = new BroadcastChannel('auth-channel');
// 로그인 처리
function login(user) {
// 로컬 상태 업데이트
localStorage.setItem('user', JSON.stringify(user));
// 다른 탭에 알림
authChannel.postMessage({
type: 'LOGIN',
payload: user
});
// UI 업데이트
updateAuthUI(user);
}
// 로그아웃 처리
function logout() {
// 로컬 상태 제거
localStorage.removeItem('user');
// 다른 탭에 알림
authChannel.postMessage({
type: 'LOGOUT'
});
// UI 업데이트
updateAuthUI(null);
}
// 메시지 수신
authChannel.onmessage = (event) => {
const { type, payload } = event.data;
switch (type) {
case 'LOGIN':
localStorage.setItem('user', JSON.stringify(payload));
updateAuthUI(payload);
break;
case 'LOGOUT':
localStorage.removeItem('user');
updateAuthUI(null);
// 로그인 페이지로 리다이렉트
window.location.href = '/login';
break;
}
};예제 2: 장바구니 동기화
// cart-sync.js
const cartChannel = new BroadcastChannel('cart-channel');
// 장바구니에 상품 추가
function addToCart(product) {
// 현재 장바구니 가져오기
const cart = getCart();
// 상품 추가
cart.push(product);
// localStorage에 저장
localStorage.setItem('cart', JSON.stringify(cart));
// 다른 탭에 알림
cartChannel.postMessage({
type: 'ADD_ITEM',
payload: product
});
// UI 업데이트
updateCartUI(cart);
}
// 장바구니에서 상품 제거
function removeFromCart(productId) {
const cart = getCart();
const updatedCart = cart.filter(item => item.id !== productId);
localStorage.setItem('cart', JSON.stringify(updatedCart));
cartChannel.postMessage({
type: 'REMOVE_ITEM',
payload: productId
});
updateCartUI(updatedCart);
}
// 메시지 수신
cartChannel.onmessage = (event) => {
const { type, payload } = event.data;
switch (type) {
case 'ADD_ITEM':
const cart = getCart();
cart.push(payload);
localStorage.setItem('cart', JSON.stringify(cart));
updateCartUI(cart);
updateCartBadge(cart.length);
break;
case 'REMOVE_ITEM':
const updatedCart = getCart().filter(item => item.id !== payload);
localStorage.setItem('cart', JSON.stringify(updatedCart));
updateCartUI(updatedCart);
updateCartBadge(updatedCart.length);
break;
case 'CLEAR_CART':
localStorage.removeItem('cart');
updateCartUI([]);
updateCartBadge(0);
break;
}
};
function getCart() {
const cart = localStorage.getItem('cart');
return cart ? JSON.parse(cart) : [];
}예제 3: React에서 사용하기
// useBroadcastChannel.ts
import { useEffect, useRef, useState } from 'react';
interface BroadcastMessage<T = any> {
type: string;
payload?: T;
}
export function useBroadcastChannel<T = any>(
channelName: string,
onMessage?: (message: BroadcastMessage<T>) => void
) {
const channelRef = useRef<BroadcastChannel | null>(null);
const [lastMessage, setLastMessage] = useState<BroadcastMessage<T> | null>(null);
useEffect(() => {
// BroadcastChannel 지원 확인
if (typeof BroadcastChannel === 'undefined') {
console.warn('BroadcastChannel is not supported');
return;
}
// 채널 생성
const channel = new BroadcastChannel(channelName);
channelRef.current = channel;
// 메시지 핸들러
channel.onmessage = (event) => {
const message = event.data as BroadcastMessage<T>;
setLastMessage(message);
onMessage?.(message);
};
// 클린업
return () => {
channel.close();
channelRef.current = null;
};
}, [channelName, onMessage]);
// 메시지 전송 함수
const postMessage = (type: string, payload?: T) => {
if (channelRef.current) {
channelRef.current.postMessage({ type, payload });
}
};
return { postMessage, lastMessage };
}
// 사용 예시
function App() {
const { postMessage, lastMessage } = useBroadcastChannel('app-channel', (message) => {
console.log('받은 메시지:', message);
if (message.type === 'THEME_CHANGED') {
setTheme(message.payload);
}
});
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme);
postMessage('THEME_CHANGED', newTheme);
};
return <div>...</div>;
}RemoteSeminar 서비스 적용 사례
본 기술을 이용해 RemoteSeminar 서비스에서 브라우저 각 탭 간 Real-Time으로 다뤄야 할 데이터에 대해 동기화를 구현했습니다.
구현 사례
세미나 참여 상태 동기화:
// seminar-sync.js
const seminarChannel = new BroadcastChannel('seminar-channel');
// 세미나 입장
function joinSeminar(seminarId) {
// 입장 처리
const seminar = fetchSeminarData(seminarId);
// 다른 탭에 알림
seminarChannel.postMessage({
type: 'SEMINAR_JOINED',
payload: {
seminarId,
timestamp: Date.now()
}
});
}
// 세미나 퇴장
function leaveSeminar(seminarId) {
// 퇴장 처리
// 다른 탭에 알림
seminarChannel.postMessage({
type: 'SEMINAR_LEFT',
payload: {
seminarId,
timestamp: Date.now()
}
});
}
// 실시간 상태 동기화
seminarChannel.onmessage = (event) => {
const { type, payload } = event.data;
switch (type) {
case 'SEMINAR_JOINED':
// 다른 탭에서 세미나에 입장했을 때
updateParticipantList();
break;
case 'SEMINAR_LEFT':
// 다른 탭에서 세미나를 나갔을 때
updateParticipantList();
break;
case 'NEW_MESSAGE':
// 새 메시지 알림
displayNewMessage(payload);
break;
}
};주의사항 및 팁
1. 메시지는 현재 탭을 제외한 모든 탭에 전달
const channel = new BroadcastChannel('test');
channel.postMessage('Hello');
// 현재 탭의 onmessage는 호출되지 않음
// 다른 탭의 onmessage만 호출됨해결 방법:
function broadcast(message) {
// 다른 탭에 전송
channel.postMessage(message);
// 현재 탭도 처리
handleMessage(message);
}2. 직렬화 가능한 데이터만 전송 가능
// ✅ 가능
channel.postMessage('string');
channel.postMessage(123);
channel.postMessage({ key: 'value' });
channel.postMessage([1, 2, 3]);
channel.postMessage(null);
// ❌ 불가능
channel.postMessage(function() {}); // 함수
channel.postMessage(Symbol('test')); // Symbol
channel.postMessage(new Date()); // Date 객체는 문자열로 변환됨해결 방법:
// Date는 ISO 문자열로 변환
channel.postMessage({
timestamp: new Date().toISOString()
});
// 함수는 타입만 전달하고 각 탭에서 실행
channel.postMessage({
action: 'LOGOUT' // 각 탭에서 logout() 함수 실행
});3. 메모리 누수 방지
// ❌ 나쁜 예
function initChannel() {
const channel = new BroadcastChannel('test');
channel.onmessage = handleMessage;
// 채널이 닫히지 않음
}
// ✅ 좋은 예
function initChannel() {
const channel = new BroadcastChannel('test');
channel.onmessage = handleMessage;
// 페이지 언로드 시 채널 닫기
window.addEventListener('unload', () => {
channel.close();
});
return channel;
}4. 에러 처리
const channel = new BroadcastChannel('test');
channel.onmessageerror = (event) => {
console.error('메시지 역직렬화 오류:', event);
};
// 안전한 postMessage
function safePostMessage(data) {
try {
channel.postMessage(data);
} catch (error) {
console.error('메시지 전송 실패:', error);
}
}BroadcastChannel vs 다른 방법
LocalStorage + Storage Event
// 예전 방식
localStorage.setItem('key', 'value');
window.addEventListener('storage', (event) => {
if (event.key === 'key') {
console.log('값 변경:', event.newValue);
}
});단점:
- LocalStorage에 실제로 저장해야 함
- 문자열만 가능 (객체는 JSON.stringify 필요)
- 현재 탭에서는 storage 이벤트가 발생하지 않음
SharedWorker
// 더 복잡한 방식
const worker = new SharedWorker('worker.js');
worker.port.postMessage('Hello');단점:
- 별도 워커 파일 필요
- 더 복잡한 설정
- 디버깅 어려움
BroadcastChannel이 가장 간단!
// 간단!
const channel = new BroadcastChannel('test');
channel.postMessage({ any: 'data' });
channel.onmessage = (e) => console.log(e.data);샘플 코드
전체 예제 코드는 GitHub 저장소를 참고하세요.
결론
BroadcastChannel의 장점
- ✅ 간단한 API: 배우기 쉽고 사용하기 쉬움
- ✅ 서버 불필요: 브라우저만으로 탭 간 통신
- ✅ 빠른 속도: 네트워크 지연 없음
- ✅ 무료: 추가 인프라 비용 없음
- ✅ 실시간 동기화: 즉각적인 데이터 동기화
사용 시나리오
추천:
- 같은 사이트의 여러 탭 간 상태 동기화
- 로그인/로그아웃 동기화
- 설정 변경 동기화
- 실시간 알림 동기화
비추천:
- 서로 다른 도메인 간 통신 (Same-Origin 제약)
- 영구 저장이 필요한 경우
- 복잡한 메시지 큐 시스템
마치며
BroadcastChannel은 단순하지만 강력한 도구입니다.
적절한 상황에서 사용한다면 복잡한 서버 인프라 없이도 탭 간 완벽한 동기화를 구현할 수 있습니다.