코딩/리액트

[새싹 성동 2기] useMemo, useCallback, React.memo 활용법

insu90 2024. 12. 6. 18:08

React 애플리케이션에서 성능 최적화는 중요한 부분입니다. 컴포넌트가 자주 리렌더링되면 불필요한 계산이 반복되어 성능에 영향을 미칠 수 있습니다. 이를 방지하기 위해 useMemo, useCallback, React.memo와 같은 성능 최적화 도구를 활용할 수 있습니다. 이 글에서는 이 세 가지 방법을 다루며, 각 방법이 어떻게 성능을 최적화하는지 구체적인 예시와 함께 설명하겠습니다.

 

1. useMemo: 불필요한 연산 방지

useMemo는 값을 메모이제이션(memoization) 하여, 특정 값이 변경될 때만 재계산되도록 합니다. 주로 계산 비용이 높은 작업이나 렌더링 중 자주 호출되는 함수에 유용합니다.

import { useRef, useState, useMemo } from "react";

// Average 컴포넌트 정의
function Average() {
  // 상태 관리: 입력값과 숫자 목록을 관리
  const [number, setNumber] = useState(''); // 입력값을 관리
  const [list, setList] = useState([]); // 등록된 숫자들의 목록을 관리

  // 입력값을 상태에 반영하는 함수
  const changeNumber = e => setNumber(e.target.value);

  // 숫자를 등록하는 함수
  const changeList = () => {
    // 현재 입력값을 목록에 추가
    const newList = list.concat(number);
    setList(newList); // 목록을 업데이트
    setNumber(''); // 입력 필드를 초기화
    refNumber.current.focus(); // 입력 필드에 포커스 설정
  };

  // ref를 사용하여 입력 필드에 직접 접근
  const refNumber = useRef();

  // 평균값을 계산하는 함수 (비용이 큰 작업을 시뮬레이션)
  const getAverage = () => {
    console.log("평균값을 계산 중 입니다.");

    // 3초 동안 대기하는 코드 (평균 계산을 시뮬레이션)
    const wakeupTime = Date.now() + 3000;
    while (Date.now() < wakeupTime) { }

    // 목록이 비어있으면 0을 반환
    if (list.length === 0) return 0;

    // 목록의 모든 숫자를 합산하여 평균을 계산
    const total = list.reduce((prev, curr) => prev + Number(curr), 0);
    return total / list.length; // 평균값 반환
  };

  return (
    <>
      {/* 입력 필드: 숫자를 입력하고 onChange 이벤트로 상태를 변경 */}
      <input ref={refNumber} type="number" value={number} onChange={changeNumber} />
      
      {/* 등록 버튼: 숫자를 목록에 추가 */}
      <button onClick={changeList}>등록</button>
      
      <div>입력값: {number}</div>
      
      {/* 평균값 표시: useMemo 훅을 사용하여 list가 변경될 때만 평균값을 계산 */}
      <div>평균값: {useMemo(() => getAverage(), [list])}</div>

      <div>등록된 숫자들:</div>
      <ul>
        {/* 등록된 숫자들을 목록으로 표시 */}
        {list.map((v, i) => <li key={i}>{v}</li>)}
      </ul>
    </>
  );
}

// App 컴포넌트: Average 컴포넌트를 렌더링
const App = () => <Average />;

// App 컴포넌트를 내보내기
export default App;

useMemo를 사용하여 getAverage 함수가 list 배열이 변경될 때만 호출되도록 했습니다. 이로 인해 숫자를 입력할 때마다 평균값을 계산하는 대신, list가 변경되었을 때만 계산이 수행됩니다. 이를 통해 불필요한 렌더링과 계산을 방지할 수 있습니다.

 

2. useCallback: 불필요한 콜백 함수 재생성 방지

useCallback은 콜백 함수가 불필요하게 다시 생성되지 않도록 최적화하는 훅입니다. 이 훅은 주로 자식 컴포넌트에 콜백 함수를 전달할 때 유용합니다. 부모 컴포넌트가 리렌더링될 때마다 동일한 콜백 함수를 사용하도록 보장할 수 있습니다.

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

// Todos 컴포넌트는 `memo`로 래핑하여 불필요한 렌더링을 방지
const Todos = memo(({ todos, addTodo }) => {
  // 3초 동안 대기하는 코드 (성능 시뮬레이션)
  const wakeup = Date.now() + 3000;
  while (Date.now() < wakeup) { }

  // 렌더링 시마다 콘솔에 메시지를 출력
  return (
    <div>
      {console.log("Todo 컴포넌트가 렌더링...")}
      {/* Todo 항목을 추가하는 버튼 */}
      <button onClick={addTodo}>Add Todo</button>
      <h2>TODOS</h2>
      <ul>
        {/* todos 배열을 순회하여 항목을 목록으로 렌더링 */}
        {todos.map((todo, index) => <li key={index}>{todo}</li>)}
      </ul>
    </div>
  );
});

// App 컴포넌트 정의
const App = () => {
  // todos 상태: 할 일 목록을 관리
  const [todos, setTodos] = useState([]);
  
  // todos 상태를 업데이트하는 함수 (배열에 새로운 할 일을 추가)
  const changeTodos = useCallback(
    () => setTodos(prevTodos => [...prevTodos, "NEW ITEM " + prevTodos.length]), 
    [todos] // todos가 변경될 때마다 이 함수가 새로 생성되지 않도록 함
  );

  // count 상태: 카운트를 관리
  const [count, setCount] = useState(0);
  
  // 카운트를 증가시키는 함수
  const changeCount = () => setCount(count + 1);

  return (
    <>
      {/* 카운트를 증가시키는 버튼 */}
      <div>
        <button onClick={changeCount}>카운트 증가</button>
        <h2>카운트: {count}</h2>
      </div>
      <hr />
      {/* Todos 컴포넌트에 todos와 addTodo 함수 전달 */}
      <Todos todos={todos} addTodo={changeTodos} />
    </>
  );
};

export default App;

useCallback을 사용하여 changeTodos 함수가 불필요하게 다시 생성되지 않도록 했습니다. 부모 컴포넌트가 리렌더링될 때마다 동일한 changeTodos 함수가 자식 컴포넌트인 Todos에 전달되며, 이로 인해 Todos 컴포넌트의 불필요한 렌더링을 방지할 수 있습니다.

3. React.memo: 자식 컴포넌트 리렌더링 최적화

React.memo는 **props가 변경되지 않으면 리렌더링을 건너뛰는 고차 컴포넌트(HOC)**입니다. 부모 컴포넌트가 리렌더링되더라도, 자식 컴포넌트의 props가 변경되지 않으면 자식 컴포넌트는 리렌더링되지 않습니다.

 

import { useState, memo } from "react";

// Todos 컴포넌트는 `memo`로 래핑되어 불필요한 렌더링을 방지
const Todos = memo(({ todos, addTodo }) => {
  // 3초 동안 대기하는 코드 (성능 테스트나 대기 시뮬레이션을 위한 코드)
  const wakeup = Date.now() + 3000;
  while (Date.now() < wakeup) { }

  return (
    <div>
      {/* 콘솔에 메시지를 출력하여 `Todos` 컴포넌트가 렌더링될 때마다 확인 */}
      {console.log("Todo 컴포넌트가 렌더링...")}
      {/* 할 일 추가 버튼, 클릭 시 `addTodo` 함수가 호출되어 새로운 할 일이 추가됨 */}
      <button onClick={addTodo}>Add Todo</button>
      <h2>TODOS</h2>
      <ul>
        {/* `todos` 배열을 순회하여 각각의 할 일을 목록으로 표시 */}
        {todos.map((todo, index) => <li key={index}>{todo}</li>)}
      </ul>
    </div>
  );
});

// App 컴포넌트 정의
const App = () => {
  // todos 상태: 할 일 목록을 저장
  const [todos, setTodos] = useState([]);

  // `changeTodos` 함수는 `todos` 상태를 업데이트하여 새로운 할 일을 추가
  const changeTodos = () => setTodos(prevTodos => [...prevTodos, "NEW ITEM " + prevTodos.length]);

  // count 상태: 카운트 값을 저장
  const [count, setCount] = useState(0);

  // `changeCount` 함수는 카운트 값을 1씩 증가시키는 함수
  const changeCount = () => setCount(count + 1);

  return (
    <>
      {/* 카운트 증가 버튼 */}
      <div>
        <button onClick={changeCount}>카운트 증가</button>
        <h2>카운트: {count}</h2>
      </div>
      <hr />
      {/* `Todos` 컴포넌트에 `todos` 배열과 `addTodo` 함수를 props로 전달 */}
      <Todos todos={todos} addTodo={changeTodos} />
    </>
  );
};

export default App;

React.memo를 사용하여 Todos 컴포넌트를 메모이제이션 처리합니다. 이렇게 하면 부모 컴포넌트가 리렌더링되더라도 todos와 addTodo props가 변경되지 않으면 Todos 컴포넌트는 리렌더링되지 않습니다. 이로 인해 자식 컴포넌트의 불필요한 렌더링을 줄여 성능을 최적화할 수 있습니다.

 

요약입니다.

  • useMemo: 값이 변경되었을 때만 계산하도록 하여 불필요한 계산을 방지합니다.
  • useCallback: 콜백 함수가 불필요하게 다시 생성되지 않도록 하여 자식 컴포넌트에 전달되는 함수가 동일하게 유지됩니다.
  • React.memo: 자식 컴포넌트의 props가 변경되지 않으면 리렌더링을 건너뛰도록 합니다.

 

 

이 세 가지 방법을 적절히 활용하면 리액트 애플리케이션의 성능을 크게 개선할 수 있습니다. 성능 최적화는 필요할 때만 적용하는 것이 중요하며, 지나치게 최적화하려다 보면 코드 복잡도가 증가할 수 있으니 신중히 사용해야 합니다.

 

 

*생성형 AI 활용한 클라우드&보안 전문가 양성캠프 과정의 교육내용 정리 자료입니다.