본문으로 바로가기

[REACT] useMemo로 랜더링 최적화 하기

category 나의 주니어 개발 일기/REACT 2025. 7. 14. 14:29
728x90
반응형
SMALL

useMemo로 랜더링 최적화 하기

메모이제이션 기법을 기반으로 불 필요한 연산을 최적화 하는 리액트 훅

  useMemo(() => {}, [])

두번재 인자인 [] 안에 들어간 값이 변경될때마다 첫번째 인자인 ()=>{}콜백함수가 실행된다.


const getAnalyzedData = () => {
  const totalCount = todos.length;
  const doneCount = todos.filter((todo) => todo.isDone).length;
  const notDoneCount = totalCount-doneCount;

  return {
    totalCount,
    doneCount,
    notDoneCount
  }
};

const {totalCount, doneCount, notDoneCount} = getAnalyzedData();

after

const {totalCount, doneCount, notDoneCount} = useMemo(() => {
  const totalCount = todos.length;
  const doneCount = todos.filter((todo) => todo.isDone).length;
  const notDoneCount = totalCount-doneCount;

  return {
    totalCount,
    doneCount,
    notDoneCount
  };
}, [todos]);

before는 메인 컴포넌트가 랜더링 될때마다 같이 리랜더링 된다.

그러나 after 변경함으로서 최초에 한번만 리랜더링 되고 todos에 관한것만 변경될때만 랜더링되기 때문에 메모리적으로 효율적이다.(랜더링 최적화)

React.memo

불필요한 리렌더링 방지가 된다.


(그림 출처: https://www.inflearn.com/courses/lecture?courseId=328340&tab=curriculum&type=LECTURE&unitId=103533&subtitleLanguage=ko)


memo 사용 전 기존의 코드

function App(){
  ...
  ...
  return (
    <div className='App'>
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

기존에는 TodoItem을 랜더링하면 모든 요소가 리랜더링 된다.

Header 같은 경우 리랜더링 될 필요도 없다.

리랜더링이 필요 없는것들은 이전 값을 그대로 사용하는 memo를 사용할 것이다.

그러면 리랜더링이 되지 않게 되므로 최적화가 가능하게 된다.

최적화 전 Header.jsx

import './Header.css'

const Header = () => {
    return (
    <div className="Header">
      <h3>오늘은 🗓️</h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
    );
};

최적화 후 Header.jsx

import './Header.css'
import { memo } from 'react';

const Header = () => {
    return (
    <div className="Header">
      <h3>오늘은 🗓️</h3>
      <h1>{new Date().toDateString()}</h1>
    </div>
    );
};

export default memo(Header);

Header 부분은 이제 더이상 리랜더링이 되지 않는것을 확인 할 수 있다.


TodoItem 부분도 최적화하고 싶다.

TodoItem.jsx

import './TodoItem.css'
import {memo} from 'react'

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

  const onChangeCheckbox = () => {
    onUpdate(id);
  };

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

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

export default memo(TodoItem);

이렇게 적용을 했지만 TodoItem 끼리는 최적화되지 않는다 왜일까?


memo는 props 가 변경되었을때 이전 값과 현재 값을 비교해서 변경된것이 없다면 그대로 있고 있다면, 리랜더링한다.

props는 이렇게 존재하고 체크박스를 체크하고 해제하는 isDone 값만 변경될 뿐이다. 그런데 왜 전체 TodoItem이 리랜더링 될까?

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

export default memo(TodoItem); 이렇게 memo를 사용했을때 TodoItem 의 props 값이 객체값(onUpdate, onDelete)이면 얕은 비교를 하게된다.

때문에 memo는 props가 바뀌었다고 판단을 하게된다. 새로운 todo를 수정을 하던 삭제를 하던 결국 onUpdate나 onDelete 같은 객체들은 얕은 비교를하기 때문에 할때마다 다른값이라고 판단을 하게되고 memo를 적용했다고 하더라도 TodoItem은 계속 리랜더링 될것이다.

객체타입을 props로 받고있는 컴포넌트는 memo 를 적용해도 최적화가 적용이 안되기 때문에 다른 방법이 필요하다.

첫번째 방법

callback 함수를 적용해서 커스터마이징 하여 최적화를 진행하자.

TodoItem.jsx

...
...
export default memo(TodoItem, (prevProps, nextProps) => {
    //반환값에 따라, Props가 바뀌었는지 안바뀌었는지 판단
    //T -> Props가 바뀌지 않음 -> 리랜더링 x
    //F -> Props 바뀜 -> 리랜더링 o
    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;
});

memo 메서드는 부모 컴포넌트가 리랜더링 될때마다 props가 바뀌엇는지 callback함수의 리턴값으로 판단하게된다.

App.jsx

function App() {

  const [todos, setTodos] = useState(mockData);
  const idRef = useRef("3");

  const onCreate = (content) => {
      const newTodo = {
        id: idRef.current++,
        isDone: false,
        content: content,
        date: new Date().getTime(),
      };

      setTodos([newTodo, ...todos]);
  }

  const onUpdate = (targetId) => {
      setTodos(todos.map((todo) => todo.id===targetId
      ? {...todo, isDone:!todo.isDone}
      : todo));
  }


  const onDelete = (targetId) => {
    setTodos(todos.filter((todo) =>todo.id !== targetId));
  }

  return (
    <div className='App'>
      <Header />
      <Editor onCreate={onCreate} />
      <List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

List.jsx

const List = ({todos, onUpdate, onDelete }) => {
    ...
    ...

    return (
    <div className="List"> 
        <h4>Todo List🌱🌱</h4>
        <input value={search}
               onChange={onChangeSearch}
               placeholder="검색어를 입력하세요"/>

        <div className="todos_wrapper">
          {filteredTodos.map((todo) => {
            return <TodoItem key={todo.id} {...todo}
             onUpdate={onUpdate}
             onDelete={onDelete} />;
          })}
        </div>

    </div>
    );
};
728x90
반응형
LIST