최근 코드 품질 향상을 위해 여러 오픈 소스 프로젝트를 탐색하던 중, toss/slash
의 @toss/react
라이브러리에서 발견한 usePreservedCallback
모듈이 인상적이었습니다. 이 모듈은 React
의 useCallback
훅 사용 시 발생했던 아쉬운 점을 개선하는 데 큰 도움이 되었습니다.
이 글에서 다룰 주제는 다음과 같습니다.
1. useCallbacK
3. usePreservedCallback
3. 사용 사례
useCallback
은 공식 문서에서 다음과 같이 정의됩니다:
useCallback 훅은 함수를 메모이제이션하며, 리렌더링 간에 함수를 재사용할 수 있게 합니다.
특히, 함수를 자식 컴포넌트의 prop으로 전달할 때 이 훅을 사용하면, 불필요한 리렌더링을 방지할 수 있습니다.
useCallback
은 아래와 같이 사용됩니다:
useCallback(fn, dependencies)
React.memo와 결합: React.memo
로 감싼 컴포넌트에 함수를 prop으로 전달할 때, useCallback
을 사용하면, 의존성 배열의 값이 변경되지 않는 한 리렌더링을 방지할 수 있습니다.
빈번한 Effect 방지: useCallback
으로 감싸진 함수는 useEffect
등의 훅에서 의존성으로 사용할 때, 함수 참조의 안정성으로 인해 불필요한 Effect 실행을 줄일 수 있습니다.
커스텀 훅 최적화: 재사용 가능한 커스텀 훅에서 useCallback
을 사용하면, 훅이 반환하는 함수의 참조를 안정적으로 유지하며 효율적인 캐싱과 성능 최적화를 이룰 수 있습니다.
자세한 정보는 React 공식 문서를 참조하세요.
이외에도 생성하는데 1초 이상의 비용이 큰 함수에서도 권장합니다.
이 코드는 자주 호출되는가?
이 코드가 실행될 때의 결과는 항상 같은가?
이 코드가 계산이 복잡한가?
위 3가지에 판별법에 맞추어서 사용하기도 합니다.
공식 문서에 따르면 useCallback
의 캐싱이 대부분의 경우, 큰 부담은 없다고 합니다. 때로는 많이 메모하는 방식을 선택할 수도 있다고 합니다.
그보다 문제점으로 이야기하는 부분은 가독성이 떨어진다는 점을 문제 삼습니다.
useCallback
훅은 함수를 메모이제이션하여 리렌더링 간에 동일한 함수 인스턴스를 재사용할 수 있게 해주지만, 몇 가지 단점이 있습니다:
useCallback
은 함수가 생성할 때의 스코프를 "기억"합니다. 이는 함수가 최신 상태나 prop을 반영하지 못할 수 있음을 의미하며, 이를 해결하기 위해 해당 값들을 의존성 배열에 포함해야 합니다.useCallback
은 새로운 함수 인스턴스를 생성하고, 이는 함수의 참조를 변경합니다. 이는 특히 함수를 prop으로 전달하거나 다른 훅의 의존성으로 사용할 때 문제가 될 수 있습니다.정리하면, useCallback
은 의존성 배열 없이 사용 시 함수의 동일한 참조를 유지할 수 있으나, 최신 상태나 prop을 반영하지 못할 수 있습니다. 반면, 의존성 배열을 사용하면 최신 값을 반영할 수 있으나, 참조가 변경될 수 있습니다.
특성 | useCallback (의존성 배열 없음) | useCallback (의존성 배열 있음) |
---|---|---|
참조 유지 | 동일한 참조 유지 | 의존성 배열의 값 변경 시 참조 갱신 |
최신 값 반영 | 초기 스코프 값을 "기억" | 의존성 배열 값 변경 시 최신 값으로 갱신 |
이러한 이유로, 때때로 useCallback
만으로는 최적화가 어려운 경우가 있습니다.
usePreservedCallback
은 useCallback
의 단점을 보완하기 위한 커스텀 훅입니다. 이 훅은 함수의 참조 안정성을 유지하면서도 내부적으로 최신 상태나 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
테스트 코드 분석usePreservedCallback
은 useCallback
의 단점을 극복하고자 개발된 커스텀 훅입니다. 이 훅의 주요 장점은 함수의 참조 안정성을 유지하면서도, 최신 상태나 prop을 반영할 수 있다는 것입니다. 이 섹션에서는 usePreservedCallback
의 실제 작동 방식과 그 효과를 검증하기 위한 테스트 코드를 살펴보겠습니다.
첫 번째 테스트는 상태 업데이트 후에도 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); // 동일 참조 확인
});
두 번째 테스트는 참조를 유지하면서 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); // 최신 상태 값 반영 확인
});
세 번째 테스트는 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
의 효과이러한 테스트 코드를 통해 usePreservedCallback
은 useCallback
의 주요 단점을 해결하면서 함수의 참조 안정성과 최신 상태 값을 유지할 수 있음을 확인할 수 있습니다. 이는 특히 상태가 자주 변경되거나 여러 컴포넌트 간에 함수를 전달해야 하는 복잡한 애플리케이션에서 유용하게 사용할 수 있습니다.
useCallback가 비교해서 표로 정리해보자
특성 | useCallback (의존성 배열 없음) | useCallback (의존성 배열 있음) | usePreservedCallback |
---|---|---|---|
참조 유지 | 동일한 참조 유지 | 의존성 배열의 값이 변경될 때만 참조 갱신 | 동일한 참조 유지 |
최신 값 | 최초 생성 시점의 스코프를 기억 | 의존성 배열에 포함된 값이 변경되면 최신 값으로 갱신 | 항상 최신 콜백 함수를 호출 |
자 이제 3개의 사용방식에 대해서 정리해보았다 그렇다면 사용은 어떻게 해야할까?
useCallback
과 usePreservedCallback
은 각각 고유한 장점을 갖고 있다. 여기서는 이 두 훅을 언제 사용해야 할지 구체적인 가이드라인을 제시하고자 합니다.
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
사용 사례usePreservedCallback
은 useCallback
의 몇 가지 한계를 극복하며, 특히 함수의 참조 안정성과 최신 상태 반영의 균형을 맞추고자 할 때 유용합니다.
동적인 상태 의존성: 여러 상태나 prop에 의존하면서도 함수의 참조를 안정적으로 유지해야 하는 경우에 적합합니다. usePreservedCallback
은 내부적으로 useRef
와 useEffect
를 활용하여 최신 상태를 반영하면서도 함수 참조를 유지합니다.
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/