inblog logo
|
석우의 개발블로그
    React

    useCallback 파헤치기

    강석우's avatar
    강석우
    Mar 05, 2024
    useCallback 파헤치기
    Contents
    useCallback 이란?useCallback, 언제 사용해야 할까useCallback 의 구성사용시 주의사항사용법1. 컴포넌트의 렌더링 건너뛰기2. 메모된 콜백에서 state 업데이트하기3. Effect가 너무 자주 발동되지 않도록 하기4. 커스텀 훅 최적화하기그래서 useMemo 와 useCallback의 차이가 뭐야?

    useCallback 이란?

    useCallback은 리렌더링 사이에 함수 정의를 캐시할 수 있게 해주는 React 훅이다.

    useCallback, 언제 사용해야 할까

    간략하게 정리하자면 리렌더링으로 인한 함수의 재정의를 하고 싶지 않을 때 사용한다.
    아래의 리스트에 대한 자세한 설명은 사용법 파트에 나와있다.

    • 컴포넌트의 리렌더링 건너뛰기

    • 메모된 콜백에서 state 업데이트하기

    • Effect가 너무 자주 발동되지 않도록 하기

    • 커스텀 훅 최적화하기

    useCallback 의 구성

    useMemo는 calculateValue와 dependencies로 나뉘어진다.
    fn : 캐시하려는 함수.
    어떤 인자도 맏을 수 있고 어떤 값이라도 반환할 수 있다. 만약 dependencies가 변경되지 않았다면 동일한 함수를 제공한다. 변경되었다면 렌더링 중에 fn를 제공하고 재사용할 수 있도록 저장한다.

    dependencies : calculateValue 에 참조되는 값들의 목록.
    Object.is 비교 알고리즘을 사용하여 각 dependencies를 이전 값과 비교한다.

    useMemo(calculateValue, dependencies)
    
    import { useMemo } from 'react';
    
    function TodoList({ todos, tab }) {
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab]
      );
      // ...
    }

    사용시 주의사항

    • useCallback은 훅이기 때문에 컴포넌트의 최상위 레벨 또는 커스텀 훅에서만 호출이 가능.

    사용법

    1. 컴포넌트의 렌더링 건너뛰기

    function ProductPage({ productId, referrer, theme }) {
      // ...
      return (
        <div className={theme}>
          <ShippingForm onSubmit={handleSubmit} />
        </div>
      );
    import { memo } from 'react';
    
    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      // ...
    });

    위와 같은 상황에서 JavaScript에서 function () {} 또는 () => {}는 {} 객체 리터럴이 항상 새 객체를 생성하는 것과 유사하게 항상 다른 함수를 생성하기 때문에 ShippingForm의 props는 결코 동일하지 않으며 memo최적화가 작동하지 않는다.
    따라서 우리는 아래와 같이 handleSubmit 함수를 변경 시켜줄 필요가 있다.

    const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]);

    위 처럼 함수를 useCallback 으로 감싸면 리렌더링 사이에 동일한 함수가 되도록 할수 있다.

    예시는 리액트 공식문서에 잘 나와있다.

    2. 메모된 콜백에서 state 업데이트하기

    일반적으로 메모화된 함수는 가능한 적은 의존성을 갖기원하기 때문에 업데이터 함수를 전달하여 의존성을 제거할 수 있다.

    function TodoList() {
      const [todos, setTodos] = useState([]);
    
      const handleAddTodo = useCallback((text) => {
        const newTodo = { id: nextId++, text };
        setTodos([...todos, newTodo]);
      }, [todos]);
      // ...
    
    function TodoList() {
      const [todos, setTodos] = useState([]);
    
      const handleAddTodo = useCallback((text) => {
        const newTodo = { id: nextId++, text };
        setTodos(todos => [...todos, newTodo]);
      }, []); // ✅ No need for the todos dependency 
              // ✅ todos에 대한 의존성이 필요하지 않음
      // ...

    3. Effect가 너무 자주 발동되지 않도록 하기

    function ChatRoom({ roomId }) {
      const [message, setMessage] = useState('');
    
      function createOptions() {
        return {
          serverUrl: 'https://localhost:1234',
          roomId: roomId
        };
      }
    
      useEffect(() => {
        const options = createOptions();
        const connection = createConnection();
        return () => connection.disconnect();
      }, [createOptions]);

    위와 같이 useEffect에 의존성을 함수로 선언해주면 렌더링시마다 의존성이 변경되기 때문에 문제가 발생한다.
    위와 같은 경우에도 createOptions를 useCallback 으로 감싸주면 해결이 된다.

    const createOptions = useCallback(() => {
        return {
          serverUrl: 'https://localhost:1234',
          roomId: roomId
        };
      }, [roomId]);

    이렇게 감싸주면 roomId가 변경되었을 때에만 함수가 변경되게 된다.
    하지만 가장 좋은 방법은 createOptions 함수 자체를 useEffect 안으로 넣는 방법이다.
    그러면 함수가 계속 변경될 일도 없고 useCallback 도 사용할 필요가 없기 때문에 best다!

    4. 커스텀 훅 최적화하기

    커스텀 훅을 작성하는 경우 반환하는 모든 함수를 useCallback으로 감싸는 것이 좋다.

    function useRouter() {
      const { dispatch } = useContext(RouterStateContext);
    
      const navigate = useCallback((url) => {
        dispatch({ type: 'navigate', url });
      }, [dispatch]);
    
      const goBack = useCallback(() => {
        dispatch({ type: 'back' });
      }, [dispatch]);
    
      return {
        navigate,
        goBack,
      };
    }

    이렇게 작성해 놓으면 훅의 소비자가 필요할 때 자신의 코드를 최적화할 수 있다.

    그래서 useMemo 와 useCallback의 차이가 뭐야?

    둘의 차이점은 캐시 할 수 있는 항목에 있다.
    useMemo는 호출한 함수의 결과를 캐시하고 useCallback은 함수자체를 캐시한다.

    import { useMemo, useCallback } from 'react';
    
    function ProductPage({ productId, referrer }) {
      const product = useData('/product/' + productId);
    
      const requirements = useMemo(() => { 
         // 함수를 호출하고 그 결과를 캐시합니다.
        return computeRequirements(product);
      }, [product]);
    
      const handleSubmit = useCallback((orderDetails) => { 
         // 함수 자체를 캐시합니다.
        post('/product/' + productId + '/buy', {
          referrer,
          orderDetails,
        });
      }, [productId, referrer]);
    
      return (
        <div className={theme}>
          <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
        </div>
      );
    }

    useMemo는 위의 예제에서 product가 변경되지 않는 한 computeRequirements를 호출한 결과를 캐시한다. 이렇게 하면 ShippingForm을 리렌더링할 필요 없이 requirements를 전달할 수 있다.

    useCallback은 위의 예제에서 제공한 함수를 호출하지 않는다. 대신 제공한 함수를 캐시하여 handleSubmit 자체가 변경되지 않도록 한다. 이렇게 하면 ShippingForm을 리렌더링할 필요없이 handleSubmit 함수를 전달할 수 있다.

    Share article

    석우의 개발블로그

    RSS·Powered by Inblog