React에서의 이벤트위임 트러블슈팅

이수빈·2023년 11월 21일
0

React

목록 보기
13/20
post-thumbnail

React에서 이벤트 위임 동작방식 정리

이벤트 등록과정

  1. click: 'onClick' 과 같은 형식으로 NativeEvent 이름과 React 이벤트 핸들러 Property를 매핑합니다. 이를 이용해 이벤트가 발생하면 적절한 프로퍼티와 연결할수 있습니다.

  2. React에서는 정의한 이벤트 타입에 따라 부여하는 이벤트의 우선순위가 다른데, 전체 Native Event를 React에서 부여하는 기준에 맞게 우선순위를 설정합니다.

  3. 앞서 언급한 이벤트 위임을 활용하여 Virtual DOM에 이벤트 핸들러들을 등록합니다.

  • 즉 이벤트 핸들러가 Root Node에 등록되는형태임. Virtual Dom이 생성될 때 이벤트 핸들러들이 Root Node에 등록되는 과정이 일어남
  • 이벤트가 트리거되는 곳과 이벤트 핸들러가 실행되는 곳이 다르다는 것임 !!!
  • next.js root Container console 출력결과

이벤트가 트리거 되는 과정

  1. Button을 클릭하면 React에서 ‘click’ 이벤트를 감지하고, 부착되어있는 이벤트 리스너가 트리거됩니다. 이때, 이 이벤트 리스너는 React에서 정의한 dispatchEvent 함수를 호출하게 됩니다.

  2. 호출시 넘어온 이벤트 객체로부터 target DOM node(Button node)를 식별하며, 내부적으로 사용하는 키를 사용하여 이 DOM node가 어떤 Fiber node와 매칭되는지를 확인합니다

  3. 해당 Fiber node를 찾고 나면, 해당 node로부터 출발해서 root node에 이르기까지 Fiber Tree를 순회합니다. 이때 매칭되는 이벤트 Property(‘onClick’)와 매칭되는 이벤트를 가지는 Fiber Node를 발견할때 마다 이 이벤트 리스너(콘솔을 찍는 함수)들을 dispatchQueue 라고 불리는 Array에 저장합니다.

  4. root node에 도착하고 나면, dispatchQueue라는 array에서 리스너를 꺼내어 실행합니다. queue이기 때문에 먼저 들어간 요소가 가장 먼저 실행되며 propagation 여부를 통과하지 못하는경우 나머지도 실행되지 않습니다.

  • 이벤트가 등록되면 먼저 호출한 target Node를 찾는다. (Fiber Node)
  • target Node를 찾았다면 해당 Node부터 이벤트가 버블링되면서 queue에 호출된 이벤트 리스너를 넣고, Root에서 실행함.
  • e.stopPropagation() 메소드를 통해 전파를 막을 수도 있음. 하지만 이벤트 자체의 전파를 막는게 아니라, Root Node에서 propagation 여부를 확인해 그다음 queue에 실행을 막는것임!!

직접 Dom에 이벤트를 등록하는 경우?

  • 아래와 같은 코드가 있다고 가정할 때,

이벤트 동작 순서는 부모요소 클릭 => 자식요소 클릭임

  • 부모요소의 이벤트 리스너가 먼저 호출되는 이유는, 직접 Dom Level에 이벤트를 등록했기 때문에 이벤트가 버블링 되는 과정에서 Dom에 부착한 이벤트 리스너가 트리거 되게 되는것임.

  • propagation을 통해 이벤트 전파를 막았기 때문에, Root Node에서 자식요소의 이벤트 리스너를 실행하고 중간요소는 실행되지 않는 것.

function App() {
  const ref = useRef(null)
  useEffect(() => {
    ref.current.addEventListener("click", () => console.log("부모 요소 클릭됨"))
  }, [])

  return (
    <div className="parents" ref={ref}>
      <div
        className="middle"
        onClick={e => {
          console.log("중간 요소 클릭됨")
        }}
      >
        <div
          onClick={e => {
            console.log("자식 요소 클릭됨")
            e.stopPropagation()
          }}
          className="children"
        ></div>
      </div>
    </div>
  )
}

export default App

useOnClickOutSide Hook

ref) https://usehooks-ts.com/react-hook/use-on-click-outside

  • 보통 모달창 이외의 부분을 클릭했을 때 모달창을 닫으려고 많이 사용하는 hook인 useOnClickOutSide가 있다.
import { RefObject } from 'react'

import { useEventListener } from 'usehooks-ts'

type Handler = (event: MouseEvent) => void

export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
  ref: RefObject<T>,
  handler: Handler,
  mouseEvent: 'mousedown' | 'mouseup' = 'mousedown',
): void {
  useEventListener(mouseEvent, event => {
    const el = ref?.current

    // Do nothing if clicking ref's element or descendent elements
    if (!el || el.contains(event.target as Node)) {
      return
    }

    handler(event)
  })
}
import { useRef } from 'react'

import { useOnClickOutside } from 'usehooks-ts'

export default function Component() {
  const ref = useRef(null)

  const handleClickOutside = () => {
    // Your custom logic here
    console.log('clicked outside')
  }

  const handleClickInside = () => {
    // Your custom logic here
    console.log('clicked inside')
  }

  useOnClickOutside(ref, handleClickOutside)

  return (
    <button
      ref={ref}
      onClick={handleClickInside}
      style={{ width: 200, height: 200, background: 'cyan' }}
    />
  )
}
  • 여기서 hook은 ref element나 자식을 클릭할때는 return하고, 아닐경우 useEventListener
    라는 hook을 통해 이벤트 핸들러를 실행한다.

  • useEventListner 코드는 아래와 같은데, hook은 Root Node에 이벤트핸들러를 부착하는 방식이 아니라 addEventListner를 통해 직접 실행한다는 것이 중요하다.

function useEventListener<
  KW extends keyof WindowEventMap,
  KH extends keyof HTMLElementEventMap,
  KM extends keyof MediaQueryListEventMap,
  T extends HTMLElement | MediaQueryList | void = void,
>(
  eventName: KW | KH | KM,
  handler: (
    event:
      | WindowEventMap[KW]
      | HTMLElementEventMap[KH]
      | MediaQueryListEventMap[KM]
      | Event,
  ) => void,
  element?: RefObject<T>,
  options?: boolean | AddEventListenerOptions,
) {
  // Create a ref that stores handler
  const savedHandler = useRef(handler)

  useIsomorphicLayoutEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    // Define the listening target
    const targetElement: T | Window = element?.current ?? window

    if (!(targetElement && targetElement.addEventListener)) return

    // Create event listener that calls handler function stored in ref
    const listener: typeof handler = event => savedHandler.current(event)

    targetElement.addEventListener(eventName, listener, options)

    // Remove event listener on cleanup
    return () => {
      targetElement.removeEventListener(eventName, listener, options)
    }
  }, [eventName, element, options])
}

export { useEventListener }

모달창에서 발생한 문제

  • React Datepicker와 MUI의 모달을 같이 사용하는 도중, MUI에서 click이벤트가 발생하지 않은 문제가 있었다.

  • 보통은 click 이벤트가 발생하지 않는 상황에서 가장 먼저 확인하는 것은 요소들의 z-index여부이다. 실제로는 보이지 않지만, z-index가 더 높은 요소가 있어서 클릭자체가 되지 않는 상황이 있을 수 있다.

  • 하지만, z-index가 주어진 요소에 자식요소라면 정상적으로 클릭이 되어야 한다.

  • React DatePicker는 내부적으로 react-onclickoutside 라는 라이브러리를 사용해, DatePicker 외부가 클릭되었을 때 달력이 닫히는 로직을 갖고 있다.

  • 이때 DatePicker안에 모달형식이 들어있을 때, click 이벤트보다 onMouseDown 이벤트가 먼저 발생해 컴포넌트가 언마운트되어서 click이벤트 자체가 발생하지 않은 상황이 있었다.

  • Mouse Event에서 Click은 가장 마지막에 발생한다. 즉 => MouseDown => MouseUp => Click의 순서로 이벤트가 발생하는데, 다른 작용으로 인해 이벤트 핸들러를 갖고있던 컴포넌트가 unmount된다면 click 이벤트가 제대로 발생하지 않을 수 있다.

  • e.stopPropagation() 또한 작동하지 않는데, 그 이유는 react-onclickoutside 자체가 직접 Node Level에 이벤트 핸들러를 등록하기 때문이였다.

  • click 이벤트가 아닌 onMouseDown으로 이벤트를 변경하므로써 문제를 해결 할 수 있었다.

profile
응애 나 애기 개발자

0개의 댓글