[LG CNS AM Inspire Camp 1기] React (7) - [성능 최적화] React.memo & useCallback

정성엽·2025년 1월 7일
0

LG CNS AM Inspire 1기

목록 보기
17/53
post-thumbnail

INTRO

이번 포스팅에서는 성능 문제를 해결하기 위한 두 가지 도구인 React.memo와 useCallback 훅을 자세히 살펴보려고 한다.

React.memo는 컴포넌트 레벨에서의 최적화를, useCallback은 함수의 메모이제이션을 통한 최적화를 가능하게 한다.

이제부터 어떤 방식으로 동작하는지 살펴보자 👀


1. React.memo

React.memo는 고차 컴포넌트(HOC)로, 컴포넌트가 동일한 props로 동일한 결과를 렌더링한다면 메모이제이션을 통해 리렌더링을 방지한다.

즉, props가 변경되지 않았다면 메모이징된 내용을 재사용하여 불필요한 리렌더링을 방지한다.

💡 React.memo로 최적화하기 - (1)

React.memo의 사용방법은 예시코드와 함께 살펴보려고 한다.

우선 부모 컴포넌트의 상태가 변경될 때마다 자식 컴포넌트가 불필요하게 리렌더링되는 상황을 만들어보자

Sample Code

import { useState } from "react";

const Todos = ({ todos, addTodo }) => {
  console.log("Child component is rendering...");
  return (
    <div>
      <button onClick={addTodo}>Add Todo</button>
      <h2>Todos</h2>
      {todos.map((todo, idx) => (
        <p key={idx}>{todo}</p>
      ))}
    </div>
  );
};

export default function App() {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => setCount(count + 1);
  const addTodo = () => {
    setTodos([...todos, "할일"]);
  };

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <div>
        <button onClick={increment}>카운트 증가</button>
        <h2>Count: {count}</h2>
      </div>
    </>
  );
}

Console View

위 예제는 부모 컴포넌트(App)에서 카운트와 Todo 상태 변수를 모두 관리하고 있다.

따라서, 상태 변경 로직도 부모 컴포넌트에서 정의하고 해당 로직을 자식 컴포넌트에게 props로 넘겨준다.

액션이 발생하면 부모 컴포넌트가 리렌더링되므로 자식 컴포넌트도 리렌더링이 발생하는 상황이다.

이를 React.memo로 최적화해보자!

💡 React.memo로 최적화하기 - (2)

React.memo는 컴포넌트 자체를 감싸버리면 된다.

리팩토링한 코드는 다음과 같다.

Sample Code

...

const Todos = React.memo(({ todos, addTodo }) => {
  console.log("Child component is rendering...");
  return (
    <div>
      <button onClick={addTodo}>Add Todo</button>
      <h2>Todos</h2>
      {todos.map((todo, idx) => (
        <p key={idx}>{todo}</p>
      ))}
    </div>
  );
});

...

Console View

React.memo로 Todos 컴포넌트를 감싸주었지만 여전히 부모 컴포넌트의 count 상태가 변경될 때마다 자식 컴포넌트가 리렌더링되는 문제가 발생한다.

이런 현상이 발생하는 이유는 부모 컴포넌트가 리렌더링될 때마다 addTodo 함수가 새로 생성되기 때문이다.

React.memo는 props의 변경을 감지하여 리렌더링을 결정하는데, 매번 새로운 함수가 생성되어 전달되므로 props가 변경된 것으로 인식한다.

즉, todos 값은 동일하더라도 addTodo 함수가 새로 생성되어 props의 변화가 감지되는 것이다.

이러한 문제를 해결하기 위해 함수의 재생성을 방지할 수 있는 useCallback 훅이 필요하다.


2. useCallback

useCallback은 useMemo와 유사하게 메모이제이션을 활용하는 훅이지만, 특별히 콜백 함수에 특화되어 있다.

이는 함수가 불필요하게 재생성되는 것을 방지하는데, 특히 자식 컴포넌트의 props로 함수를 전달할 때 발생하는 불필요한 리렌더링을 최적화하는데 매우 효과적이다.

💡 useCallback 기본 구조

useCallback도 useEffect와 마찬가지로 의존성 배열을 listening한다.

구조는 다음과 같다.

useCallback 구조

const memoizedCallback = useCallback(() => {
    doSomething(dependency);
}, [dependency]);

이처럼 useCallback도 콜백함수와 의존성 배열을 매개변수로 사용한다.

useCallback은 의존성 배열이 변경될 때마다 함수를 새롭게 정의한다.

이제 React.memo와 useCallback을 같이 사용하여 코드를 리팩토링해보자!

Sample Code

import React, { useCallback, useState } from "react";

const Todos = React.memo(({ todos, addTodo }) => {
  console.log("Child component is rendering...");
  return (
    <div>
      <button onClick={addTodo}>Add Todo</button>
      <h2>Todos</h2>
      {todos.map((todo, idx) => (
        <p key={idx}>{todo}</p>
      ))}
    </div>
  );
});

export default function App() {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => setCount(count + 1);
  const addTodo = useCallback(() => {
    setTodos([...todos, "할일"]);
  }, [todos]);

  return (
    <>
      <Todos todos={todos} addTodo={addTodo} />
      <div>
        <button onClick={increment}>카운트 증가</button>
        <h2>Count: {count}</h2>
      </div>
    </>
  );
}

Console View

코드에서 변경된 부분은 App 컴포넌트 내부에 정의된 addTodo 함수에 useCallback을 사용했다는 것이다.

여기서 알 수 있는 useMemo와의 차이점은 콜백 함수 자체 반환여부에 있다.

이전 포스팅에서 살펴봤다시피 useMemo의 경우 함수 자체를 반환하기 위해서는 return문에 함수를 정의해야했다.

즉, useMemo는 콜백함수의 return에 들어가는 값을 반환하는 방식으로 동작하고 useCallback은 콜백함수 그 자체를 반환하는 방식으로 동작한다.

콘솔 화면과 같이 count 상태가 변경되어도 Todos 컴포넌트는 리렌더링되지 않는다.

오직 todos가 변경될 때만 addTodo 함수가 새로 생성되고, 그에 따라 Todos 컴포넌트가 리렌더링된다.

이처럼 React.memo와 useCallback을 함께 사용하면 불필요한 리렌더링을 효과적으로 방지할 수 있다!


OUTRO

2편의 포스팅을 통해서 리액트의 주요 성능 최적화 도구인 useMemo, React.memo, useCallback에 대해 살펴봤다.

메모이제이션의 개념을 공유하는 useMemo와 useCallback은 비슷해 보이지만, useMemo는 계산된 값을, useCallback은 함수를 메모이제이션한다는 차이가 있다.

React.memo는 이러한 최적화를 컴포넌트 레벨에서 도와준다.

각 도구의 특성을 이해하고 적절한 상황에 활용한다면 불필요한 계산과 리렌더링을 효과적으로 제어할 수 있을 것이다.

다만 모든 곳에 메모이제이션을 적용하는 것은 오히려 성능을 저하시킬 수 있으니, 실제로 최적화가 필요한 상황에서만 사용하는 것이 좋다 👊


참고
HOC(고차 컴포넌트)가 무엇인가요?

profile
코린이

0개의 댓글

관련 채용 정보