[번역] useEvent 리액트 훅이란 (또 무엇이 아닌지)

eunbinn·2022년 5월 20일
12

FE 번역

목록 보기
9/10
post-thumbnail

원문: https://typeofnan.dev/what-the-useevent-react-hook-is-and-isnt/

지난주 리액트 코어팀은 새로운 리액트 훅인 useEvent에 대해 RFC([역주] RFC: Request For Comment, 개발에 있어 필요한 기술, 연구 결과, 절차 등을 기술해놓은 메모)를 발표했습니다. 이 훅이 무엇인지, 또 무엇이 아닌지, 그리고 훅에 대한 제 생각을 전달드리고자 합니다.

이 내용은 RFC이기 때문에 출시된 것이 아닙니다. 따라서 아직 사용할 수 없으며 세부 동작이 변경될 수 있다는 점 유의 부탁드립니다.

진짜 문제를 해결하려는 노력

useEvent가 해결하고자 하는 문제가 있습니다. useEvent가 무엇인지 알아보기 전에, 해결하고자 했던 문제가 무엇인지 확인해 봅시다.

리액트의 실행 모델은 대부분 현재 값과 이전 값을 비교하여 동작합니다. 이는 컴포넌트 내부와 useEffect, useMemo, useCallback과 같은 훅에서 발생합니다.

아래와 같은 컴포넌트가 있다고 가정해 보겠습니다.

function MyApp() {
  const [count, setCount] = useState(0);

  return <Counter count={count} />;
}

Counter 컴포넌트는 count 변수가 변경되면 리렌더됩니다. count가 변경될 때 어떠한 effect([역주] useEffect 훅에 넘긴 함수를 effect라고 부릅니다)가 실행되기를 원한다고 가정해 봅시다. 아래처럼 useEffect 훅을 사용할 수 있을 것입니다.

function MyApp() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(count);
  }, [count]);

  return <Counter count={count} />;
}

countuseEffect훅의 의존성 배열에 추가했기 때문에 effect는 count가 변경될 때마다 재실행될 것입니다.

그래서 무엇이 문제인가요?

이 모델을 사용하면 많은 리액트 개발자들은 컴포넌트가 너무 많이 리렌더되거나 훅이 너무 많이 재실행(때로는 무한히!)된다는 동일한 문제에 직면하게 됩니다.

RFC에 훌륭한 예시가 있기 때문에 이를 그대로 사용하도록 하겠습니다! 먼저 컴포넌트에 text라는 상태가 있고 SendMessage를 위한 또 다른 버튼 컴포넌트가 있는 채팅앱을 가정해보겠습니다.

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

  const onClick = () => {
    sendMessage(text);
  };

  return <SendButton onClick={onClick} />;
}

문제는 text가 변경될 때마다, onClick 함수가 재생성된다는 것입니다. SendButton에 전달된 이전의 onClick 함수와 참조가 동일하지 않으므로 모든 키 입력마다 SendButton을 쉽게 리렌더링할 수 있습니다.

useEffect를 너무 자주 실행하는 예시를 살펴보겠습니다. 이 예시는 리액트 코어팀 소속의 Dan Abramov의 트윗에서 가져온 예시입니다. 여기에는 route.url이 바뀔 때마다 페이지 방문을 로깅하는 effect가 있습니다.

function Page({ route, currentUser }) {
  useEffect(() => {
    logAnalytics("visit_page", route.url, currentUser.name);
  }, [route.url, currentUser.name]);
}

사용자의 이름이 업데이트될 때에도 페이지 방문을 기록하는데, 이는 원하지 않는 동작 입니다. currentUser.name을 의존성 배열에서 제거할 수도 있습니다. 하지만 이는 리액트에서 권장되지 않습니다. 만약 의존성 배열이 effect 함수 내부의 모든 의존을 반영하지 않는다면 오래된 클로저(stale closures)와 추적하기 어려운 버그가 발생합니다. 이는 리액트 코어팀이 강력하게 권장하는 react-hooks ESLint 플러그인에 "exhaustive deps" 룰이 있을 정도로 중요합니다.

제안된 해결책: useEvent

서문이 길었습니다. 여기까지 같이 따라오셨다면 좋습니다! 드디어 useEvent에 대해 이야기하고자 합니다. 이 새로운 훅은 의존에 따라 새로운 함수를 만들지 않고도 함수에 대한 안정적인 참조를 보장하기 위해 작성되었습니다.

백문이 불여일견입니다. 앞서 사용했던 예시인 너무 많이 리렌더링 되는 SendButton을 가진 채팅 앱으로 돌아가 봅시다. 만약 useEvent가 있다면 클릭 핸들러를 감싸, text가 변경되더라도 참조가 변경되지 않는 함수로 만들 수 있을 것입니다.

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

  const onClick = useEvent(() => {
    sendMessage(text);
  });

  return <SendButton onClick={onClick} />;
}

이제 onClick은 렌더링 될 때마다 새로 생성되는 대신 항상 같은 함수를 참조할 것입니다. 따라서 SendButton이 계속 리렌더링 되는 일이 없을 것입니다.

다음 예시였던 페이지 방문 로거도 살펴봅시다.

function Page({ route, currentUser }) {
  const logVisit = useEvent((pageUrl) => {
    logAnalytics("visit_page", pageUrl, currentUser.name);
  });

  useEffect(() => {
    logVisit(route.url);
  }, [route.url]);
}

이제 안정적인 logVisit함수를 만들었습니다. currentUser.nameuseEffect 함수의 본문에서 제거하고 route.url이 변경될 때에만 effect를 실행시킬 수 있습니다.

첫 인상

useEvent훅에 대한 저의 초기 반응은 "허, useEvent라니 대체 무슨 의미야?"였습니다. 다른 많은 사람들처럼 저 또한 훅의 이름에 동의할 수 없습니다. 하지만 그건 차치하고, 이 훅은 지난 몇 년 동안 effect의 의존성과 씨름하며 겪었던 고생을 많이 덜어줄 것입니다. 전반적으로 리액트 생태계에 큰 도움이 될 것이라고 생각합니다.

이 훅이 해결하지 않는 것

useEvent 훅은 만병통치약이 아닙니다. useEvent는 리액트에 또 다른 개념을 추가합니다. 또한 리액트가 진정한 반응성이 아니라는 사실도 변함없습니다. 진정한 반응성 프레임워크인 SolidJS 예시로 얼마나 간단히 로거를 만들 수 있는지 빠르게 살펴보겠습니다.

function Page(props) {
  createEffect(() => {
    logAnalytics(
      "visit_page",
      props.route.url,
      untrack(() => props.currentUser.name)
    );
  });
}

참고사항: 저는 SolidJS docs 팀에 속해있기 때문에 SolidJs를 선호합니다. 여전히 저는 SolidJs가 더 간단하다고 생각합니다. effect의 의존성인 props.route.urlprops.currentUser.name자동으로 추적됩니다. "추적 해제" 하기 위해서 Solid는 untrack 함수를 제공합니다. 위 코드는 props.route.url이 변경될 때에만 실행되나 사용자 이름에 대해 클로저를 갖고 있습니다.

0개의 댓글