useCallback 사용하기

꾸준히·2025년 6월 23일

React

목록 보기
2/2

useCallback

const memoizedFn = useCallback(() => {
  // some logic
}, [deps]);
  • React 16.8에서 도입된 기본 Hook
  • 함수 자체를 메모이제이션해서, 동일한 함수를 재사용하도록 하는 Hook
  • 재렌더링 시 불필요한 함수가 재생성되는 것을 방지함
  • 그로인해, 성능 최적화에 도움을 줌

동일한 함수 재사용?

동일한 함수라고 하면 내가 선언한 함수라고 생각할 수 있음.
뭐 이것도 어떻게 보면 맞는 말이긴 함.
그런데 여기서 말하고자 하는 바는, 참조값임.

자바스크립트 함수는 객체로 취급되기 때문에, 변수에 값(value)가 아니라 메모리 주소(참조값)를 저장함.

그래서 useCallback 없이 함수를 불러올 때, 눈에 보이는 건 동일한 함수를 불러오는 것 같지만? 까보면 참조값이 다르기 때문에 React에서는 다른 함수로 간주함.

그래서 리렌더링 할 때마다 새로운 함수가 다시 정의되면서(새로운 참조값), React는 props가 변경된 것으로 판단해 의도치 않은 리렌더링이 발생하는거.


그래서 이런걸 방지하고자

useCallback을 사용함.

useCallback은 함수의 참조값을 고정해줘서 리렌더링이 되어도 의존성 배열이 바뀌지 않는 한 같은 함수를 유지함


그럼 이걸 언제 사용?

1) 자식 컴포넌트에 props로 함수를 전달하는 경우

고유 함수를 생성하고 이걸 부모를 통해 자식에게 전달되면, props를 통해 함수를 전달받은 자식 컴포넌트는 props가 변경되었다고 인식해서 리렌더링을 발생시킴.

이런걸 방지하고자 useCallback으로 함수를 만들어 자식 컴포넌트에게 전달하고,
자식컴포넌트는 함수 재사용하는 것으로 인식해 리렌더링을 방지

const Parent = () => {
  const handleClick = () => {
    console.log("Clicked");
  };

  return <Child onClick={handleClick} />;
};
  • 이렇게 하면, Parent가 리렌더링 될 때마다 handleClick도 새로 만들어지고, Child는 매번 리렌더링 됨.
const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);

그래서 이렇게 useCallback으로 리렌더링을 방지

2) useEffect, useMemo, useRef 등 deps에 함수가 들어가는 경우

무슨 말이냐면, 각 Hooks의 의존성 배열에 함수가 들어가는 경우를 말함.

function App() {
  const [count, setCount] = useState(0);

  const doSomething = () => {
    console.log("doing something");
  };

  useEffect(() => {
    doSomething(); // 매번 실행됨
  }, [doSomething]); // 함수가 의존성 배열에 들어감
}

예를 들어 이런 상황이다?

  • doSomething 함수는 컴포넌트가 리렌더링 될 때마다 새로 생성됨
  • 그래서 useEffectdoSomething의 참조값이 매번 달라진다고 판단
  • 결론 : useEffect는 의미 없이 매번 실행됨

이러한 불필요 리렌더링 방지하기 위해 useCallback을 사용한다면?

const doSomething = useCallback(() => {
  console.log("doing something");
}, []);
  • doSomething은 리렌더링돼도 같은 참조값 유지
  • 의존성 배열에 넣어도 useEffect가 실행되지 않음

이건 useMemouseRef도 마찬가지임.

  • useMemo(() => doHeavyThing(fn), [fn])
  • useRef(fn)에서 fn이 리렌더링마다 바뀌면 ref.current도 바뀜

3) 외부 의존성인 경우(외부에서 값을 가져오는 api 호출)

const [userId, setUserId] = useState("123");

const fetchUserData = () => {
	 const res = await axios.get(`/api/users/${userId}`);
}

useEffect(() => {
	...
}, [fetchUserData])

이렇게 되면,

  1. fetchUserData에 새로운 함수(참조값) 할당
  2. useEffect 함수가 호출
  3. 의존성 배열에 의해 새로운 fetchUserData 계속 생성
  4. 무한루프 발생

그래서 이런걸 방지하고자

const [userId, setUserId] = useState("123");

const fetchUserData = useCallback(async () => {
  const res = await axios.get(`/api/users/${userId}`);
  console.log(res.data);
}, [userId]);

useEffect(() => {
	...
}, [fetchUserData])
  • useCallback에서 userId가 변경될 때만 새로운 fetchUserData가 할당되고
  • 새로운 fetchUserData가 할당될 때 useEffect 실행되게 해서 무한루프 방지

그럼 이거 사용하면 무조건 성능 좋아짐?

그렇지는 않음. 모든 함수를 useCallback으로 감싸면 오히려 코드 복잡도만 높이고 성능 이점은 거의 없음

그래서 꼭 필요한 경우에만 사용!


번외 : Tanstack Query와는?

✅ 같이 써도는 됨. 근데 이것도 필요한 경우에만!

일반적으로는 굳이 useCallback을 같이 쓸 필요는 없음

왜냐면,

  1. queryFn은 대부분 컴포넌트 밖에서 정의함
    • 일반적으로 useQueryqueryFn은 외부에서 한 번 정의하고 그대로 사용함
      → 렌더링마다 함수가 새로 만들어지는 문제가 애초에 없음
  2. React Query는 함수 참조가 바뀌어도 무조건 refetch하지 않음
    • queryKey가 바뀌지 않는 한, 함수 참조값이 바뀌었다고 해서 자동 refetch 안 함
    • 내부적으로 캐시와 queryKey를 기준으로 동작하기 때문에
  3. 리렌더링을 유발하지 않음
    • useQuery, useMutation 등은 내부 상태를 자체적으로 관리해서
    • queryFn이 새로 만들어져도 React의 일반적인 props처럼 리렌더링 트리거로 작용하지 않음

useCallback을 굳이 함께 쓰지 않아도 되는 이유

TanStack Query에서는 queryFn을 컴포넌트 외부에서 정의하거나,
함수가 새로 생성되어도 내부 캐시 전략으로 인해 불필요한 refetch나 리렌더링이 발생하지 않기 때문에, 일반적으로 useCallback을 굳이 함께 쓸 필요는 없다.

그럼에도 같이 써야하는 경우는?

  1. queryFn이 외부 의존성을 가지고, 컴포넌트 내부에서 정의 될 때

    const fetchData = useCallback(() => {
      return axios.get(`/api/data?filter=${filter}`);
    }, [filter]);
    
    const { data } = useQuery(['data', filter], fetchData);
    • fetchData가 매번 바뀌면 queryKey는 같아도 queryFn이 달라져서 useQuery가 새로 실행될 수 있음
      useCallback으로 fetchData를 정의하는게 좋음
  2. useEffect 등에 deps로 넣고 쓰는 경우,
    자식 컴포넌트에 refetch 같은 함수를 props로 넘겨야하는 경우

    const { refetch } = useQuery(...);
    
    const handleRefresh = useCallback(() => {
      refetch();
    }, [refetch]);
    
    <Child onRefresh={handleRefresh} />

0개의 댓글