React의 useCallback 최적화한 커스텀 훅 usePreservedCallback

hodu·2024년 2월 4일
7

Library

목록 보기
4/5
post-thumbnail

✅ 개요

최근 코드 품질 향상을 위해 여러 오픈 소스 프로젝트를 탐색하던 중, toss/slash@toss/react 라이브러리에서 발견한 usePreservedCallback 모듈이 인상적이었습니다. 이 모듈은 ReactuseCallback 훅 사용 시 발생했던 아쉬운 점을 개선하는 데 큰 도움이 되었습니다.

이 글에서 다룰 주제는 다음과 같습니다.

1. useCallbacK
3. usePreservedCallback
3. 사용 사례




📌 useCallback 소개

useCallback은 공식 문서에서 다음과 같이 정의됩니다:

useCallback 훅은 함수를 메모이제이션하며, 리렌더링 간에 함수를 재사용할 수 있게 합니다.

특히, 함수를 자식 컴포넌트의 prop으로 전달할 때 이 훅을 사용하면, 불필요한 리렌더링을 방지할 수 있습니다.


🔍 사용법

useCallback은 아래와 같이 사용됩니다:

useCallback(fn, dependencies)
  • 첫 번째 인자는 메모이제이션할 함수입니다.
  • 두 번째 인자는 의존성 배열로, 배열 내 값이 변경될 때만 함수가 재생성됩니다.
    배열이 비어 있으면, 함수는 컴포넌트가 처음 마운트할 때 생성하고 이후 재렌더링에서는 재생성하지 않습니다.

🚀 권장 사용 사례

  1. React.memo와 결합: React.memo로 감싼 컴포넌트에 함수를 prop으로 전달할 때, useCallback을 사용하면, 의존성 배열의 값이 변경되지 않는 한 리렌더링을 방지할 수 있습니다.

  2. 빈번한 Effect 방지: useCallback으로 감싸진 함수는 useEffect 등의 훅에서 의존성으로 사용할 때, 함수 참조의 안정성으로 인해 불필요한 Effect 실행을 줄일 수 있습니다.

  3. 커스텀 훅 최적화: 재사용 가능한 커스텀 훅에서 useCallback을 사용하면, 훅이 반환하는 함수의 참조를 안정적으로 유지하며 효율적인 캐싱과 성능 최적화를 이룰 수 있습니다.

자세한 정보는 React 공식 문서를 참조하세요.

이외에도 생성하는데 1초 이상의 비용이 큰 함수에서도 권장합니다.

이 코드는 자주 호출되는가?
이 코드가 실행될 때의 결과는 항상 같은가?
이 코드가 계산이 복잡한가?

위 3가지에 판별법에 맞추어서 사용하기도 합니다.

공식 문서에 따르면 useCallback의 캐싱이 대부분의 경우, 큰 부담은 없다고 합니다. 때로는 많이 메모하는 방식을 선택할 수도 있다고 합니다.

그보다 문제점으로 이야기하는 부분은 가독성이 떨어진다는 점을 문제 삼습니다.


⚠️ useCallback의 단점

useCallback 훅은 함수를 메모이제이션하여 리렌더링 간에 동일한 함수 인스턴스를 재사용할 수 있게 해주지만, 몇 가지 단점이 있습니다:

  1. 스코프의 고정: useCallback은 함수가 생성할 때의 스코프를 "기억"합니다. 이는 함수가 최신 상태나 prop을 반영하지 못할 수 있음을 의미하며, 이를 해결하기 위해 해당 값들을 의존성 배열에 포함해야 합니다.
  2. 참조의 변화: 의존성 배열 내의 값이 변경할 때마다 useCallback은 새로운 함수 인스턴스를 생성하고, 이는 함수의 참조를 변경합니다. 이는 특히 함수를 prop으로 전달하거나 다른 훅의 의존성으로 사용할 때 문제가 될 수 있습니다.

정리하면, useCallback은 의존성 배열 없이 사용 시 함수의 동일한 참조를 유지할 수 있으나, 최신 상태나 prop을 반영하지 못할 수 있습니다. 반면, 의존성 배열을 사용하면 최신 값을 반영할 수 있으나, 참조가 변경될 수 있습니다.

특성useCallback (의존성 배열 없음)useCallback (의존성 배열 있음)
참조 유지동일한 참조 유지의존성 배열의 값 변경 시 참조 갱신
최신 값 반영초기 스코프 값을 "기억"의존성 배열 값 변경 시 최신 값으로 갱신

이러한 이유로, 때때로 useCallback만으로는 최적화가 어려운 경우가 있습니다.


⭐ usePreservedCallback

usePreservedCallbackuseCallback의 단점을 보완하기 위한 커스텀 훅입니다. 이 훅은 함수의 참조 안정성을 유지하면서도 내부적으로 최신 상태나 prop을 반영할 수 있는 방식으로 설계되었습니다.

import { useCallback, useEffect, useRef } from 'react';

export function usePreservedCallback(callback) {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback; // 최신 콜백으로 업데이트
  }, [callback]);

  return useCallback(() => callbackRef.current(...args), []);
}

이 접근 방식의 핵심은 useRef를 사용하여 함수 참조를 저장하고, useEffect로 최신 함수를 ref.current에 지속해서 업데이트하는 것입니다. 이를 통해, 컴포넌트 리렌더링 시에도 함수의 동일한 참조를 유지하면서 최신 값이나 상태를 반영할 수 있습니다.

usePreservedCallback은 특히 여러 상태나 prop에 의존하면서도 함수의 참조 안정성이 중요한 경우에 유용하며, useCallback의 단점을 보완하여 보다 효과적인 성능 최적화를 가능하게 합니다.


🧪 usePreservedCallback 테스트 코드 분석

usePreservedCallbackuseCallback의 단점을 극복하고자 개발된 커스텀 훅입니다. 이 훅의 주요 장점은 함수의 참조 안정성을 유지하면서도, 최신 상태나 prop을 반영할 수 있다는 것입니다. 이 섹션에서는 usePreservedCallback의 실제 작동 방식과 그 효과를 검증하기 위한 테스트 코드를 살펴보겠습니다.

테스트 1: 상태 업데이트 후의 참조 안정성

첫 번째 테스트는 상태 업데이트 후에도 usePreservedCallback이 반환하는 함수의 참조가 유지하는지 확인합니다. 상태 값이 변하고 컴포넌트가 다시 렌더링되어도, usePreservedCallback은 동일한 함수 참조를 유지함으로써 불필요한 리렌더링을 방지합니다.

it('preserves the callback reference even after state updates', () => {
  const { result, rerender } = renderHook(() => {
    const [stateValue, setStateValue] = useState(10);
    const testCallback = jest.fn(() => stateValue);
    const preservedCallback = usePreservedCallback(testCallback);

    return { preservedCallback, setStateValue };
  });

  const initialCallback = result.current.preservedCallback;
  act(() => { result.current.setStateValue(20); });
  const updatedCallback = result.current.preservedCallback;

  expect(updatedCallback).toBe(initialCallback); // 동일 참조 확인
});

테스트 2: 동일 참조로 최신 상태 값 반영 여부

두 번째 테스트는 참조를 유지하면서 usePreservedCallback이 상태 업데이트를 정확히 반영하는지 검증합니다. 상태가 업데이트하면 usePreservedCallback은 내부적으로 최신 상태를 반영하는 새로운 함수를 생성하지 않고도 최신 상태 값을 정확히 반환합니다.

 it('returns updated value from the callback after state change', () => {
    const { result } = renderHook(() => {
      const [stateValue, setStateValue] = useState(10);
      const testCallback = jest.fn(() => stateValue);
      const preservedCallback = usePreservedCallback(testCallback);

      return { preservedCallback, setStateValue };
    });

    const initialValue = result.current.preservedCallback();
    expect(initialValue).toBe(10); // 초기 상태 값 반영 확인

    act(() => {
      result.current.setStateValue(20);
    });

    const updatedValue = result.current.preservedCallback();
    expect(updatedValue).toBe(20); // 최신 상태 값 반영 확인
  });

테스트 3: 인자의 정확한 전달

세 번째 테스트는 usePreservedCallback을 통해 전달된 인자가 내부 콜백 함수에 올바르게 전달되는지 확인합니다. 이는 usePreservedCallback이 인자를 적절히 처리하여 기대하는 동작을 수행함을 보여줍니다.

it('ensures arguments are correctly passed to the wrapped callback function', () => {
  const externalCallback = jest.fn((increment) => increment);
  const { result } = renderHook(() => usePreservedCallback(externalCallback));

  act(() => { result.current(10); });

  expect(externalCallback).toHaveBeenCalledWith(10); // 인자 전달 확인
});

usePreservedCallback의 효과

이러한 테스트 코드를 통해 usePreservedCallbackuseCallback의 주요 단점을 해결하면서 함수의 참조 안정성과 최신 상태 값을 유지할 수 있음을 확인할 수 있습니다. 이는 특히 상태가 자주 변경되거나 여러 컴포넌트 간에 함수를 전달해야 하는 복잡한 애플리케이션에서 유용하게 사용할 수 있습니다.

useCallback가 비교해서 표로 정리해보자

특성useCallback (의존성 배열 없음)useCallback (의존성 배열 있음)usePreservedCallback
참조 유지동일한 참조 유지의존성 배열의 값이 변경될 때만 참조 갱신동일한 참조 유지
최신 값최초 생성 시점의 스코프를 기억의존성 배열에 포함된 값이 변경되면 최신 값으로 갱신항상 최신 콜백 함수를 호출

자 이제 3개의 사용방식에 대해서 정리해보았다 그렇다면 사용은 어떻게 해야할까?


🚀 실제 사용 사례

useCallbackusePreservedCallback은 각각 고유한 장점을 갖고 있다. 여기서는 이 두 훅을 언제 사용해야 할지 구체적인 가이드라인을 제시하고자 합니다.

useCallback 사용 사례

의존성 배열 없음: 함수가 외부 데이터에 의존하지 않고, 동일한 로직을 수행해야 할 경우에 적합합니다. 이러한 경우, useCallback은 함수의 참조를 유지하여 불필요한 재생성을 방지합니다.

    const logMessage = useCallback(() => {
      console.log('This message is always the same.');
    }, []);

의존성 배열 있음: 함수가 특정 상태나 prop에 의존하여 작동해야 할 때 사용됩니다. useCallback은 의존성 배열에 명시된 값이 변경될 때만 함수를 재생성하므로, 함수의 동작이 해당 값들에 정확히 의존하게 됩니다.

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

    const incrementCount = useCallback(() => {
      setCount(count + 1);
    }, [count]);

usePreservedCallback 사용 사례

usePreservedCallbackuseCallback의 몇 가지 한계를 극복하며, 특히 함수의 참조 안정성과 최신 상태 반영의 균형을 맞추고자 할 때 유용합니다.

동적인 상태 의존성: 여러 상태나 prop에 의존하면서도 함수의 참조를 안정적으로 유지해야 하는 경우에 적합합니다. usePreservedCallback은 내부적으로 useRefuseEffect를 활용하여 최신 상태를 반영하면서도 함수 참조를 유지합니다.

   const [value, setValue] = useState(0);

   const handleUpdate = usePreservedCallback(() => {
     console.log(value); // 항상 최신 상태를 반영
   });

🎯 결론: 언제 어떤 훅을 사용해야 할까?

  • 명시성이 중요할 때: useCallback은 의존성 배열을 통해 함수가 어떤 값에 의존하는지 명확하게 표현할 수 있습니다. 함수의 동작이 명확한 의존성을 갖고 있을 때, useCallback을 사용하는 것이 좋습니다.

  • 참조 안정성과 최신 상태가 중요할 때: 여러 상태나 prop에 의존하는 복잡한 함수에서 참조 안정성을 유지하면서 최신 상태를 반영해야 할 경우, usePreservedCallback이 더 적합할 수 있습니다.

각 훅의 사용은 컴포넌트의 성능 최적화와 코드의 가독성 사이에서 균형을 맞추는 것이 중요합니다.
명확성과 최신 상태 반영의 필요성을 고려하여 적절한 훅을 선택하여 적합한 최적화를 진행하시길 바랍니다!


출처 :
https://react-ko.dev/reference/react/useCallback
https://slash.page/ko/
https://slash.page/ko/libraries/react/react/src/hooks/usepreservedcallback.i18n/
https://slash.page/ko/libraries/react/react/src/hooks/usepreservedreference.i18n/

profile
잘부탁드립니다.

0개의 댓글