💻Dev/React

React : 기본 훅(Hook) - useCallback (+ 메모이제이션 : Memoization, useState의 함수형 업데이트)

jini-dev 2025. 5. 13. 15:51
SMALL

React : Hook

 

React : Hook

React : 컴포넌트와 Props, State (+ useState 훅) 화면을 각 요소로 쪼갠 것- 하나의 JSX 를 반환하는 함수 컴포넌트 만들기 - PascalCase를 사용하여 컴포넌트를 만든다.- 컴포넌트는 기본" data-og-host="jini-dev.t

jini-dev.tistory.com


useCallback

 

컴포넌트에서 최초 렌더링 시 특정 함수를 기억했다가 리렌더링 할 때 재활용 하는 훅이다.

- 메모이제이션 하고 싶은 함수를 콜백 함수로 작성한다.

=> 리렌더링이 발생해도 내부의 함수는 여러번 생성하지 않는다.

- 의존성 배열(dependency list)의 값에 따라 useCallback 내부 로직의 실행 조건을 적용 할 수 있다.

  1. 의존성 배열이 빈 경우 : 최초 컴포넌트 렌더링 시 한번만 실행(함수를 저장)한다.

  2. 의존성 배열에 값이 들어간 경우 : 들어간 상태의 값이 변경 될 때에만 실행 한다.

  (값이 여러개 인경우 여러 상태 값 중 하나라도 변경될 때마다 함수를 지우고 재생성한다)

  => 메모이제이션 했던 함수를 지우고 현재 상태로 함수를 재생성한다.

useCallback 의 사용

모든 함수에 useCallback을 남용한다면 오히려 불필요한 메모이제이션으로 코드가 복잡해지고, 성능이 저하될 수 있다.

아래의 경우(함수 객체의 참조 동일성이 중요한 상황 등)가 아니라면 굳이 사용할 필요가 없다.

 

1.  함수를 자식 컴포넌트에 props로 전달할 때

: 부모 컴포넌트가 리렌더링 될 때 함수가 새로 생성되면 자식 컴포넌트의 props가 변경된 것으로 인식되어 불필요하게 자식 컴포넌트가 리렌더링 된다

2. React.memo, useMemo 등과 함께 렌더링 최적화가 필요할 때

3. useEffect, useMemo 등 의존성 배열에 함수를 넣어야 할 때

: 의존성 배열에 함수를 넣어야 하는 경우 useCallback으로 불필요한 재실행을 막을 수 있다.

4. 이벤트 핸들러 함수가 자주 재생성되어 성능에 영향을 줄 때

 

메모이제이션(Memoization)?
특정 부하가 생기는 함수를 반복해서 작업할 때, 함수 호출의 결과를 저장해두었다가
동일한 입력이 들어오면 다시 계산하지 않고 저장된 결과를 반복하여 사용하는 최적화 기법이다.

대표적인 메모이제이션 도구는 아래와 같다.
- React.memo : 컴포넌트의 props가 변하지 않으면 이전 렌더링 결과를 재사용해서 불필요한 렌더링을 막아준다.
- useMemo : 복잡한 계산의 결과 값을 저장해두고, 의존성이 바뀌지 않는 한 재계산하지 않는다.
- useCallback : 함수 자체를 메모이제이션하여, 의존성이 바뀌지 않으면 같은 함수 객체를 재사용한다.

메모이제이션 하는 이유?
컴포넌트가 리렌더링 될 때마다 함수를 계속 만든다.
이전에 생성한 함수는 브라우저에서 자동으로 Garbage Collecting을 해주지만 그래도 불필요하게 똑같은 역할을 하는 함수를 계속 생성한다.
=> 이러한 코드가 많을 수록 성능이 떨어지기 때문에 메모이제이션을 사용해서 성능 최적화에 도움을 줄 수 있다.
import React, { useCallback } from 'react';

export default function ComponentName() {
  const handleClickButton = useCallback(() => {
    console.log('버튼을 클릭 했습니다.');
  }, []); // 빈 배열 : 최초 1회만 함수 저장
  return <button onClick={handleClickButton}>버튼</button>;
}

 

버튼 3번 클릭

❗useCallback 은 왜 개발자 모드에서도 콘솔이 한번만 출력될까? (useEffect 에서는 두번씩 출력됐는데..)
React.StrictMode여도 useCallback으로 생성된 함수는 의존성 배열이 변하지 않는 한 최초 한 번만 생성된다.
=> 컴포넌트가 여러번 마운트/언마운트 되어도 새로 생성하지 않고, 이전에 생성된 함수의 객체가 재사용되기 때문에
useState나 useEffect 등에서 처럼 콘솔이 두번씩 출력되지 않는다.

 

사용 예시

import React, { useState, useCallback } from 'react';

export default function UseCallbackComponent() {
  // 1. 의존성 배열이 [] 빈 배열 인 경우
  const handleClickButton = useCallback(() => {
    console.log('버튼을 클릭 했습니다.');
  }, []); // 의존성 배열 : [] 빈 배열 => 최초 렌더링 시 1회만 함수를 저장하고 그대로 사용한다.

  // 2. 의존성 배열에 값이 들어간 경우
  const [value, setValue] = useState(0);
  const handleIncreaseValue = useCallback(() => {
    console.log('value 값을 증가합니다.');
    setValue(value + 1);
  }, [value]); // 의존성 배열 : [value] => value 값이 변경 될 때마다 기존 함수를 지우고 재생성한다.
  
  return (
    <>
      <button onClick={handleClickButton}>버튼</button>
      <h1>value : {value}</h1>
      <button onClick={handleIncreaseValue}>증가버튼</button>
    </>
  );
}

의존성 배열에 따른 useCallback
import React, { useState, useCallback } from 'react';

export default function ComponentName() {
  // 1. 의존성 배열이 [] 빈 배열 인 경우
  const handleIncreaseValue1 = useCallback(() => {
    setValue(value + 1);
    console.log('빈배열 버튼 :', value);
  }, []);

  // 2. 의존성 배열에 값이 들어간 경우
  const [value, setValue] = useState(0);
  const handleIncreaseValue2 = useCallback(() => {
    setValue(value + 1);
    console.log('value 배열 버튼 :', value);
  }, [value]);
  return (
    <>
      <h1>value : {value}</h1>
      <button onClick={handleIncreaseValue1}>빈배열버튼</button>
      <button onClick={handleIncreaseValue2}>value배열버튼</button>
    </>
  );
}

 

위와 같이 코드를 짜보았다

 

 

1. 빈배열 버튼을 5번 눌러보았다

값이 1이 증가한 상태로 계속 머물러 있다

 

????

 

설명

useCallback의 의존성 배열이 [ ] 빈 배열이기 때문에,
useCallback의 내부 함수는 컴포넌트가 처음 렌더링(마운트) 될 때 한 번만 생성되었다.
이때 함수 내부에서 참조하는 value의 값도 마운트 시점의 value( 즉, value의 초기 값인 0) 으로 고정되었기 때문에
value가 아무리 바뀌어도 이 함수 내부에서 참조하는 value 의 값은 0으로 변하지 않는 것이다.
(즉, 버튼을 아무리 눌러도 함수 내부 value는 항상 0 이므로 setValue(0+1) 만 반복해서 실행되는 것)

ex) 
value : 0 -> 버튼 클릭 -> setValue(1), console.log(0) -> 리렌더링 -> 화면 value : 1
value : 0 -> 버튼 클릭 -> setValue(1), console.log(0) -> 리렌더링 -> 화면 value : 1

 

콘솔 값이 계속 0인 이유는 아래(2번) 참고

 


 

2. value 배열 버튼을 5번 눌러보았다

값이 원하는대로 증가했다.

handleIncreaseValue2 는 value 가 바뀔 때 마다 새로 만들어졌다. => value 값이 원하는대로 증가했다.

 

근데 왜 콘솔에 출력된 value 값은 4인가

??????????

 

설명

useState의 setter 함수는 비동기적으로 동작해서,
setter함수를 호출하면 컴포넌트가 리렌더링 될 때 state(상태) 값이 변경되어 반영된다.
EX) setValue를 호출 한 후 변경한 값이 컴포넌트가 리렌더링 될 때 value에 반영된다.

따라서 setValue(value + 1)을 호출하면 value 값이 즉시 바뀌지 않고, 다음 렌더링 때 반영되기 때문에
setValue 호출 직후 console.log를 출력 하면 변경되기 이전의 value 값을 출력하는 것이다.
=> 아직 렌더링 되기 이전에 setValue 호출 후 value 값을 콘솔로 출력했기 때문에 value가 증가하기 이전의 값을 출력함

ex)
value : 0 버튼 클릭 -> setValue(1), console.log(0) -> 리렌더링 -> 화면 value : 1
value : 1 버튼 클릭 -> setValue(2),console.log(1) -> 리렌더링 -> 화면 value : 2

 

위와 같은 문제를 해결하고 싶다면 코드를 이런식으로 작성해서 확인하면 된다.


useState의 함수형 업데이트

 

1번의 문제를 해결해보자

=> useCallback 에 의존성 배열을 빈 배열로 넣었더니 value값이 초기 값으로 고정되어 변경되지도, 화면에서 바뀌지도 않는다.

 

  const handleIncreaseValue1 = useCallback(() => {
    setValue((prev) => prev + 1); // setter 함수의 함수형 업데이트를 사용 => prev : 기존 value의 값
    console.log('버튼 클릭 :', value);
  }, []);

 

useCallback 내부의 value는 초기 값 0으로 고정되어, 콘솔에서는 0으로 찍히지만

실제 value 값이 변경되었기 때문에 화면 상의 value는 변경되었다.


2번 문제를 해결해보자

=> 콘솔로그가 변경되는 화면과 동일하게 나오게 하고 싶다.

  const handleIncreaseValue2 = useCallback(() => {
    setValue((prev) => { // setter 함수의 함수형 업데이트를 사용 => prev : 기존 value의 값
      // 기존 value에 + 1 값을 next 변수로 지정하여 콘솔로 출력하고 setValue 변경 값으로 반환한다
      const next = prev + 1; 
      console.log('value 배열 버튼 :', next);
      return next;
    });
  }, [value]);

 

❗콘솔에는 바로 원하는 값이 나오게 했지만,

useState의 함수형 업데이트를 사용 하는 경우에는 의존성 배열에 값을 ([value]) 넣을 필요가 없다!!!

 

이건 왜 콘솔에 두번 씩 찍히는가?
❗setValue 에서 updater 함수( 함수형 업데이트)에 적용한 콘솔은 StrictMode 에서 두 번씩 실행된다.
- useCallback 함수 내부 콘솔은 React.StrictMode 에서 한 번 실행
- useState의 updater 함수 (setter 함수의 함수형 업데이트) 내부 콘솔은 React.StrictMode 에서 두 번 실행

근데 위 1번 해결 방법 함수를 useCallback 을 사용하는거랑 useCallback 을 사용하지 않는 코드가 성능 차이가 크게 많이 나나??
-> 이벤트 핸들러로만 쓸때는 성능상 거의 차이 없다고 함

 

그리고 위 2번 해결 방법과 같이 value가 변경될 때마다 함수가 지워지고 재생성되면 useCallback 사용 안해도 되는 것 아닌가???

(물론 2번 해결 방식과 같이 쓰면 안된다! 함수형 업데이트를 하는 경우 의존성 배열에 값을 넣을 필요가 없음)

 

오히려 useCallback을 남용하면 메모이제이션 비용이 발생해 성능이 떨어질 수 있다.

그래서 보통은 아래와 같은 경우에 useCallback을 사용한다.

useCallback을 사용하는 경우

 

import React, { useState, useCallback } from 'react';

export default function UseCallbackComponent() {
  const [value, setValue] = useState(0);

  const increaseValue = () => {
    setValue(value + 1);
    console.log('증가 버튼 클릭');

  };

  const resetValue = useCallback(() => {
    setValue(0);
    console.log('리셋 버튼 클릭');
    
  }, []);

  return (
    <>
      <h1>value : {value}</h1>
      <button onClick={increaseValue}>증가 버튼</button>
      <button onClick={resetValue}>리셋 버튼</button>
    </>
  );
}

 

increaseValue 함수는useCallback 을 사용해도 value 값이 변경될 때마다 함수를 재생성하기 때문에 useCallback 이 불필요하다.

resetValue와 같은 함수에 (어떤 값이 와도 value를 0으로 만들어서 재활용성이 좋은 함수) useCallback을 사용하면 최초 렌더링 후 같은 함수를 재활용해서 쓰기 때문에 좀 더 좋은 성능으로 컴포넌트를 실행할 것이다.

 

 

LIST