[React] useCallback - 불 필요한 함수 재생성 방지하기

Ahnzi·2024년 11월 28일

React의 memo 메서드는 특정 컴포넌트의 props가 변경되었는지를 얕은 비교로 판단합니다. 따라서, 객체 타입의 값을 props로 전달할 경우, 제대로 최적화되지 않을 수 있습니다. 이를 해결하기 위해 추가적인 콜백 함수를 전달하여, props를 명시적으로 비교해줘야 합니다.

또한, props의 이름이 변경되면 콜백 함수 내에서 비교하던 props 이름도 모두 수정해야 하며, 새로운 props가 추가되면 그에 따른 조건도 새롭게 작성해야 합니다.

따라서, 객체 타입의 값을 처음부터 다시 생성되지 않도록 최적화하는 것이 더 나은 방법이 될 수 있습니다. 이를 가능하게 해주는 것이 바로 useCallback이라는 React Hook입니다.


useCallback 사용하기

우선 App 컴포넌트 상단에 useCallback React Hook을 불러옵니다.

import { useState, useRef, useReducer, useCallback } from "react";

useCallback React Hook을 호출할 때, 첫 번째 인수로는 재생성을 방지하고 싶은 함수를, 두 번째 인수로는 의존성 배열(deps)을 전달합니다.

useCallback은 기본적으로 첫 번째 인수로 전달된 콜백 함수를 그대로 반환하며, deps가 변경될 때만 해당 함수를 다시 생성하도록 최적화합니다.

바꿔 말하면, useCallback은 함수를 메모이제이션해주는 역할을 합니다. 따라서, 의존성 배열(deps)을 빈 배열로 설정하면, App 컴포넌트가 최초로 렌더링될 때(Mount 시)에만 useCallback Hook이 호출되어 함수를 생성합니다. 이후, 리렌더링이 아무리 많이 발생하더라도 해당 함수는 새롭게 생성되지 않습니다.

이런 점을 활용하면 onDeleteonUpdateonCreate 와 같은 함수들을 다시 생성하지 않도록 최적화할 수 있습니다.

먼저, onDelete 함수를 useCallback을 사용하여 최적화해 보겠습니다.

...

const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }, []);

...

onDelete 함수는 useCallback에 의해 Mount 시 딱 한 번 생성되며, 이후에는 리렌더링이 아무리 자주 발생하더라도 다시 생성되지 않도록 최적화되었습니다.

나머지 함수들도 useCallback을 이용하여 최적화시켜줍니다.

...

const onCreate = useCallback((content) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      },
    });
  }, []);

  const onUpdate = useCallback((targetId) => {
    dispatch({
      type: "UPDATE",
      targetId: targetId,
    });
  }, []);

  const onDelete = useCallback((targetId) => {
    dispatch({
      type: "DELETE",
      targetId: targetId,
    });
  }, []);

...

이제 TodoItem 컴포넌트에 콜백 함수를 memo 메서드에 전달하지 않아도, onUpdate와 onDelete 함수가 다시 생성되지 않을 것입니다.

import { memo } from "react";
import "./TodoItem.css";

const TodoItem = ({ id, isDone, content, date, onUpdate, onDelete }) => {
  const onChangeCheckbox = () => {
    onUpdate(id);
  };

  const onClickDeleteButton = () => {
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <input type="checkbox" checked={isDone} onChange={onChangeCheckbox} />
      <div className="content">{content}</div>
      <div className="date">{new Date(date).toLocaleDateString()}</div>
      <button onClick={onClickDeleteButton}>삭제</button>
    </div>
  );
};

// export default memo(TodoItem, (prevProps, nextProps) => {
// if (prevProps.id !== nextProps.id) return false;
// if (prevProps.isDone !== nextProps.isDone) return false;
// if (prevProps.content !== nextProps.content) return false;
// if (prevProps.date !== nextProps.date) return false;

// return true;
// });

export default memo(TodoItem);

최적하는 언제하는가? 어떤것을 하는가?

대표적인 세 가지 최적화 기능에 대해 살펴보았는데, 최적화는 언제 해야 하고 무엇을 최적화해야 하는가가 매우 중요한 기준이 될 수 있습니다. 왜냐하면, 최적화를 너무 이른 시점에 하거나 너무 많은 부분을 최적화하려고 하면 오히려 문제가 발생할 수 있기 때문입니다.

1. 언제 최적화를 하면 좋을까?

그렇다면 최적화를 언제 하면 좋을까요? 보통 React 앱의 최적화는 프로젝트가 거의 완성된 상태에서 진행하는 것이 일반적입니다. 항상 기능 구현이 최우선이며, 기능이 완성된 후에 최적화를 진행하는 것이 바람직합니다.

이는 최적화를 위해 useCallback과 같은 React Hook을 미리 적용해 두면, 나중에 새로운 기능을 추가하거나 기존 기능을 수정할 때 최적화가 풀리거나 문제가 발생할 수 있기 때문입니다.

따라서 기능 구현을 먼저 완료한 뒤, 마지막 단계에서 최적화를 진행하는 것을 권장합니다.

2. 어떤 것들이 최적화의 대상이 되어야 하는가?

모든 컴포넌트에 최적화를 적용해서는 안 되며, 꼭 최적화가 필요한 연산, 함수, 또는 컴포넌트에만 최적화를 적용하는 것이 좋습니다.

예를 들어, Header 컴포넌트처럼 매우 단순한 컴포넌트에 React.memo 메서드를 적용한다고 가정해 봅시다. memo와 같은 최적화 메서드는 자체적으로 연산이 필요합니다. 예를 들어, props 값을 비교하거나, 메모이제이션을 위해 메모리에 컴포넌트의 결과값을 저장하는 등의 작업을 수행합니다.

하지만 최적화하려는 컴포넌트가 단순히 별다른 동작 없이 UI만 렌더링하는 컴포넌트라면, 해당 컴포넌트를 리렌더링하는 것이 오히려 더 빠르거나, 거의 차이가 없을 수 있습니다.

따라서, 사소한 컴포넌트까지 모두 최적화하려고 하지 말고, ToDoItem 컴포넌트처럼 사용자의 행동에 따라 개수가 많아질 가능성이 있는 컴포넌트나, 많은 함수와 복잡한 로직을 포함해 코드가 무거운 컴포넌트에 한해서만 최적화를 적용하는 것이 훨씬 효과적입니다.

결론적으로, React 앱의 최적화는 반드시 필요한 경우에만, 최적화가 꼭 필요한 컴포넌트나 로직에 한해서 적용하는 것이 가장 효율적입니다. 그래서 최적화는 가장 마지막에 하는 것이 좋습니다.

profile
운동하는 개발자 Ahnzi 입니다.

0개의 댓글