| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- Array
- javascript
- IntelliJ
- 스프링부트
- math
- 자바스크립트
- junit
- input
- ArrayList
- Java
- Visual Studio Code
- 정규식
- 문자열
- 자바문법
- CSS
- vscode
- js
- 단위테스트
- list
- 인텔리제이
- HashMap
- java테스트
- html
- Eclipse
- SpringBoot
- 자바
- 배열
- 테스트자동화
- string
- junit5
- Today
- Total
어제 오늘 내일
[ React ] useCallback 사용법: 함수를 메모이제이션해 리렌더링 최적화하기 본문
이전 글에서 "useCallback은 useMemo로 함수를 감싼 것과 같다"고 잠깐 언급했었죠. 이번 글에서는 그 이야기를 이어서 useCallback을 자세히 다뤄보겠습니다. useCallback은 함수를 메모이제이션하는 Hook으로, React.memo와 함께 쓰일 때 진가를 발휘합니다. 이 글에서는 useCallback이 왜 필요한지, 언제 써야 하는지 예제와 함께 알아보겠습니다.
문제의 시작: 함수는 매번 새로 만들어진다
먼저 자바스크립트의 기본 성질부터 짚고 가야 합니다. 함수도 객체이기 때문에, 컴포넌트가 렌더링될 때마다 함수는 매번 새로 생성됩니다.
function Parent() {
// 이 handleClick 함수는 Parent가 렌더링될 때마다 새로 만들어짐
// 내용은 똑같지만, 자바스크립트 입장에서는 매번 "다른 함수"
const handleClick = () => {
console.log('클릭!');
};
return <Child onClick={handleClick} />;
}
내용이 완전히 같아도, 자바스크립트에서 함수는 새로 만들면 서로 다른 참조로 취급됩니다.
const fn1 = () => console.log('hi');
const fn2 = () => console.log('hi');
console.log(fn1 === fn2); // false! 내용은 같지만 다른 함수
평소에는 이게 문제가 되지 않지만, React.memo와 만나면 이야기가 달라집니다.
React.memo와 함께 보는 문제 상황
React.memo는 props가 바뀌지 않으면 컴포넌트의 리렌더링을 건너뛰게 해주는 최적화 도구입니다.
import { memo } from 'react';
// React.memo로 감싸면, props가 바뀌지 않는 한 리렌더링을 건너뜀
const Child = memo(function Child({ onClick }) {
console.log('Child 렌더링'); // 언제 렌더링되는지 확인용
return <button onClick={onClick}>버튼</button>;
});
이제 부모에 다른 상태가 있어서 리렌더링이 일어난다고 가정해봅시다.
function Parent() {
const [count, setCount] = useState(0);
// ❌ Parent가 리렌더링될 때마다 handleClick이 새로 생성됨
// Child에 넘기는 onClick의 참조값이 매번 달라짐
const handleClick = () => {
console.log('클릭!');
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
{/* count만 바뀌었는데 Child도 매번 리렌더링됨 */}
{/* 왜냐면 handleClick의 참조가 매번 새롭기 때문 */}
<Child onClick={handleClick} />
</div>
);
}
count만 증가시켰을 뿐인데, Child도 함께 리렌더링됩니다. React.memo로 감쌌는데도 말이죠. 이유는 handleClick이 매 렌더링마다 새로 생성되어 참조값이 달라지고, React.memo가 "props가 바뀌었다"고 판단하기 때문입니다.
useCallback으로 해결하기
바로 이 지점에서 useCallback이 등장합니다. useCallback은 함수를 메모이제이션해서, 의존성이 바뀌지 않는 한 같은 함수(같은 참조)를 재사용하게 해줍니다.
import { useState, useCallback, memo } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// ✅ useCallback으로 함수를 감쌈
// 의존성 배열([])이 바뀌지 않는 한 항상 같은 함수를 반환
// → Child에 넘기는 onClick의 참조가 유지됨
const handleClick = useCallback(() => {
console.log('클릭!');
}, []); // 의존성이 없으므로 함수는 최초 1회만 생성
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
{/* 이제 handleClick의 참조가 유지되므로 */}
{/* count가 바뀌어도 Child는 리렌더링되지 않음 */}
<Child onClick={handleClick} />
</div>
);
}
이제 count를 아무리 증가시켜도 Child는 리렌더링되지 않습니다. handleClick의 참조가 유지되어 React.memo가 "props가 그대로다"라고 판단하기 때문입니다.
useCallback의 기본 문법
const memoizedFn = useCallback(() => {
// 실행할 함수 내용
doSomething(a, b);
}, [a, b]); // 의존성 배열
- 첫 번째 인자: 메모이제이션할 함수
- 두 번째 인자: 의존성 배열
- 배열 안의 값이 바뀌면 함수를 새로 생성
- 값이 그대로면 이전에 만든 함수를 그대로 재사용
의존성 배열의 규칙은 useMemo, useEffect와 완전히 동일합니다.
의존성 배열 다루기
함수 내부에서 사용하는 값은 의존성 배열에 넣어줘야 합니다.
function SearchBox({ onSearch }) {
const [keyword, setKeyword] = useState('');
// keyword를 함수 내부에서 사용하므로 의존성 배열에 포함
// keyword가 바뀌면 함수도 새로 생성됨 (최신 keyword를 반영하기 위해)
const handleSearch = useCallback(() => {
onSearch(keyword);
}, [keyword, onSearch]);
return (
<div>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<button onClick={handleSearch}>검색</button>
</div>
);
}
만약 의존성 배열에서 keyword를 빼먹으면, 함수가 처음 만들어질 때의 오래된 keyword(빈 문자열)만 참조하는 버그가 생깁니다. 이를 stale closure(오래된 클로저) 문제라고 부릅니다.
useCallback vs useMemo
이전 글에서 다뤘듯, 이 둘은 형제 같은 관계입니다.
// useMemo: 함수를 "실행한 결과값"을 메모이제이션
const value = useMemo(() => computeValue(a, b), [a, b]);
// useCallback: 함수 "그 자체"를 메모이제이션
const fn = useCallback(() => doSomething(a, b), [a, b]);
// 사실 useCallback(fn, deps)는 useMemo(() => fn, deps)와 동일함
const fn = useMemo(() => () => doSomething(a, b), [a, b]);
| 구분 | useMemo | useCallback |
| 메모이제이션 대상 | 계산된 값 | 함수 자체 |
| 반환값 | 함수 실행 결과 | 함수 그 자체 |
| 대표 용도 | 무거운 계산 결과 캐싱 | 자식에 함수 props 전달 |
한 문장으로: 결과값을 기억하고 싶으면 useMemo, 함수를 기억하고 싶으면 useCallback입니다.
최적화 3종 세트: memo + useMemo + useCallback
리렌더링 최적화는 보통 이 세 가지가 팀으로 움직입니다. 각자의 역할을 정리하면 이렇습니다.
- React.memo: 컴포넌트를 감싸서, props가 그대로면 리렌더링을 건너뜀
- useMemo: 자식에게 넘기는 객체/배열의 참조를 유지 (또는 무거운 계산 캐싱)
- useCallback: 자식에게 넘기는 함수의 참조를 유지
즉, React.memo로 자식의 리렌더링을 막고 싶다면, 그 자식에게 넘기는 함수는 useCallback으로, 객체는 useMemo로 참조를 유지시켜줘야 비로소 최적화가 완성됩니다. 셋 중 하나라도 빠지면 최적화가 깨질 수 있습니다.
const Child = memo(function Child({ data, onClick }) {
return <div onClick={onClick}>{data.title}</div>;
});
function Parent() {
// 함수는 useCallback으로 참조 유지
const handleClick = useCallback(() => { /* ... */ }, []);
// 객체는 useMemo로 참조 유지
const data = useMemo(() => ({ title: "제목" }), []);
// 이제 세 요소가 맞물려 Child의 불필요한 리렌더링을 막음
return <Child data={data} onClick={handleClick} />;
}
남용하지 말 것
useMemo와 마찬가지로, useCallback도 공짜가 아닙니다. 함수를 저장하고 의존성을 비교하는 비용이 들기 때문에, 모든 함수에 붙이는 것은 오히려 손해입니다.
// ❌ React.memo로 감싸지 않은 일반 컴포넌트에 넘기는 함수라면
// useCallback을 써도 최적화 효과가 없고 오버헤드만 늘어남
const handleClick = useCallback(() => {}, []);
<NormalChild onClick={handleClick} />
// ✅ 그냥 일반 함수로 충분함
const handleClick = () => {};
<NormalChild onClick={handleClick} />
useCallback이 실제로 의미가 있는 경우는 다음과 같습니다.
React.memo로 감싼 자식에게 함수를 props로 넘길 때useEffect의 의존성 배열에 함수가 들어갈 때 (함수 참조가 유지되어야 effect가 불필요하게 재실행되지 않음)- 커스텀 Hook이 반환하는 함수를 안정적으로 유지하고 싶을 때
이 조건에 해당하지 않는다면 굳이 useCallback을 쓸 필요가 없습니다.
마무리
useCallback은 함수를 메모이제이션해서 참조 동일성을 유지하는 Hook입니다. 그 자체로는 마법을 부리지 않고, React.memo나 useEffect와 짝을 이룰 때 비로소 의미가 생긴다는 점이 핵심입니다.
정리하면 이렇습니다. 함수는 렌더링마다 새로 생성되어 참조가 달라지고, 이것이 React.memo의 최적화를 무력화시킵니다. useCallback은 이 참조를 붙잡아둠으로써 불필요한 리렌더링을 막아줍니다. 다만 아무 데나 붙이면 오히려 손해이니, "이 함수가 정말 참조 유지가 필요한 곳으로 전달되는가?"를 확인한 뒤 적용하는 것이 좋은 습관입니다.
Tip: useMemo 글에서도 언급했듯, React 19의 React Compiler는 이런 수동 메모이제이션을 상당 부분 자동화해줍니다. 하지만 원리를 이해하고 있어야 컴파일러가 없는 환경이나 예외 상황에 대응할 수 있으니, useCallback의 동작 방식은 확실히 익혀두는 것을 추천합니다.
'IT > React' 카테고리의 다른 글
| React에서 Tailwind CSS 제대로 활용하기 (설치, 활용) (0) | 2026.08.07 |
|---|---|
| [ React ] useReducer 사용법: 복잡한 상태 로직을 체계적으로 관리하기 (0) | 2026.08.07 |
| [React] JSX 기초 완벽 가이드: React의 화면을 그리는 문법 (0) | 2026.08.03 |
| [React] useMemo 사용법: 불필요한 연산을 줄이는 메모이제이션 (0) | 2026.08.03 |
| [React] Context API 완벽 가이드: Props Drilling 없이 상태 공유하기 (0) | 2026.08.03 |