리액트 EventBus 활용

2024. 7. 21. 20:17·프론트엔드/React

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' 카테고리의 다른 글

오픈소스 라이브러리 react-access-guard 만들기  (0) 2025.03.23
웹 접근성  (0) 2024.03.17
리액트 프로젝트 폴더 구조  (0) 2023.04.08
useIntersectionObserverRef 커스텀 훅 만들기  (1) 2023.02.16
Portal  (0) 2022.07.06
'프론트엔드/React' 카테고리의 다른 글
  • 오픈소스 라이브러리 react-access-guard 만들기
  • 웹 접근성
  • 리액트 프로젝트 폴더 구조
  • useIntersectionObserverRef 커스텀 훅 만들기
정현우12
정현우12
  • 정현우12
    정현우의 개발 블로그
    정현우12
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 프론트엔드 (56)
        • JavaScript, TypeScript (12)
        • 스타일링 (1)
        • React (13)
        • Next.js (4)
        • 개발 환경 (9)
        • 테스트 (3)
        • 성능 최적화 (11)
        • 함수형 프로그래밍 (2)
        • 구조 (1)
      • 프로젝트 회고 (23)
        • 이미지편집기 개발 (7)
        • 엑셀 다운로드, 업로드 공통 모듈 개발 (4)
        • 사용자 매뉴얼 사이트 개발 (3)
        • 통계자동화 솔루션 개발 (1)
        • 엑셀 편집기 개발 (5)
        • API 플랫폼 (1)
        • 콜센터 솔루션 OB 캠페인 (1)
        • AI 스튜디오 (1)
      • 백엔드 (0)
  • 블로그 메뉴

    • 홈
    • 포트폴리오
    • 태그
  • 인기 글

  • 태그

    이미지 편집기
    엑셀
    React
    웹 성능 최적화
    엑셀 에디터
    React-boilerplate
    JavaScript
    TypeScript
    useReducer
    Github Actions
    로딩 성능 최적화
    frontend
    커스텀 훅
    회고
    사용자 매뉴얼 사이트
    라이브러리 선정
    webpack
    memoization
    렌더링 성능 최적화
    Next.js
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
정현우12
리액트 EventBus 활용
상단으로

티스토리툴바