pub/sub 방식의 통신 패턴
window의 eventListener 작동과 거의 유사하다.
이벤트 발생시 해당 이벤트를 구독하는 곳에서 콜백을 실행한다.
구현
출처: https://github.com/DawChihLiou/eventbus-demo/blob/main/eventbus/eventbus.ts
type EventKey = string | symbol
type EventHandler<T = any> = (payload: T) => void
type EventMap = Record<EventKey, EventHandler>
type Bus<E> = Record<keyof E, E[keyof E][]>
interface EventBus<T extends EventMap> {
on<Key extends keyof T>(key: Key, handler: T[Key]): () => void
off<Key extends keyof T>(key: Key, handler: T[Key]): void
once<Key extends keyof T>(key: Key, handler: T[Key]): void
emit<Key extends keyof T>(key: Key, ...payload: Parameters<T[Key]>): void
}
interface EventBusConfig {
onError: (...params: any[]) => void
}
export function eventbus<E extends EventMap>(
config?: EventBusConfig
): EventBus<E> {
const bus: Partial<Bus<E>> = {}
const on: EventBus<E>['on'] = (key, handler) => {
if (bus[key] === undefined) {
bus[key] = []
}
bus[key]?.push(handler)
return () => {
off(key, handler)
}
}
const off: EventBus<E>['off'] = (key, handler) => {
const index = bus[key]?.indexOf(handler) ?? -1
bus[key]?.splice(index >>> 0, 1)
}
const once: EventBus<E>['once'] = (key, handler) => {
const handleOnce = (payload: Parameters<typeof handler>) => {
handler(payload)
// TODO: find out a better way to type `handleOnce`
off(key, handleOnce as typeof handler)
}
on(key, handleOnce as typeof handler)
}
const emit: EventBus<E>['emit'] = (key, payload) => {
bus[key]?.forEach((fn) => {
try {
fn(payload)
} catch (e) {
config?.onError(e)
}
})
}
return { on, off, once, emit }
}
클로져를 활용해서 이벤트 핸들러들을 객체에 관리한다.
on
호출: 이벤트 구독, 객체에 핸들러가 추가됨
off
호출: 객체에서 핸들러 삭제
emit
호출: 개체의 해당 이벤트에 해당하는 핸들러들 다 실행
활용
eventbus를 통해 멀리 떨어져 있는 컴포넌트, 함수간 통신이 가능하다.
1. 바닐라 javascript 함수에서 react 컴포넌트의 메소드, hook 트리거
실무에서 활용한 예시이다.
지금 진행중인 프로젝트에서 Next.js, react, 인증은 authjs를 활용한 모노레포 환경에서 서버 fetch 함수들을 따로 remote란 패키지로 관리하고 있다.
서버 fetch시 응답이 401일 경우에 토큰을 재발급하는 구조로 되어 있다. 토큰을 재발급 받고 서버, 클라이언트에서 공유되는 사용자 세션을 갱신시켜야 하는데,
이 때 문제가 fetch함수들은 바닐라 javascript이고, authjs의 세션 업데이트 (useSession.update)는 hook으로 제공이 되서 fetch함수내에서 호출할 수가 없었다.
이를 해결하기 위해 ui에 세션 업데이트용 컴포넌트를 하나 넣고, 세션 관련 이벤트를 구독하여, 세션 업데이트를 처리하도록 구현했다.
"use client"
export default function SessionSyncer() {
const session = useSessionData()
const onLogout = useLogout()
// client에서 토큰 재발급, 로그아웃시 세션 정리
useEffect(() => {
const unSubscribeOnUpdate = sessionEventChannel.on(
"onUpdate",
({ accessToken, authUid }) => {
// session 업뎃 서버 처리시간 때문에.. 로컬스토리지 먼저 업데이트함
sessionManager.set("accessToken", accessToken)
sessionManager.set("authUid", authUid)
session.update({ accessToken, authUid })
}
)
const unSubscribeOnLogout = sessionEventChannel.on("onLogout", () => {
onLogout("session-expired")
})
return () => {
unSubscribeOnUpdate()
unSubscribeOnLogout()
}
}, [session.update])
// authjs session 과 localStorage 동기화
useEffect(() => {
async function syncSession() {
if (session.data) {
const { accessToken, userId, authUid } = session.data.info
sessionManager.set("accessToken", accessToken)
sessionManager.set("userId", userId)
sessionManager.set("authUid", authUid)
}
}
syncSession()
}, [session.data, sessionManager])
return null
}
2. 멀리 떨어져 있는 컴포넌트간 통신
위와 동일한 형태로 구현이 가능하다. 실무에서 아직 사용한 적은 없다.
컴포넌트간에는 prop이나 context, 전역 상태, 포탈 등 통신할 수 있는 방법이 많고, 해당 방법들이 데이터가 1방향으로 흘러서 관리하기 편하기 때문에 웬만하면 이 방법은 꼭 필요한 경우가 아니라면 사용하지 않는게 나을 것 같다.
요약
- 장점
- 멀리 떨어져있는 컴포넌트, 함수간 통신이 가능
- event기반이기 때문에 특정 event 발생시 여러 곳에서 해당 event를 다뤄야 할 경우 사용하면 좋을 듯
- 단점
- 기존 react와 다르게, 양방향으로 통신함. 추적, 관리가 힘들 수 있다.
- 내 생각
- vanila js에서 react hook, 컴포넌트 method를 트리거할 수 있는 건 활용성이 크다.
- 컴포넌트간 통신은.. 꼭 필요한 경우에만 쓰는게 좋겠다.
'프론트엔드 > React' 카테고리의 다른 글
웹 접근성 (0) | 2024.03.17 |
---|---|
리액트 프로젝트 폴더 구조 (0) | 2023.04.08 |
useIntersectionObserverRef 커스텀 훅 만들기 (1) | 2023.02.16 |
Portal (0) | 2022.07.06 |
커스텀 훅 적용하기 - 팝업(모달) (0) | 2022.07.02 |