리액트 훅 | useCallback에 대해 알아보기

2025. 8. 12. 15:59·FRONTEND/React

 

리액트에서 함수를 사용할 때 그냥 쓰면 안 되고, useCallback으로 감싸야한다는 이야기를 한 번쯤 들어보셨을 겁니다.

그런데 왜 그래야 하는지, 의존성 배열은 어떻게 설정해야 하는지 감이 잘 오지 않는 경우가 많습니다.

이번 글에서는 간단한 예제를 통해 useCallback의 유무와 의존성 배열 차이가 렌더링에 어떤 영향을 주는지 살펴보겠습니다.

 

0. 함수 참조란

useCallback에 대해 알아보기 전에 "함수 참조"에 대해 알아보겠습니다.

자바스크립트에서는 함수는 객체로, 동일한 코드라도 매번 새로 만들게 되면 참조(=주소)가 달라집니다.

const fn1 = () => {}; // 참조 A
const fn2 = () => {}; // 참조 B (fn1과 다름)

리액트는 props 변경 여부를 얕은 비교(===)로 판단합니다.

그렇기 때문에 참조가 달라지면 내용이 같아도 다른 값으로 인식하며,

그 결과, 자식 컴포넌트가 리렌더 됩니다.

 

1. useCallback이란

함수의 참조(주소)를 기억해 두었다가 의존성이 바뀔 때만 새 함수를 만드는 훅입니다.

쉽게 말해, 매번 함수를 새로 만드는 대신, 필요할 때만 새로 만들어 쓸 수 있게 해주는 도구입니다.

 

기본 문법

const fn = useCallback(() => {
  // 실행 로직
}, [dep1, dep2]);

- 첫번째 인자 : 기억하고 싶은 함수

- 두번째 인자 : 의존성 배열 - Object.is 비교 알고리즘을 통해 각 의존성을 이전값과 비교

(두번째 인자값이 변경되면 새 함수 생성, 그렇지 않으면 이전 함수 재사용)

 

useCallback이 왜 필요한가?

리액트는 부모가 리렌더 될 때 함수도 새로 만들게 됩니다. (-> 참조 변경 발생)

그때 자식이 React.memo로 감싸져 있어도 props로 받은 함수가 바뀌었다고 판단되며,

불필요하게 자식이 다시 렌더링 됩니다.

 

이때, useCallback을 사용해 함수 주소를 고정하게 되면

이러한 불필요 렌더링을 줄일 수 있게 됩니다.

 

의존성 배열에 따른 동작

형태 설명
없음 부모가 리렌더될 때마다 새 함수 생성 (참조 변경 O)
[ ] 최초 1회 생성, 이후 참조 유지 (참조 변경 X)
[state] 해당 state가 바뀔 때만 새 함수 생성

 

2. 예제로 보는 useCallback

이번 예제에서는 다음 3가지의 경우를 함께 비교해보고자 합니다.

(1) useCallback을 사용하지 않은 경우

(2) [] 사용한 경우 

(3) [state] 사용한 경우

 

3개의 자식 컴포넌트(Child)는 onClick 참조가 바뀔 때 배경색이 랜덤 하게 변경되도록 만들었습니다.

 

여기서는 참조 변경이 될 때 색상이 변경되기 때문에 

의존성 배열에 따라 언제 참조가 변경되는지를 한눈에 확인해 볼 수 있습니다.

// 1) useCallback 없음 : 매 렌더마다 새 함수
const handleLeft = () => setC1((v) => v + 1);

// 2) useCallback([]) : 최초 한 번 만든 함수(참조 고정)
const handleMiddle = useCallback(() => setC2((v) => v + 1), []);

// 3) useCallback([c3]) : c3가 바뀔 때만 새 함수
const handleRight = useCallback(() => setC3((v) => v + 1), [c3]);


// onClick "참조"가 바뀔 때만 색상 변경
useEffect(() => {
  onClickRefChangeRef.current += 1;
  setBg(rand());
}, [onClick]);

 

비교 결과

(1) 페이지 새로고침의 경우

3가지 경우 모두 새로 함수가 새롭게 생성되기 때문에 모두 색상이 변경되는 것을 확인할 수 있습니다.

- 초기 마운트 및 초기 함수 생성

 

(2) 부모만 리렌더 버튼 클릭

  - 없음 : 매번 함수 참조 변경 발생 -> 색상 변경

  - [] : 참조 고정 -> 색상 유지

  - [c3] : c3가 변하지 않는 한 참조 유지 -> 색상 유지

 

 

(3) 각 카운트 버튼 클릭 시

  - 없음 : 매번 함수 참조 변경 -> 색상 변경

  - [] : 참조 고정 -> 색상 유지

  - [c3] : c3가 변경되면 참조 변경 -> 색상 변경

 

3. useCallback 사용 시기

(1) 메모된 자식에게 함수 props를 줄 때

  - 상황 : 리스트 아이템이 memo되어있고, onSelect를 props로 내려줌

  - 문제 : 부모가 리렌더될 때마다 새로운 함수가 내려가 아이템이 불필요하게 리렌더가 됨

  - 해결 방법

// 부모
const onSelect = useCallback((id: string) => setSelectedId(id), []);

// 자식
const Item = memo(({ id, onSelect }) => (
  <button onClick={() => onSelect(id)}>{id}</button>
));

 

(2) 웹소켓/이벤트 구독 (= 재구독 비용이 큰 경우)

  - 상황 : 웹소켓이나 이벤트 소스에 콜백을 등록하는 useEffect

  - 문제 : 콜백 참조가 자주 바뀌면 구독/해제가 매번 반복됨 (= 불필요한 작업 + 깜빡임/누락위험 가능성 존재)

  - 해결 방법

const onMessage = useCallback((msg) => { /* ... */ }, [/* 필요한 값들 */]);

useEffect(() => {
  socket.on('message', onMessage);
  return () => socket.off('message', onMessage);
}, [socket, onMessage]); // 불필요 재구독 방지

 

(3) DOM 전역 이벤트

  - 상황 : resize/scroll 같은 전역 이벤트 등록

  - 포인트 : 같은 핸들러 참조가 있어야 removeEventListener가 정확히 동작(매 렌더마다 새로운 함수라면 해제가 빗나갈 수 있기 때문 !)

const onResize = useCallback(() => {
  // ...
}, []);
useEffect(() => {
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, [onResize]);

 

( + ) 안 써도 되는 경우 (사용 비추)

- 자식이 memo가 아니거나 렌더 비용이 매우 작은 경우

- 콜백이 컴포넌트 내부에서만 쓰일 때

 

4. 의존성 쉽게 고르는 방법

(1) 업데이트만 하는 경우 : [] 고정 (함수형 업데이트)

  - 최신 상태를 읽을 필요가 없고, 규치만 적용하면 될 땐 항상 같은 함수로 두는 것이 베스트

// 최신값을 읽지 않고, 규칙만 사용 -> [] 가능
const inc = useCallback(() => setCount(prev => prev + 1), []);
// prev는 React가 최신 값을 보장해주니까 콜백을 고정해도 안전

 

(2) 최신값을 읽어 분기하는 경우 : [그 값들]

  - 콜백 안에서 form, enabled 같은 현재 값을 확인해야 한다면 그 값들을 의존성에 포함

// form, enabled를 읽으니 의존성에 포함
const submit = useCallback(() => {
  if (enabled && form.valid) doSubmit(form);
}, [enabled, form]); // TIP. form 전체 대신 필요한 원시값(form.id, .. )만 넣으면 불필요한 변경 감소

 

(3) 최신 값도 읽고 참조도 고정하고 싶은 경우 : useRef + []

  - 빈 배열로 참조를 고정하고 싶은데 최신 값은 써야 한다면, 값만 ref에 저장해서 읽기

const valueRef = useRef(value);
useEffect(() => { valueRef.current = value; }, [value]);

const onClick = useCallback(() => {
  doSomething(valueRef.current); // 항상 최신
}, []); // 참조 고정

 

5. useCallback을 남용하면 안 되는 이유

(1) " 체감 이득 < 비용 "인 경우가 종종 존재

  - 작은 핸들러를 한두 번 새로 만드는 비용은 거의 들지 않는다고 볼 수 있습니다.

  - useCallback은 메모 저장 + 의존성 비교 + 코드 복잡도의 비용이 생깁니다.

(2) 복잡성이 높아지면 버그 발생률도 같이 상승

  - 의존성 배열을 틀리게 된다면, 스테일 클로저(오래된 값 사용)로 이어지기 쉽습니다.

  - 효과가 없는 곳에 쓰인다면 의미가 사라집니다.

다음과 같은 체크 방법을 활용하실 수 있습니다.
1. React DevTools Profiler로 적용 전 렌더 횟수/시간 기록
2. memo + useCallback 적용
3. 재측정 후 차이가 없다면 원복 (과최적합 XXX !! )

 


다시 한번 정리해보자면,

useCallback은 함수 참조(주소)를 안정화하여 불필요한 리렌더를 줄이는 것입니다.

그렇지만 아무 곳에서나 사용하게 된다면 오히려 복잡해지거나 스테일 클로저 문제로도 이어질 수 있습니다.

'FRONTEND > React' 카테고리의 다른 글

리액트 훅 | useActionState에 대해 알아보기  (3) 2025.08.14
리액트 훅 | useMemo에 대해 알아보기  (6) 2025.08.13
리액트 훅 | useEffect에 대해 알아보기  (5) 2025.08.11
리액트 훅 | useState에 대해 알아보기  (4) 2025.08.08
부모 요소 패딩 값에 영향을 받지 않고 전체 너비 구분선 그리기  (0) 2025.04.04
'FRONTEND/React' 카테고리의 다른 글
  • 리액트 훅 | useActionState에 대해 알아보기
  • 리액트 훅 | useMemo에 대해 알아보기
  • 리액트 훅 | useEffect에 대해 알아보기
  • 리액트 훅 | useState에 대해 알아보기
uoaheu
uoaheu
uoaheu 님의 블로그 입니다.
  • uoaheu
    uoaheu 님의 블로그
    uoaheu
  • 전체
    오늘
    어제
    • 분류 전체보기 (50)
      • 알고리즘 (7)
      • CS (9)
      • FRONTEND (9)
        • React (12)
        • Kotlin (1)
        • JS (5)
        • HTML (2)
      • SQL (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    toss uiux
    알고리즘
    mysql로 피벗테이블만들기
    useactionstate
    이더넷프레임
    리액트usestate
    토스 앱 분석
    toss 분석
    BFS
    유레카3기
    혼자서 공부하는 네트워크
    부트캠프후기
    엘지유플러스유레카프론트엔드
    mysql
    토스 uiux
    boj25418
    백준1926번
    멀티캠퍼스it부트캠프
    토스분석
    mysql 피벗테이블
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
uoaheu
리액트 훅 | useCallback에 대해 알아보기
상단으로

티스토리툴바