CustomEvent로 모듈간 이벤트 발생시키기

#JavaScript#React#CustomEvent2024-08-07

Before Start 📍

저희 회사는 현재 모노레포 구조로 하나의 레포 안에 각각의 기능을 모듈로 분리한 뒤 모듈 페더레이션을 통해 각각의 모듈 컨테이너를 빌드하고 있습니다.

중요한 점은, 모듈간 빌드가 진행되기 때문에 A라는 모듈에서 발생하는 상태변화 혹은 선택값에 대해 B라는 모듈에서는 인식할 수 없다는 것입니다.

물론 모듈을 원격으로 불러올 때 props를 통해 값을 전달해줄 수는 있지만, 만약 해당 모듈이 불러와있지 않은 경우는 어떻게 해야할까요?

예를 들어 어떠한 상태값이 변경되거나, 사용자가 클릭을 했을때 클릭한 내용 혹은 변경된 상태값을 원격으로 불러와있지 않은 모듈에 전달해야하는 경우는 어떻게 해야할까요?

이럴때 사용할 수 있는 하나의 방법으로 **“커스텀 이벤트”**가 있습니다.

CustomEvent란?

CustomEvent는 JavaScript에서 기본적으로 제공하는 이벤트 객체 중 하나로, 브라우저에서 제공하는 기본 이벤트(예: click, mouseover, keydown) 외에 사용자가 직접 정의한 이벤트를 생성할 수 있게 해줍니다. 이를 통해 특정 애플리케이션이나 UI 동작에 적합한 맞춤형 이벤트를 구현할 수 있습니다.

새로운 커스텀 이벤트 뿐만 아니라 기존의 이벤트들(click, mousedown 등)과 같은 내장 이벤트 역시 직접 커스텀으로 만들 수 있습니다.

CustomEvent의 주요 특징

커스텀 이벤트의 주요 내용을 순차적으로 정리해보자면 아래와 같습니다.

사용자 정의 이벤트 생성

위에서 언급했듯이 사용자 정의 이벤트를 생성할 수 있습니다. new CustomEvent(eventName, eventInit) 형식으로 생성합니다.

  • eventName은 이벤트 이름으로 문자열을 받습니다. (커스텀 이벤트의 이름을 설정하는 것으로, 해당 이벤트 이름의 커스텀 이벤트가 발생되고, 이를 추적합니다.)
  • eventInit은 옵션값으로, 추가적인 이벤트 초기화 설정을 제공하는 객체입니다. bubbles, cancelable, detail 속성을 포함합니다.
  • 만들어진 커스텀 이벤트는 dispatchEvent 메서드를 통해 발생시키고, addEventListener를 통해 발생된 이벤트를 전달 받습니다.
const event = new CustomEvent("myCustomEvent", {
  bubbles: true, // 이벤트 전파 여부
  cancelable: true, // 이벤트 취소 가능 여부
  detail: { message: "Hello, World!" }, // 사용자 정의 데이터
})

이벤트 이름은 명확하니 두번째 인자값인 eventInit 객체의 값들에 대해 살펴보겠습니다.

이벤트 전파

커스텀 이벤트를 생성할 때 옵션으로 보낼 수 있는 eventInit 객체의 bubbles 값의 설정에 따라 이벤트 전파를 설정할 수 있습니다. (기본값은 false 입니다.)

bubbles: true 를 설정하면 이벤트가 DOM 트리를 따라 상위로 전파됩니다. 이벤트 캡처링과 버블링을 활용할 수 있어 이벤트 위임이 가능합니다. 즉, 상위 요소가 해당 커스텀 이벤트를 인식하고자 한다면 true 옵션을 사용합니다.

다만 커스텀이벤트의 경우 보통 특정 컴포넌트나 모듈에서 정의된 동작을 직접 실행하거나, 다른 모듈에 필요한 데이터를 전달하는 데 사용됩니다.

또한 dispatchEvent를 호출하여 커스텀 이벤트를 발생시키는데, dispatchEvent를 호출한 요소가 해당 이벤트의 시작과 끝을 담당하기 때문에 이벤트 전파를 고려하지 않아도 되는 경우가 종종 있습니다.

따라서 필요한 경우를 잘 파악하여 bubbles 옵션을 사용한다면 보다 효과적으로 커스텀 이벤트를 다룰 수 있습니다.

이벤트 취소

cancelable은 이벤트가 취소될 수 있는지 여부를 나타내는 속성입니다. 즉, 이벤트가 cancelable: true로 생성된 경우에만, event.preventDefault()를 호출해서 해당 이벤트의 기본 동작을 취소할 수 있습니다.

사실 커스텀이벤트에는 기본동작(기본 이벤트)가 없기 때문에 설정할 필요가 없는 경우가 많고, 상태관리로 조건에 따라 발생시킬 수 있어 취소 여부를 사용하지 않는 경우도 많습니다.

다만 특수하게 이벤트 핸들러가 여러개 있어 특정 조건에서 동작을 막아야하는 경우가 있다면 cancelable 을 통해 제어할 수 있습니다.

이벤트 전파에 비해 이벤트 취소가 무엇인지 사실 조금 이해가 되지 않을 수 있는데요 🤔

이벤트 취소 옵션인 cancelable은 브라우저 기본 이벤트에도 적용됩니다. 브라우저 기본 이벤트를 예시로 이벤트 취소를 살펴보겠습니다.

기본적으로 취소 가능한 이벤트

  • click, submit, keydown 등은 브라우저 기본 동작이 있기 때문에 cancelable: true입니다.

  • cancelable: true이기 때문에 preventDefault 를 통해 해당 이벤트를 취소할 수 있습니다.

    const link = document.querySelector("a")
    
    link.addEventListener("click", event => {
      event.preventDefault() // 링크 이동 취소
    
      console.log("링크 이동이 취소되었습니다.")
    })
    

기본적으로 취소 불가능한 이벤트

  • mousemove, focus 등은 브라우저 동작을 취소할 수 없기 때문에 cancelable: false입니다

  • cancelable: false이기 때문에 preventDefault 를 호출하여도 아무런 동작을 하지 않습니다.

    document.addEventListener("mousemove", event => {
      event.preventDefault() // 아무 효과 없음
    
      console.log("mousemove는 취소할 수 없습니다.")
    })
    

사용자 정의 데이터 전달

detail 속성을 사용해 이벤트에 추가적인 정보를 포함할 수 있습니다

아래 코드에서는 “update”라는 이벤트를 발생시키며 해당 이벤트를 통해 updatedValue 값을 전달하고 있습니다.

const myEvent = new CustomEvent("update", {
  detail: { updatedValue: 42 },
})

element.dispatchEvent(myEvent)

이때 마지막의 dispatchEvent는, 사용자가 정의한 커스텀 이벤트*(코드에서는 update이벤트)*를 실제로 발생시키는 역할을 합니다.

// 이벤트 리스너 등록하여 커스텀이벤트를 받음
document.addEventListener("update", event => {
  console.log("CustomEvent 받음!")
  console.log("전달된 데이터:", event.detail) // { updatedValue: 42 }
})

그리고 커스텀 이벤트를 받을 요소에 이벤트리스너를 통해 전달 받습니다.

진단 및 처방 모듈에서 커스텀 이벤트 발생시키기

아래 코드가 제가 실제 작성한 코드입니다. ‘티켓 이력’이라는 타이틀의 버튼을 클릭하면(onClick) 커스텀 이벤트를 발생시키는 함수를 실행하게 되어 있습니다. 첫번째 인자로 이벤트명, 두번째 인자로 eventInit 객체를 보내는 것을 알 수 있습니다.

저는 이벤트를 발생시킬 때, 티켓이력 스레드를 열고 싶은 것이기 때문에 detail값에 티켓 스레드에 해당하는 메뉴타입을 전달하였습니다.

참고로 customEventService는 커스텀 훅으로, dispatch는 new CustomEvent를 실제로 발생시키는 dispatchEvent 역할을 합니다.

export declare const customEventService: {
  dispatch: <T extends string, V = void>(
    event: T,
    body?: CustomEventInit<V>,
  ) => void
}

스레드 모듈에서 커스텀 이벤트 전달받기

스레드 모듈 내에서는 useCustomEventListener를 통해 발생시킨 ‘[patient_thread] open patient thread’ 커스텀 이벤트를 받고 있는 것을 볼 수 있습니다.

참고로 useCustomEventListener는 addEventListener와 동일한 역할을 하는 "커스텀 훅"입니다. addEventListener를 사용하지 않아도 함수를 실행시킴으로써 해당 커스텀 이벤트를 받아오는 역할을 합니다.

이때 커스텀 이벤트를 받아 onToggleMenu 함수를 실행하는데요, 해당 함수 내에서는 menuType을 인자값으로 받고 있습니다.

handler 내부에서 실행될 때, menuType은 커스텀이벤트에서 전달한 detail 값을 받게되고, 아까 전달한 ‘TICKET_HISTORY’ 문자열을 로컬스토리지의 스레드 키의 값으로 저장하며 티켓 이력 스레드가 열리게 되는 것입니다.

원래는 스레드의 아이콘을 클릭하면 해당 아이콘의 메뉴가 onToggleMenu의 메뉴타입에 지정되어 해당하는 스레드가 열리는데, 커스텀 이벤트로 실행시켜주고 있는 것입니다.