왜 이 내용을 정리하나
부모 컴포넌트의 상태가 바뀌면 그 안의 함수는 매번 새로 만들어진다. 자식 컴포넌트에 이 함수를 props로 넘기면, 자식은 props가 바뀌었다고 판단해 불필요하게 다시 렌더링된다. 자식 컴포넌트를 React.memo로 감싸 두었는데도 렌더링이 줄지 않는다면 이 문제를 의심해야 한다. 함수를 useCallback으로 감싸면 의존성이 바뀌지 않는 한 같은 참조가 유지되는데, 실제로 그런지 눈으로 확인하는 과정이 필요하다. 이 노트에서는 console.log로 자식의 렌더링 횟수를 세어 두 방식의 차이를 정리해 본다.
개념 설명
React.memo는 props가 이전과 얼가하게 같으면 컴포넌트 렌더링을 건너뛰는 고차 컴포넌트다. 비교는 얕은 비교(shallow compare)로 이루어진다. 함수나 객체는 참조로 비교되므로, 부모가 렌더링될 때마다 새로 만든 함수는 내용이 같아도 다른 값으로 판정된다.
useCallback(fn, deps)은 deps 배열의 값이 바뀌지 않으면 이전에 만든 함수 참조를 그대로 돌려준다. 이 노트의 예제에서는 deps를 빈 배열로 두었으므로, 부모가 여러 번 렌더링되어도 memoizedHandler는 처음 만든 함수 하나만 계속 쓰인다.
따라서 React.memo 자식에 useCallback으로 감싼 함수를 넘기면 자식은 props가 바뀌지 않았다고 판단해 렌더링을 건너뛴다. 반면 일반 함수를 넘기면 부모 렌더링마다 새 참조가 생기므로 자식도 매번 다시 렌더링된다.
코드 예제
저장소 경로: https://github.com/JooSeunghyeon/React-Atelier/tree/main/packages/usecallback-memo-child-rerender
아래 세 파일로 구성된 예제다. 먼저 부모 컴포넌트다.
import { useState, useCallback } from 'react';
import { Child } from './Child';
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const memoizedHandler = useCallback(() => {
console.log('memoized handler called');
}, []);
const plainHandler = () => {
console.log('plain handler called');
};
return (
count: {count}
setText(e.target.value)} />useCallback 적용
일반 함수 전달
);
}
위 코드는 memoizedHandler와 plainHandler를 같은 자식에 각각 넘긴다. 부모 카운터 버튼을 누르거나 입력창에 글자를 입력하면 부모가 다시 렌더링되고, 이때 두 함수가 새로 만들어지는지가 비교 지점이다.
다음은 자식 컴포넌트다.
import { memo } from 'react';
export const Child = memo(function Child({ onClick, label }) {
console.log(`[Child:${label}] render`);
return 자식 버튼 ({label});
});
자식은 렌더링될 때마다 [Child:memo] render 또는 [Child:plain] render 로그를 찍는다. 부모 카운터를 한 번 누르면 plain 쪽 로그는 한 번 더 찍히지만 memo 쪽 로그는 찍히지 않는다. 이 동작은 브라우저 콘솔에서 직접 확인한다.
위 예제에서 memoizedHandler가 찍는 로그와 plainHandler가 찍는 로그는 클릭했을 때만 출력되므로, 렌더링 여부는 자식의 render 로그로 판단한다.
헷갈리는 점
첫째, useCallback만으로는 자식 렌더링이 줄지 않는다. 자식이 React.memo로 감싸져 있어야 props 참조 비교 결과가 렌더링 건너뛰기로 이어진다. 자식이 메모이제이션되어 있지 않다면 useCallback을 써도 자식은 부모와 함께 매번 렌더링된다.
둘째, deps 배열을 잘못 채우면 오래된 값을 참조하게 된다. 이 예제는 deps를 빈 배열로 두었으므로 함수 안에서 바깥의 count나 text를 읽으면 처음 값이 고정된다. 함수 안에서 상태를 쓴다면 deps에 해당 값을 넣어야 한다.
셋째, 렌더링 횟수를 세는 로그는 개발 모드에서 StrictMode가 켜져 있으면 한 번의 렌더링에 두 번 찍힐 수 있다. 횟수를 비교할 때는 StrictMode 설정을 함께 확인해야 정확하다.
정리
부모가 렌더링될 때 새로 만들어지는 함수는 참조가 매번 달라서 React.memo 자식의 얕은 비교를 통과하지 못한다. useCallback으로 감싸면 deps가 바뀌지 않는 한 같은 참조가 유지되어 자식의 렌더링을 건너뛸 수 있다. 이 노트에서는 console.log의 render 로그 횟수를 비교해 memo 쪽은 렌더링이 줄고 plain 쪽은 그대로 늘어나는 차이를 확인하는 과정을 정리해 본다.
#React #useCallback #React.memo #리렌더링 #메모이제이션
'React' 카테고리의 다른 글
| useMemo 의존성 배열 값이 같으면 계산 함수가 실제로 덜 실행되는지 console.count로 확인하기 (0) | 2026.10.25 |
|---|---|
| React JSX 중괄호 안의 산술 표현식 계산 결과 확인하기 (0) | 2026.10.24 |
| React useEffect 빈 의존성 배열([]) 마운트 시 한 번 실행 확인하기 (0) | 2026.10.23 |
| React useState로 카운터 상태 관리하기 - 가장 짧은 함수형 컴포넌트 (0) | 2026.10.23 |
| React 고급 개념 정리: Escape Hatches, Refs,Effects (0) | 2025.03.16 |