탭간 데이터 공유하기
같은 origin에서 탭간 데이터를 서로 공유하는 방법
2025-09-14
채팅상담 시 상담원이 보는 화면을 개발하고 있는 중에 아래와 같은 요구사항이 존재
- 어느 채팅상담 페이지에 있더라도, 특정 버튼을 클릭 시 상담메인 페이지로 라우팅 후, 고객정보를 새로운 탭UI로 띄운다.
- 이 때 상담메인 페이지가 브라우저 상에서 여러 탭일 경우, 회원정보가 모든 브라우저에서 동기화되어야 한다.
처음 생각한 방식
버튼 클릭 시 커스텀한 이벤트를 만들어서 전파를 하고, 해당 이벤트를 감지하면 되지 않을까?
//커스텀한 이벤트를 만들고, 감지하자
//요런 느낌으로 구현하면 될 거 같았음.
const customEvent = new CustomEvent('custom-click', {
bubbles: true,
detail: 채팅상담중인사용자의정보
});
//버튼 클릭 시 핸들러
const handleButtonClick = () => {
dispatchEvent(customeEvent);
}
//최상단에서 이벤트 감지
const 사용자의정보를_받아서_상담메인_탭에_추가하는_함수 = (data:사용자의정보) => {}
useEffect(() => {
window.addEventListener("custom-click",사용자의정보를_받아서_상담메인_탭에_추가하는_함수))
},[])
해당 방식의 문제점
브라우저의 여러 탭에서 같은 상담메인페이지를 열어둔 경우 사용자가 보고 있는 페이지에서만 동작함.
브라우저의 탭은 최상위 브라우징 컨텍스트로 각 탭은 자체 Document, Window, History, FrameTree, 자바스크립트 글로벌 객체를 가진다.
window.addEventListener는 여러 탭에 걸쳐 동작하지 않는 이유는 브라우저의 기본 보안 모델 때문. 각 탭은 별도의 실행 환경(context)에서 작동하기 때문
브라우저에서 각 탭은 독립된 윈도우 객체를 가지며,
이벤트 리스너는 해당 윈도우 객체에만 바인딩됩니다.
따라서 한 탭에서 등록한 이벤트 리스너는 다른 탭에서는 감지되지 않습니다.
- 암묵적 공유가 없다: 탭은 서로의 힙에 손댈 수 없다. 공유를 원하면 반드시 저장소나 메시징 같은 “명시적 채널”을 열어야 한다.
- 세션 단위 상태: sessionStorage, 페이지 내부 상태, window.name 등은 본질적으로 탭 한정.
위에서 언급한 "명시적 채널"을 사용한다면 통신이 가능하며 이는 대표적으로
로컬 스토리지
, BroadcastChannel
, postMessage
등이 있다.
이 과정에서 BroadCastChannel을 알게 되었고, 해당 API를 이용해서 구현하기로 결정
BroadCastChannel을 통해 상담메인의 여러 탭들간 동기화를 시켜주고자 했음
BroadCastChannel
BroadcastChannel API란 동일한 origin의 브라우징 맥락(창, 탭, 프레임, iframe, …) 간 데이터 통신을 가능하게 하는 기술
new BroadcastChannel('name')로 참가할 수 있으며 channel.postMessage(data)로 모든 참가자에 전달할 수 있다.
하지만, 지원되지 않는 브라우저가 있을 수 있으며 동일 출처만 참가할 수 있다는 단점(높은 보안성을 의미하므로 단점으로 보기는 어려울 수 있음)이 있다.
기본적으로 BroadcastChannel 객체를 생성하고,
생성된 객체에서 postMessage 메서드를 호출하면 해당 채널에 연결된 BroadcastChannel 객체에 전달되게끔 한다.
간략하게 사용법을 정리해보자면,,
// 채널 생성 (동일한 이름의 채널끼리 통신 가능)
const channel = new BroadcastChannel("tab_communication");
// 메시지 수신 이벤트 리스너 등록
channel.onmessage = (event) => {
console.log("메시지 수신:", event.data);
document.getElementById("received").textContent = event.data;
};
// 메시지 전송 함수
function sendMessage() {
const message = document.getElementById("message").value;
channel.postMessage(message);
}
// 채널 연결 종료 (페이지 언로드 시 권장)
function closeChannel() {
channel.close();
}
그래서 어떻게 구현하였을까?
먼저 싱글톤형태의 클래스를 하나 만들었음.
요 클래스는 브로드캐스트 채널을 사용하여 여러 탭 간에 고객 정보를 공유하는 것이 목적임
- sendCustomerInfo(): 고객 정보를 다른 탭으로 전송.
- onReceiveCustomerInfo(): 다른 탭에서 전송된 고객 정보를 받아 콜백함수 실행
import { CustomerTabInfo } from "@/app/(상담)/(상담메인)/c2001/hooks/use-consulting-tabs";
class CustomerInfoChannel {
//싱글톤형태로 만들기
static instance: CustomerInfoChannel | null = null;
channel: BroadcastChannel | null = null;
constructor() {
if (CustomerInfoChannel.instance) {
return CustomerInfoChannel.instance;
}
this.channel = new BroadcastChannel("customer-info-channel");
CustomerInfoChannel.instance = this;
}
//고객정보를 브로드캐스트 채널을 통해 전송(내부적으로 채널의 postMessage사용)
sendCustomerInfo(newTabInfo: CustomerTabInfo) {
if (this.channel == null) {
return;
}
this.channel.postMessage(newTabInfo);
}
//브로드캐스트 채널에서 메시지 수신 시 등록된 콜백 실행
onReceiveCustomerInfo(callback: (newTabInfo: CustomerTabInfo) => void) {
if (this.channel == null) {
return;
}
this.channel.onmessage = (event) => {
callback(event.data);
};
}
static getInstance() {
if (!CustomerInfoChannel.instance) {
CustomerInfoChannel.instance = new CustomerInfoChannel();
}
return CustomerInfoChannel.instance;
}
}
export { CustomerInfoChannel };
이렇게 CustomerInfoChannel이라는 브로드캐스트 채널을 만들고, 실제 채팅페이지에서 상담메인 버튼 클릭 시 요런 로직을 넣어둠
//버튼 클릭 시 브로드캐스트 채널에서 sendCustomerInfo호출
const handleConsultingMainButton = async (contact) => {
const customerChannel = CustomerInfoChannel.getInstance();
if (회원인가) {
const customerInfo = await searchCustomerInfoByUserId(userId);
customerChannel.sendCustomerInfo({
userInfo: {
phoneNumber: customerInfo?.phoneNumber ?? null,
clientNo: customerInfo?.clientNo ?? null,
type: "member",
isAuthenticated:
contact.getAttributes()?.["isAuthenticated"]?.value === "Y",
},
});
return;
}
window.open("/contacts-main", "managerTab");
};
이제 sendCustomerInfo로 전송된 고객정보를 받는 로직을 심어둠.
export function ReceiveAddCustomerEvent({
children,
}: {
children: React.ReactNode;
}) {
const { addUserInConsultantTab } = useConnectHandler();
const { addNewTabAndActivate } = useConsultingTabs();
const channel = CustomerInfoChannel.getInstance();
useEffect(() => {
//채널에서 고객정보를 받을 때 새 탭으로 추가를 한다.
channel.onReceiveCustomerInfo((customerData) => {
addNewTabAndActivate(customerData);
});
}, [addNewTabAndActivate, channel]);
return <>{children}</>;
}
onReceiveCustomerInfo를 통해, 전송된 고객정보를 받아서 상담메인의 새 탭에 넣는 콜백을 실행
언제 쓰일 수 있을까?
지금과 같은 요구사항 이외에도 충분히 다른 곳에서 활용할 수 있다.
예를 들어서,,,
A 웹뷰 액티비티에서 B 액티비티, 또는 B, C, D 액티비티 전부로 데이터를 보내야 하는 경우가 있을 수 있다.
웹뷰의 액티비티란?
-
웹뷰 액티비티는 안드로이드 앱 내에서 웹 콘텐츠를 표시하는 액티비티입니다.
액티비티는 안드로이드에서 사용자 인터페이스를 구성하는 기본 요소로, 하나의 화면을 의미합니다.
웹뷰(WebView)는 앱 내에서 웹 콘텐츠를 표시할 수 있는 컴포넌트이고,
이를 포함한 액티비티를 웹뷰 액티비티라고 합니다 .
예를 들어서 ,,
- 게시글 리스트에서 게시글 상세 액티비티를 연다.
- 게시글 상세 액티비티에서 좋아요를 누른다
- 게시글 상세 액티비티를 닫았을 때(뒤로가기), 수정된 정보가 게시글 리스트 액티비티에 반영되어 있다.
추가: 브로드캐스트 채널 기능을 제공하는 라이브러리
요런 추상화된 훅으로 분리해도 좋을듯..
import { useEffect, useMemo, useState } from "react";
export function useBroadcastChannel<T>(key: string) {
const [message, setMessage] = useState<T>();
const channel = useMemo(() => {
return new BroadcastChannel(key);
}, [key]);
useEffect(() => {
channel.addEventListener("message", (event) => {
setMessage(event.data);
});
}, [channel]);
return { message, channel };
}
tanstack-query에서도 실험적으로 연구중..
broadcastQueryClient (실험적)
매우 중요: 이 유틸리티는 현재 실험적 단계입니다. 이는 마이너 및 패치 릴리스에서 호환성이 깨지는 변경사항이 발생할 수 있음을 의미합니다. 사용 시 주의하세요. 실험적 단계의 기능을 프로덕션 환경에서 사용하시려면, 예기치 않은 문제를 방지하기 위해 패치 레벨 버전을 고정하는 것을 권장합니다.
broadcastQueryClient는 동일한 출처(origin)를 가진 브라우저 탭/창 간에 queryClient의 상태를 브로드캐스트하고 동기화하는 유틸리티입니다.
설치
이 유틸리티는 별도의 패키지로 제공되며 @tanstack/query-broadcast-client-experimental` 임포트로 사용할 수 있습니다.
사용법
broadcastQueryClient함수를 임포트하고, 귀하의 QueryClient인스턴스를 전달하며, 선택적으로broadcastChannel을 설정할 수 있습니다.
tsx
import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental";
const queryClient = new QueryClient();
broadcastQueryClient({
queryClient,
broadcastChannel: "my-app",
});
broadcastQueryClient
이 함수에 QueryClient인스턴스와 선택적으로 broadcastChannel을 전달합니다.
broadcastQueryClient({ queryClient, broadcastChannel });
Options
옵션 객체:
interface BroadcastQueryClientOptions {
/** 동기화할 QueryClient */
queryClient: QueryClient;
/** 탭과 창 사이에 통신하는 데 사용될
* 고유한 채널 이름입니다 */
broadcastChannel?: string;
/** BroadcastChannel API에 대한 옵션 */
options?: BroadcastChannelOptions;
}
기본 옵션은 다음과 같습니다:
{
broadcastChannel = 'tanstack-query',
}
https://tanstack.com/query/latest/docs/framework/react/plugins/broadcastQueryClient#broadcastqueryclient