탭간 데이터 공유하기

2025-09-14

같은 origin에서 탭간 데이터를 서로 공유하는 방법

채팅상담 시 상담원이 보는 화면을 개발하고 있는 중에 아래와 같은 요구사항이 존재

처음 생각한 방식

버튼 클릭 시 커스텀한 이벤트를 만들어서 전파를 하고, 해당 이벤트를 감지하면 되지 않을까?
//커스텀한 이벤트를 만들고, 감지하자
//요런 느낌으로 구현하면 될 거 같았음.
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)에서 작동하기 때문

브라우저에서 각 탭은 독립된 윈도우 객체를 가지며,

이벤트 리스너는 해당 윈도우 객체에만 바인딩됩니다.

따라서 한 탭에서 등록한 이벤트 리스너는 다른 탭에서는 감지되지 않습니다.

위에서 언급한 "명시적 채널"을 사용한다면 통신이 가능하며 이는 대표적으로로컬 스토리지, 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();
}

그래서 어떻게 구현하였을까?

먼저 싱글톤형태의 클래스를 하나 만들었음.

요 클래스는 브로드캐스트 채널을 사용하여 여러 탭 간에 고객 정보를 공유하는 것이 목적임

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)는 앱 내에서 웹 콘텐츠를 표시할 수 있는 컴포넌트이고,
이를 포함한 액티비티를 웹뷰 액티비티라고 합니다 .

예를 들어서 ,,

  1. 게시글 리스트에서 게시글 상세 액티비티를 연다.
  2. 게시글 상세 액티비티에서 좋아요를 누른다
  3. 게시글 상세 액티비티를 닫았을 때(뒤로가기), 수정된 정보가 게시글 리스트 액티비티에 반영되어 있다.

추가: 브로드캐스트 채널 기능을 제공하는 라이브러리

GitHub - pubkey/broadcast-channel: :satellite: BroadcastChannel to send data between different browser-tabs or nodejs-processes + LeaderElection over the channels https://pubkey.github.io/broadcast-channel/

요런 추상화된 훅으로 분리해도 좋을듯..

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