개발여행
리액트의 memo와 useCallback 사용법 본문
리액트 앱에서 setInterval처럼 자동으로 반복 실행되는 함수를 사용할 땐 리렌더링으로 인한 사이드이펙트를 걱정할 수밖에 없다. 내 앱은 1초마다 시간을 세는 타이머 앱인데, 부모 컴포넌트의 상태가 변하면 부모 컴포넌트는 물론 자식 컴포넌트까지 리렌더링되는 리액트의 특성 때문에 타이머와 상관없는 상태 값을 바꿔도 타이머가 리셋되는 현상이 있었다.
// 부모 컴포넌트 - TimerContainer
const TimerContainer = () => {
const [isRunning, setIsRunning] = useState(false); // 부모 컴포넌트의 상태
const intervalId = useRef<null | number>(null);
const startTimer = () => {
setIsRunning(true);
};
const setIntervalId = (id: number) => {
intervalId.current = id;
}
return (
{isRunning && <RunningTimer setIntervalId={setIntervalId} />}
);
};
// 자식 컴포넌트 - RunningTimer
// isRunning 값이 바뀔 때마다 setIntervalId 함수의 재정의로 인해 리렌더링됨
const RunningTimer = ({ setIntervalId }: RunningTimerProps) => {
// ...
};
자식 컴포넌트가 리렌더링되는 문제는 한참을 헤맨 후에야 리액트의 memo와 useCallback 훅을 써서 고칠 수 있었다.
컴포넌트의 memoization
컴포넌트를 리액트의 memo 함수 안에 넣어서 memoize를 하면, 그 컴포넌트의 prop이 바뀌지 않는 이상 부모 컴포넌트와 함께 리렌더링되지 않는다.
자식 컴포넌트를 memo로 감싸주기만 하면 되는데, 내 앱의 경우 부모 컴포넌트로부터 전달받는 prop이 있기 때문에 memo만으로는 부족했다. 부모 컴포넌트 안에 정의한 함수를 자식 컴포넌트에게 넘겨주는데, 리렌더링될 때마다 그 함수가 새로 정의돼서 prop이 바뀌는 것과 동일한 효과를 주기 때문이었다. 해당 함수에서 부모 컴포넌트의 상태 값을 조작하기 때문에 함수를 부모 컴포넌트 밖으로 뺄 수도 없었다.
함수가 재정의돼서 자식 컴포넌트를 리렌더링하는 문제는 useCallback으로 해결했다.
함수의 캐싱
useCallback 훅은 함수가 매 리렌더링 사이클마다 새로 정의되지 않도록 함수를 캐싱해준다. 함수가 의존하고 있는 값이 바뀌지 않는 이상 마지막으로 캐싱됐던 함수를 그대로 사용하고, 만약 dependency 값이 바뀌면 바뀐 값을 사용하도록 함수가 갱신된다.
따라서 자식 컴포넌트의 prop으로 전달하는 모든 함수를 useCallback 훅으로 캐싱해두었더니, 부모 컴포넌트만 리렌더링되고 자식 컴포넌트는 영향을 받지 않았다.
const TimerContainer = () => {
const [isRunning, setIsRunning] = useState(false);
const intervalId = useRef<null | number>(null);
const startTimer = () => {
setIsRunning(true);
};
// useCallback으로 캐싱한 함수
const setIntervalId = useCallback(
(id: number) => {
intervalId.current = id;
},
[intervalId]
);
return (
{isRunning && <RunningTimer setIntervalId={setIntervalId} />}
);
};
// memoize한 자식 컴포넌트
const RunningTimer = memo(function RunningTimer({ setIntervalId }: RunningTimerProps) {
// ...
});
'Frontend' 카테고리의 다른 글
Next.js App Router에서 라우팅 처리하기 (0) | 2024.04.21 |
---|---|
SSR과 RSC의 차이 (0) | 2024.04.18 |
Material UI 시작하기 (0) | 2024.04.11 |
STOMP.js로 프론트엔드에서 웹소켓 통신 구현 (0) | 2024.04.05 |
리액트에서 사용하는 intersection observer (0) | 2023.12.12 |