얼마 전 리액트 프로젝트의 성능을 위해 memo를 적용하여 컴포넌트의 재사용성을 높이는 작업을 진행하고 있었습니다.
이 과정에서 memo를 적용하였음에도 일부 컴포넌트에서 계속 리렌더링이 발생했고 영향을 줄 수 있는 요소들을 꼼꼼히 확인하였음에도 원인을 찾지 못하고 있었습니다.
하지만, 해당 컴포넌트들의 공통점을 찾아보니 컴포넌트 내부에서 react-router의 훅 중 하나인 useNavigate를 사용하고 있었고 일시적으로 navigate를 사용하지 않도록 처리하자 정상적으로 작동하는걸 확인했습니다.
이에 해당 훅과 관련된 내용을 구글링 해보았고 저와 비슷한 현상을 겪은 사람의 글을 찾을 수 있었습니다.
원인은 react-router가 V6로 업그레이드 되면서 URL이 변경 될 경우 useNavigate함수를 계속 재생성하도록 변경 되었다는것을 알게 되었습니다.
사실, 이 부분은 일반적인 상황에서는 크게 문제가 될게 없지만 제 프로젝트의 경우 클릭할 때마다 아이템의 id값이 URL의 query에 붙는 형태이기 때문에 이로 인한 불필요한 리렌더링이 계속 발생하는것이였습니다.
이를 해결하기 위해서는 값이 변경 되더라도 리렌더링을 유발하지 않는 ref 객체를 활용해 해결할 수 있습니다.
즉, navigate함수를 useRef의 값으로 넣고 각 컴포넌트에서는 Context API를 통해 navigate함수에 접근하는것입니다.
// navigate 함수를 제공해줄 Context
const StableNavigateContext = createContext(null);
// navigate 함수를 ref의 값으로 집어넣어 Context API를 활용해 제공
const StableNavigateContextProvider = ({ children }) => {
const navigate = useNavigate();
const navigateRef = useRef(navigate);
return (
<StableNavigateContext.Provider value={navigateRef}>
{children}
</StableNavigateContext.Provider>
);
};
// 컴포넌트 내부에서 사용할 훅
const useStableNavigate = () => {
const navigateRef = useContext(StableNavigateContext);
if (navigateRef?.current === null) {
throw new Error("StableNavigate context is not initialized");
}
return navigateRef.current;
};
이렇게 하면 URL변경으로 인해 useNavigate훅이 재생성 되더라도 리렌더링이 발생하지 않으므로 의도했던대로 동작할 수 있게 됩니다.
따라서, 이러한 로직을 별도의 StableNavigate
라는 커스텀 훅으로 분리하여 해결할 수 있습니다.
일반 Navigate와 StableNavigate를 사용 할 때의 차이는 아래의 제 코드샌드박스로 예시를 만들어 두었으니 참고하시길 바랍니다.
navigation_re-render_test | Codesandbox