본문 바로가기

[React] useEvent is not dead!

by mugglim 2022. 11. 6.

Github에서 관련 코드를 확인할 수 있으며, 배포 URL은 아래 링크와 같습니다.


프롤로그

useEvent

useEvent는 함수를 메모제이션 하기 위한 훅입니다. useEvent로 생성된 함수는 재생성되지 않고, 항상 동일한 참조값을 가집니다. useCallback과 유사하게, 함수를 메모제이션 하는 도구라고 생각하시면 좋습니다.

⚠︎ 주의!, useEvent 는 아직 정식 hook은 아닙니다. React RFC에 제안되고 있는 훅 중 하나입니다.

useEvent 코드 살펴보기

// ref : <https://github.com/Volune/use-event-callback/blob/master/src/index.ts>

import { useLayoutEffect, useMemo, useRef } from 'react';

type Fn<ARGS extends any[], R> = (...args: ARGS) => R;

const useEvent = <A extends any[], R>(fn: Fn<A, R>): Fn<A, R> => {
  let ref = useRef<Fn<A, R>>(() => {
    throw new Error("Can't called");
  });

  useLayoutEffect(() => {
    ref.current = fn;
  });

  return useMemo(
    () =>
      (...args: A): R => {
        const { current } = ref;
        return current(...args);
      },
    [],
  );
};

export default useEvent;

1. ref를 통해 콜백 함수를 저장

let ref = useRef<Fn<A, R>>(() => {
  throw new Error("Can't called");
});

useLayoutEffect(() => {
  ref.current = fn;
});

2. useMemo을 통한 메모제이션

  • useMemo를 통해 함수를 메모제이션합니다.
  • useMemo의 디펜던시 목록을 비워두어 재생성을 방지합니다.
  • 인자 값을 통해 동적으로 받을 수 있습니다.
return useMemo(
    () =>
      (...args: A): R => {
        const { current } = ref;
        return current(...args);
      },
    [],
  );

간단한 사용 예

const Chat = () => {
    const [text, setText] = useState();

    // without args
    const onMessageSubmit = useEvent(() => {
      submit(text);
  })

    // with args
    const onMessageReceived = useEvent((message) => {
        console.log(message);
    })

    useEffect(() => {
        socket.on('message', onMessageReceived);
    }, [])

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={onMessageSubmit} />
    </>
  );
}

useEvent를 언제 사용해야 할까 ??

일반적으로 함수는 렌더링 마다 참조값이 변경됩니다. 만약 생성된 함수가 자식 컴포넌트의 prop으로 전달되고, 자식 컴포넌트의 렌더링 비용이 비싸다고 가정해봅시다. 이런 상황에서는 useCallback 또는 React.memo과 같은 도구를 통해 함수를 메모제이션하고 자식 컴포넌트의 리렌더링을 방지해야 합니다.

React.memo을 사용하면 바로 해결 되지 않나요?

해결이 불가합니다.

React.memo는 prop이 변경되지 않으면 렌더링을 하지 않도록 도와주는 도구입니다. 함수의 타입은 주소 타입(reference type)입니다. 만약 부모 컴포넌트에서 전달되는 함수의 주소값이 변경되었다면, React.memo로는 자식 컴포넌트는 리렌더링됩니다.

useCallback을 사용하면 해결 되지 않나요?

해결이 불가합니다.

useCallback은 의존성 배열의 값들이 변경되지 않았다면 함수를 메모제이션합니다. useCallback에 전달되는 함수가 별도로 인자값이 필요하지 않는다면 함수를 메모제이션 할 수 있습니다. 그러나, 부모 컴포넌트의 state가 바뀔때 마다 useCallback의 의존성 배열의 값이 바뀐다면 해결이 불가합니다.

추가로, useCallback의 디펜던시들가 변경에 안전한지 추적하기는 상당히 어렵다고 생각합니다. (DX 저하)

const withUseCallback = useCallback(() => {
    // ...
}, [
    dep1,
    dep2,
    dep3,
    dep4,
    dep5,
    // ...  🤯🤯🤯🤯
])

const withUseEvent = useEvent(() => {
    // ... 😀😀😀😀
})

 

Q1) text가 변경될 때 onButtonClick은 재생성 될까요?

const [text, setText] = useState('');
const onButtonClick = () => {};

 

Q2) text가 변경될 때 ExpensiveComponent은 리렌더링 될까요?

const [text, setText] = useState('');
const onButtonClick = () => {};

return (<>
    <ExpensiveComponent onClick={onButtonClick} />
</>
);

// ExpensiveComponent.tsx
const ExpensiveComponent = React.memo(({ onButtonClick }) => {
  // ...
});

 

Q3) text가 변경될 때 ExpensiveComponent은 리렌더링 될까요?

const [text, setText] = useState('');
const onButtonClick = useCallback(() => {}, [text]);

return (<>
    <ExpensiveComponent onClick={onButtonClick} />
</>
);

// ExpensiveComponent.tsx
const ExpensiveComponent = React.memo(({ onButtonClick }) => {
  // ...
});

 

Q4) text가 변경될 때 ExpensiveComponent은 리렌더링 될까요?

const [text, setText] = useState('');
const onButtonClick = useEvent(() => {});

return (<>
    <ExpensiveComponent onClick={onButtonClick} />
</>
);

// ExpensiveComponent.tsx
const ExpensiveComponent = React.memo(({ onButtonClick }) => {
  // ...
});

 

 

그럼 좋은것 같은데, 왜 개발이 중단됐나요?

완전히 중단되지 않았습니다! 훅의 이름, 사용 방법, 활용 가능한 유즈 케이스를 정리해서 다시 개발 예정입니다.
(관련 링크 : https://github.com/reactjs/rfcs/pull/220#issuecomment-1261018254)

요약

  • useEvent는 함수를 메모제이션 하는 훅이다.
  • useEvent의 개발을 중단됐고, 동일한 목적을 가진 새로운 이름의 API가 개발 될 예정

레퍼런스

댓글