Front/React

최적화 - React 배우기

oodada 2024. 3. 18. 00:00

최적화

리액트에서의 최적화 기법은 불필요한 연산을 줄여 성능을 향상시키는 것입니다.
리액트 공식 문서에서는 성능 최적화를 위한 방법을 다음과 같이 소개합니다.

최적화 방법

  • 코드, 폰트, 이미지 등의 리소스를 압축합니다.
  • 메모이제이션을 이용하여 불필요한 연산을 줄입니다.

메모이제이션

메모이제이션이란 연산의 결과를 기억했다가 필요할 때 사용함으로써 불필요한 연산을 방지하는 것입니다.

  • useMemo : 함수의 불필요한 재실행을 방지합니다.
  • React.memo : 컴포넌트의 불필요한 리렌더링을 방지합니다.
  • useCallback : 함수의 불필요한 재생성을 방지합니다.

useMemo 사용하기

문법 : useMemo(() => 연산 결과, [의존성 배열])

useMemo는 React에서 값을 메모이제이션(caching)하여 성능을 최적화하기 위한 훅입니다.

메모(캐싱)된 값은 의존성 배열에 있는 값이 변경될 때만 다시 계산되므로 복잡한 연산을 계속 반복하지 않고, 이전에 계산한 값을 재사용할 수 있습니다.

- filteredTodo 함수 최적화하기

할 일 관리 앱 만들기 글의 예제를 기반으로 최적화를 진행해보겠습니다.

아래 예제에서 filteredTodo 함수가 빈번하게 호출되어 불필요한 연산이 발생합니다. 테스트를 위해 filteredTodo 함수에 로깅을 추가하고, 콘솔을 확인해보시면 빈번한 호출이 발생하는 것을 확인할 수 있습니다.

// src/components/TodoList.js
(...)

function TodoList() {
    (...)

    const filteredTodo = () => {
        return todos.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()));
    };

    // lookBack 함수가 빈번하게 호출됩니다.
    const lookBack = () => {
        console.log('lookBack')
        const total = todos.length
        const done = todos.filter((todo) => todo.isDone).length
        const left = total - done

        return { total, done, left }
    }

    return (
        <div>
            <h3>할 일 목록 📃</h3>
            <input type='text' placeholder="검색어를 입력하세요" onChange={onChangeSearch} value={search} />

            <div>
                {filteredTodo().map((item) => (
                    <TodoItem key={item.id} {...item} />
                ))}
            </div>

            <div>
                {lookBack().total}개 중에 {lookBack().done}개 완료, {lookBack().left}개 남음
            </div>
        </div>
    );
}

export default TodoList;

useMemo를 이용하여 할 일 목록 렌더링 최적화하기

useMemo를 이용하여 filteredTodo 함수의 결과를 기억합니다. useMemo를 사용함으로써 filteredTodo는 함수가 아니라 해당 값을 계산한 결과인 배열을 반환합니다.

// src/component/TodoList.js
import React, { useState, useMemo } from 'react'
import TodoItem from './TodoItem'

export default function TodoList({ todo, onUpdate, onDelete }) {
    (...)

    // useMemo를 이용하여 filteredTodo 함수의 결과를 기억합니다.
    // useMemo를 사용함으로써 filteredTodo는  함수가 아니라 해당 값을 계산한 결과인 배열을 반환합니다.
    const filteredTodo = useMemo(() => {
        return todo.filter((item) => item.task.toLowerCase().includes(search.toLowerCase()));
    }, [search, todo]); // 의존성 배열에 search와 todo를 추가합니다.

    // 최적화 테스트
    const lookBack = useMemo(() => {
        console.log('lookBack')
        const total = todos.length
        const done = todos.filter((item) => item.isDone).length
        const left = total - done

        return { total, done, left }
    }, [todos]) // 의존성 배열에 todo를 추가합니다.

    return (
        <div>
            (...)
            <ul>
            // filteredTodo useMemo를 적용한 경우, filteredTodo 자체가 함수가 아니라 해당 값을 계산한 결과인 객체를 반환하기 때문에 ()를 제거합니다.
                {filteredTodo.map((item) => (
                    <TodoItem key={item.id} onUpdate={onUpdate} onDelete={onDelete} {...item} />
                ))}
            </ul>

            <div>
                // lookBack에 useMemo를 적용한 경우, lookBack 자체가 함수가 아니라 해당 값을 계산한 결과인 객체를 반환하기 때문에 ()를 제거합니다.
                {lookBack.total}개 중에 {lookBack.done}개 완료, {lookBack.left}개 남음
            </div>
        </div>
    )
}

useCallback 사용하기

문법 : useCallback(() => 함수, [의존성 배열])

- useCallback가 필요한 상황

  1. 자식 컴포넌트로 함수를 전달해야 할 때

부모 컴포넌트가 리렌더링되면 함수가 새로 생성되어 자식 컴포넌트가 다시 렌더링될 수 있다.
useCallback을 사용하면 함수 참조값을 유지하여 이러한 문제를 방지.

  1. 함수가 자주 생성되어 성능 문제가 발생할 때

복잡한 연산을 포함한 함수가 렌더링마다 새로 생성되면 성능에 영향을 줄 수 있다.
useCallback으로 해당 함수를 캐싱하여 성능 문제를 해결.

- onChangeSearch

// src/component/TodoList.js
const onChangeSearch = e => {
    setSearch(e.target.value);
};

useCallback을 이용하여 onChangeSearch 함수를 기억하고, 의존성 배열에 추가합니다.

// src/component/TodoList.js
const onChangeSearch = useCallback(e => {
        setSearch(e.target.value);
    }, []);

- onUpdate, onDelete

함수 재생성 문제

onUpdateonDelete를 그대로 TodoItem에 전달하면, TodoList가 리렌더링될 때마다 새로운 함수 참조가 생성돼 모든 TodoItem이 다시 렌더링된다.

React는 컴포넌트가 받는 props가 변경되었는지 확인할 때, 객체나 함수는 참조값이 변경되었는지를 기준으로 비교하기 때문에, 같은 로직이라도 새롭게 생성된 함수는 다른 것으로 간주돼 TodoItem이 리렌더링된다.

useCallback을 이용한 함수 재사용

useCallback으로 handleUpdatehandleDelete를 생성하면, 의존성 배열([onUpdate, onDelete])이 변경되지 않는 한 항상 같은 참조값을 가진 함수를 사용한다. 따라서 React.memo로 감싼 TodoItem은 불필요한 리렌더링을 방지할 수 있다.

// src/components/TodoList.js
<TodoItem key={todo.id} onUpdate={onUpdate} onDelete={onDelete} {...todo} />
// src/components/TodoList.js
const handleUpdate = useCallback((id) => onUpdate(id), [onUpdate]);
const handleDelete = useCallback((id) => onDelete(id), [onDelete]);

<TodoItem
  key={todo.id}
  onUpdate={() => handleUpdate(todo.id)}
  onDelete={() => handleDelete(todo.id)}
  {...todo}
/>

- useMemo와 useCallback의 차이점

  • useMemo: 값을 기억하여 불필요한 연산을 방지합니다.
  • useCallback: 함수를 기억하여 불필요한 재생성을 방지합니다.

filterTodo 함수는 값이 변경될 때마다 새로 계산되어야 하므로 useMemo를 사용하고, onChangeSearch 함수는 값이 변경되지 않으므로 useCallback을 사용합니다.

React.memo 사용하기

문법 : React.memo(컴포넌트)

- React.memo를 이용하여 컴포넌트 최적화하기

props가 변경되지 않으면 리렌더링을 방지하는 React.memo를 사용하여 컴포넌트를 최적화합니다.

React.memo를 사용하여 동일한 props가 전달되면 컴포넌트 리렌더링을 방지합니다.

// src/components/TodoItem.js
import React from 'react';

// React.memo를 사용하여 동일한 props가 전달되면 컴포넌트 리렌더링을 방지.
const TodoItem = React.memo(({ id, isDone, task, createdDate, onUpdate, onDelete }) => {
  console.log(`TodoItem ${task}: isDone = ${isDone}`); // 추가

  return (
    <div>
      <li key={id}>
        <input
          type="checkbox"
          checked={isDone}
          onChange={onUpdate} // 부모에서 전달받은 핸들러 호출
        />
        <span className={`${isDone ? 'line-through text-gray-400' : 'no-underline text-black'}`}>
          {task}
        </span>
        <span>{new Date(createdDate).toLocaleDateString()}</span>
        <button onClick={() => onDelete(id)}>삭제</button>
      </li>
    </div>
  );
});

// displayName 추가
TodoItem.displayName = 'TodoItem';

export default TodoItem;
티스토리 친구하기