멀티 탭간 인터렉션에 대해 고려해야하는 순간이 있습니다.
독립적 탭에서 발생하는 인터렉션을 구독해서 화면을 업데이트(예를 들면 받은 좋아요 혹은 새로운 메세지)를 해야할 수 있습니다.
그럴 때 유용한 기능이 Broadcast Channel API입니다.
Broadcast Channel API는 동일 origin을 가진 웹페이지간 통신 기능을 제공합니다.
채널 명이 동일하다면 해당 채널에 대한 메세지를 수신할 수 있기 때문에 멀티 탭간 통신에 유용한 API 입니다.
이 기능을 이용해서 React 앱에서의 멀티 탭 통신 예제를 만들어 보도록 하겠습니다.
BroadcastChnnelManager를 만들어 중앙에서 모든 채널을 관리하도록 의도했습니다.
BroadcastChnnelManager의 목표는 앱에서 이용되는 채널과 각 채널의 메세지의 관리입니다.
아래와 같이 정의했고 가장 중요한 채널의 이름과 메세지 포멧의 타입을 제한했습니다.
class BroadcastChannelManager<
TChannelNames extends string = string,
> {
private channels: Map<TChannelNames, BroadcastChannel> = new Map()
constructor() {}
getChannel(name: TChannelNames) {
return this.channels.get(name)
}
createChannel(name: TChannelNames) {
if (!this.channels.has(name)) {
const channel = new BroadcastChannel(name)
this.channels.set(name, channel)
}
return this.channels.get(name)
}
postMessage(name: TChannelNames, message: MessageData) {
let channel = this.getChannel(name)
if (channel) {
channel.postMessage(message)
} else {
channel = new BroadcastChannel(name)
channel.close()
}
}
addEventListener(
name: TChannelNames,
listener: (event: MessageEvent<MessageData>) => void,
) {
const channel = this.getChannel(name)
if (channel) {
channel.addEventListener('message', listener)
}
}
removeEventListener(
name: TChannelNames,
listener: (event: MessageEvent<MessageData>) => void,
) {
const channel = this.getChannel(name)
if (channel) {
channel.removeEventListener('message', listener)
}
}
closeChannel(name: TChannelNames) {
const channel = this.getChannel(name)
if (channel) {
channel.close()
this.channels.delete(name)
}
}
}
이렇게 정의한 BroadcastChnnelManager는 useBroadcastChannel훅을 통해 컴포넌트에 공급됩니다.
import { useEffect, useRef } from 'react'
import BroadcastChannelManager from './channel-manager'
import { ChannelNames, MergedMessage } from './types'
const manager = new BroadcastChannelManager<ChannelNames>()
manager.createChannel('global')
export function useBroadcastChannel<T extends ChannelNames>(
name: T,
onMessage?: (payload: MergedMessage[T]) => void,
) {
const onMessageRef = useRef(onMessage)
onMessageRef.current = onMessage
const postMessage = (message: MergedMessage[T]) => {
manager.postMessage(name, message)
}
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
onMessageRef.current?.(event.data)
}
manager.addEventListener(name, handleMessage)
return () => {
manager.removeEventListener(name, handleMessage)
}
}, [name])
return {
postMessage,
}
}
사용예시
메세지 발신
홈에서 메세지를 전파합니다. 지속적 운영 관리를 위해 가장 중요한 부분은 채널에서 사용중인 메세지 타입과 payload를 타입으로 관리하는 것이라 생각합니다. 아래 postMessage에서 전달되는 매개변수는 타입정의에 의해 자동완성됩니다.
export default function Home() {
const { postMessage } = useBroadcastChannel('global')
const handleClick = () => {
postMessage({
type: 'updateProfile',
payload: {
updateDate: Date.now().toString(),
userName: '새로운 이름',
},
})
}
return (
<main>
<Button label="버튼" onClick={handleClick} />
<Text type="title">홈 페이지</Text>
</main>
)
}
메세지 수신
수신처 두번째 매개변수로 채널의 메세지를 수신합니다.
마찬가지로 채널별 메세지 타입을 관리되고 있기 때문에 payload.userName은 자동완성되고 타입이 엄격하게 관리됩니다.
export default function Page() {
useBroadcastChannel('global', data => {
switch (data.type) {
case 'updateProfile': {
console.log(data.payload.userName)
break
}
}
})
return (
<div>
<Text type="title"> 관리자만 볼 수 있음</Text>
</div>
)
}
'react' 카테고리의 다른 글
[설계] 격리된 컴포넌트간의 통신 설계 (feat. Eventbus) (0) | 2024.06.28 |
---|---|
[Nextjs] next13에서 react-query의 필요성 (0) | 2024.06.27 |
[설계] 의존성 역전으로 변경에 유연한 설계하기 (0) | 2024.06.26 |
[설계] Compound Pattern으로 변경에 유연한 컴포넌트 설계하기 (0) | 2024.06.24 |
실무에서 가장 많이 실수하는 useEffect 사례 (0) | 2024.06.23 |