Component Performance Optimization

nara_lee·2025년 3월 12일
0
post-thumbnail

useCallback 과 React.memo 의 차이

In React, both React.memo and useCallback are related to optimizing performance, but they serve different purposes and are used in different contexts.

  1. React.memo:

    • Purpose: It is a higher order component (HOC: Component that wraps a component) for memoizing a functional component. React.memo is used to prevent a functional component from re-rendering unless its props have changed.
    • Usage: You wrap your functional component with React.memo when you want to ensure that the component only re-renders when its props change. This is particularly useful when rendering expensive components that rely on props that often remain the same.
    • Example:
      const MyComponent = React.memo(function MyComponent(props) {
        // component implementation
      });
  2. useCallback:

    • Purpose: This hook returns a memoized version of the callback function that only changes if one of the dependencies has changed. It’s useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
    • Usage: You use useCallback to memoize callback functions in your component. This is helpful when passing callbacks as props to highly optimized child components, or when a callback is used in dependency arrays for other hooks like useEffect or useMemo.
    • Example:
      const memoizedCallback = useCallback(() => {
        doSomething(a, b);
      }, [a, b]);

Key Difference:

  • React.memo is about preventing re-renders of the component itself based on props changes.
  • useCallback is about memoizing a callback function so it doesn’t cause re-renders of the components that use it as a dependency or prop due to reference changes.

Both tools help in optimizing performance but target different aspects of the rendering behavior.

Preventing Functions from Being Repeatedly Created

Option 1: Functional Update of useState

const [number, setNumber] = useState(0);

// prevNumber refers to the current number state's value
const onIncrease = useCallback(
	() => setNumber(prevNumber => prevNumber +1),
    [], 
);

useState의 가장 기본적인 사용법은 전에 배웠 듯 state 값을 직접 넣는 것이다.

setNumber(number+1)

다만 이렇게 하면 최신 상태의 number를 참조하는 함수들이 number가 바뀔 때마다 함수를 새로 생성하게된다.

state 값을 직접 넣지 않고 함수형으로 쓰면 이 현상을 방지할 수 있다.

예시

setTodos 는 useState 에서 지정한 함수이다.

// BEFORE
setTodos(todos.concat(todo));
setTodos(todos.filter((todo) => todo.id !== id));
setTodos(
  todos.map((todo) => todo.id === id ? { ...todo, checked: !todo.checked } : todo));
// BEFORE
setTodos(todos => todos.concat(todo));
setTodos(todos => todos.filter((todo) => todo.id !== id));
setTodos(todos => 
  todos.map((todo) => todo.id === id ? { ...todo, checked: !todo.checked } : todo));

setTodos 사용할 때 그 안에 todos => 만 붙혀주면 된다

Option 2: useReducer 사용하기

import React, { useReducer, useRef, useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';

function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: `할 일 ${i}`,
      checked: false,
    });
  }
  return array;
}

function todoReducer(todos, action) {
  switch (action.type) {
    case 'INSERT': // 새로 추가
      // { type: 'INSERT', todo: { id: 1, text: 'todo', checked: false } }
      return todos.concat(action.todo);
    case 'REMOVE': // 제거
      // { type: 'REMOVE', id: 1 }
      return todos.filter(todo => todo.id !== action.id);
    case 'TOGGLE': // 토글
      // { type: 'REMOVE', id: 1 }
      return todos.map(todo =>
        todo.id === action.id ? { ...todo, checked: !todo.checked } : todo,
      );
    default:
      return todos;
  }
}

const App = () => {
  const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);

  // 고유 값으로 사용 될 id
  // ref 를 사용하여 변수 담기
  const nextId = useRef(2501);

  const onInsert = useCallback(text => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    dispatch({ type: 'INSERT', todo });
    nextId.current += 1; // nextId 1 씩 더하기
  }, []);

  const onRemove = useCallback(id => {
    dispatch({ type: 'REMOVE', id });
  }, []);

  const onToggle = useCallback(id => {
    dispatch({ type: 'TOGGLE', id });
  }, []);

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
    </TodoTemplate>
  );
};

export default App;
const [todos, dispatch] = useReducer(todoReducer, undefined, createBulkTodos);
  • 원래 useReducer를 사용할 때는 두 번째 파라미터에 초기 상태를 넣어준다.
  • 여기서는 대신 undefined를 넣고 세 번째 파라미터에 초기상태를 만들어주는 함수인 createBulkTodos를 써줌으로써 컴포넌트가 처음 렌더링 될 때만 createBulkTodos 가 호출된다

RECALL useReducer

불변성의 중요성

const array = [1,2,3,4,5]
const nextArrayBad = array //same address
const nextArrayGood = [...array] //different address

근데 여기서 주의할 점은 ... 스프레드 연산자를 쓰면 객체나 배열 내부의 값이 shallow copy로 복사된다. 즉 가장 바깥 쪽에 있는 값만 복사된다.

따라서 object/array 안에 nested 된 object/array까지 불번성 유지하는게 어렵다.

immer library 사용!

어떨 때 리렌더링이 발생했더라?

💡 RECALL
4 Things that trigger UPDATE
props changed
state changed
parent component re-rendered
force render by this.forceUpdate

React.memo(TodoList)

위에 코드는 프로젝트 성능에 전혀 영향을 주지 않는다! 왜냐 TodoList 컴포넌트의 부모컴포넌트인 App 컴포넌트가 리렌더링되는 유일한 이유가 todo 배열이 업데이트될 때이기 때문. 즉 지금 TodoList 컴포넌트는 불필요한 리렌더링이 발생하지 않는다.

하지만 App 컴포넌트에 다른 state가 추가되어 해당 값들이 업데이트될 때는 TodoList 컴포넌트가 불필요한 리렌더링을 할 수도 있음. 한마디로 미리 대비한거임.

react-virtualized (npm library)

list 컴포넌트에서 스크롤되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게끔 하는 라이브러리

FlatList of ReactNative


본 후기는 [한글과컴퓨터x한국생산성본부x스나이퍼팩토리] 한컴 AI 아카데미 (B-log) 리뷰로 작성 되었습니다.

#한컴AI아카데미 #AI개발자 #AI개발자교육 #한글과컴퓨터 #한국생산성본부 #스나이퍼팩토리 #부트캠프 #AI전문가양성 #개발자교육 #개발자취업

0개의 댓글