리액트팀은 정식적인 기능으로 도입하기전에 "RFC"라는 곳에서 해당 기능에 대해서 논의합니다.
제가 지금 살펴보려는 훅은 useEvent 훅입니다.
https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
rfcs/text/0000-useevent.md at useevent · reactjs/rfcs
RFCs for changes to React. Contribute to reactjs/rfcs development by creating an account on GitHub.
github.com
위의 문서에 자세히 작성되어있지만 핵심은 아래와같습니다.
문제점 (불편함)
예를들어 아래 코드를 보시면,
import { memo, useState } from "react";
export function Handler() {
const [count, setCount] = useState(0);
const handleClick = () => {
alert(`현재 count: ${count}`);
}
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>증가</button>
<HandlerChild onClick={hadleClicByUseEvent} />
</div>
);
}
const HandlerChild = memo(function Child({ onClick }: { onClick: () => void }) {
console.log("Child 렌더링 발생");
return <button onClick={onClick}>자식 버튼</button>;
});
문제점이 바로 보이실것입니다.
자식 컴포넌트를 메모제이션했지만, props로 전달하는 onClick 함수가 상위 컴포넌트가 리렌더링될때마다(setCount를 활용한 버튼 클릭시) 새로운 참조값을 생성하여 결과적으로 메모제이션이 의미가 없어지고있습니다.
이런 경우 대부분의 해결책은 props로 내려가는 훅을 useCallback으로 감싸서 참조값을 유지시키는 방법입니다.
import { memo, useCallback, useState } from "react";
export function Handler() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
alert(`현재 count: ${count}`);
}, [count]);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>증가</button>
<HandlerChild onClick={hadleClicByUseEvent} />
</div>
);
}
const HandlerChild = memo(function Child({ onClick }: { onClick: () => void }) {
console.log("Child 렌더링 발생");
return <button onClick={onClick}>자식 버튼</button>;
});
이러면 문제가 해결될까요?
문제는 지속됩니다.
여전히 상위 컴포넌트가 리렌더링되면(카운트를 업데이트하면) 자식 컴포넌트가 리렌더링됩니다.
이유는 당연하게도 useCallback의 의존성에 count가 설정되어있어서입니다.
함수자체는 메모제이션되었지만, useCallback의 특성으로 의존성이 변경되면 다시 생성됩니다.
이러면 문제가 지속됩니다.
해결 제안
useEvent 여기서 근본적인 문제를 해결합니다.
바로 의존성을 삭제합니다. 그렇게되면 count와는 상관없이 함수의 참조값이 유지될것입니다.
하지만 바로 다른 문제가 발생합니다. -> count가 오래된 상태일수있습니다.
useCallback등에서 의존성 배열을 제공하는 이유는 상태가 변경되면 그 상태의 최신값을 반영해야하기때문입니다.
하지만 의존성 배열을 없에면 최신값이 아닐수있게됩니다.
이 문제를 해결하기 위해서는 아래와 같은 구현이 필요할것입니다.
함수의 참조값은 유지되는 동시에 상태도 최신 상태여야함
그러면서 아래와 같이 구현할수있다고 예시를 보여줍니다 (간단한 버전)
function useEvent(handler) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
ref는 문서에 나와있듯이 리렌더링을 유발하지 않고 상태를 저장하는 방법중 하나입니다.
해당 ref와 useLayoutEffect(또는 useEffect)를 이용하여 리렌더링마다(의존성 x) 최신 헨들러를 ref에 저장합니다.
이제 useCallback으로 감싸서 해당 최신 헨들러를 메모제이션합니다
여기서 의존성 배열이 빈배열이므로 딱 한번 저장됩니다.
이러면 결과적으로 최신 상태는 가지고있는, 참조값을 유지하는 함수가 됩니다.
해결필요
하지만 아직 아래와 같은 부분이 해결되었지 않다고합니다.
- 새로운 개념 추가
- 어쨋든 새로운 훅이 생기는것이고 useCallback과 비슷하지만 다르므로 이 둘의 차이점에 대해서 또 학습해야합니다.
- 의도치 않게 useEvent를 남용하면 의존성 누락 버그
- 상태가 변경되었을때 무조건 리렌더링이 되어야하는 상태들이 존재합니다 (렌더링에 관여하는 state등,,) 이러한 상태를 의존성에 포함시키지 않고 무조건 useEvent로 감싸면 기대하던 효과랑 다른 결과가 나올것입니다.
- 호출 타이밍 차이
- 위의 간단한 예시에서는 useLayoutEffect로 처리하지만 실제로 사용되려면 이 시점에 대한 깊은 논의가 필요합니다.
하지만 저는 이 훅이 언젠가는 도입될것이라고 생각하고 한번 직접 구현해보려합니다.
'React' 카테고리의 다른 글
[React] React + SSR과 서버 컴포넌트는 뭐가 다를까? (2) | 2024.12.27 |
---|---|
[React] React Query 알아보기 03 - React Query with SSR (1) | 2024.11.24 |
[React] React Query 알아보기 02 - React Query with Suspense (0) | 2024.11.24 |
[React] useSyncExternalStore 알아보기 (0) | 2024.11.09 |
[React] 헤드리스 컴포넌트란 무엇일까 (1) | 2024.11.07 |