5 minute read

React.memo 란?

예제

const MyComponent = React.memo(({ name }) => {
    return <div>Hello, {name}!</div>;
});

React.memo 과정
💡 위의 MyComponent 컴포넌트에서는 name이라는 props를 사용.
💡 React.memo는 이전에 렌더링된 결과를 name 값이 변경되었는지를 비교.
💡 변경이면 재랜더링 / 그대로면 재사용.

리스트 컴포넌트 최적화

필수개념
💡 리스트 컴포넌트를 사용한다면 리스트 내부에서 사용하는 컴포넌트도 최적화 해야한다.(권장)
💡 리스트는 반복적으로 생성되기 때문이다. (갱신이 계속 일어난다.)

ScheduleBoard.js

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

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

    return array;
}

const ScheduleBoard = () => {

    const [todos, setTodos] = useState(createBulkTodos);

    const nextId = useRef(todos.length);

    // 삽입
    // 갱신 되지지않지만 접근 방법을 자식컴포넌트에서 미리 useMemo 해놨다. 그래서 오버헤드는 없다.
    const onInsert = useCallback( (text) => {
        const temp = {
            id: nextId.current += 1
            , text: text
            , checked: false
        };
        setTodos(todos.concat(temp));
    }, []);

    // 삭제
    const onRemove = useCallback( (id) => {
        setTodos(todos => (todos.filter((todo) => todo.id !== id)));
    }, []);

    // 토글
    const onToggle = useCallback( (id) => {
        setTodos(todos => ( 
                            todos.map((todo) => 
                                todo.id === id ? { ...todo, checked: !todo.checked } : todo,
                            )
        ));
            
    }, []);
    
    return (
        <div>
            <h1>ScheduleBoard Page</h1>

            <TodoTemplate>
                <TodoInsert onInsert={onInsert} />
                <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
            </TodoTemplate>
        </div>

    );
};

export default ScheduleBoard;

이전 포스팅 처럼 최적화가 된 상태이다.
💡 여기서 리스트를 렌더링하는 를 집중해서 보자.
💡 등록된 props 는 [todos], [onRemove], [onTogle] 이다.

현재 리렌더링 조건
💡 부모 컴포넌트의 리렌더링
💡 todos 상태 변경
💡 onInsert, onRemove, onToggle 함수의 변경 시점에 리렌더링

TodoList.js

  • AS-IS

      import TodoListItem from './TodoListItem';
      import '../TodoList.scss';
    
      const TodoList = ({ todos, onRemove, onToggle }) => {
    
          return (
              // 리스트 상세 컴포넌트
              <div className="TodoList">
                  {todos.map(todo => (
                      <TodoListItem
                          todo={todo}
                          key={todo.id}
                          onRemove={onRemove}
                          onToggle={onToggle}
                      />
                  ))}
              </div>
    
          );
      };
    
      export default TodoList;
        
    
  • TO-BE

      import React from 'react';
      import TodoListItem from './TodoListItem';
      import '../TodoList.scss';
    
      const TodoList = ({ todos, onRemove, onToggle }) => {
    
          return (
              // 리스트 상세 컴포넌트
              <div className="TodoList">
                  {todos.map(todo => (
                      <TodoListItem
                          todo={todo}
                          key={todo.id}
                          onRemove={onRemove}
                          onToggle={onToggle}
                      />
                  ))}
              </div>
    
          );
      };
    
      export default React.memo(TodoList);  
      // React.Memo 사용
      // todos, onRemove, onToggle 프롭스가 변경되지 않는 한 재렌더링을 방지함.
        
    

    React.memo 기능으로 방지되는 항목
    리액트 렌더링 조건과 TodoList 비교

    • 💡 자신이 전달받은 props가 변경될 때
      • todos, onRemove, onToggle 프롭스를 전달받고 있다.
      • TodoList 컴포넌트가 React.memo 기능으로 인해 위 프롭스가 변경되지 않는한 리랜더링 되지 않는다.
    • 💡 자신의 state가 바뀔 때
      • TodoList 컴포넌트는 상태(state)를 사용하고 있지 않으므로, 해당 상황은 발생하지 않는다.
    • 💡 부모 컴포넌트가 리렌더링될 때
      • 부모 컴포넌트가 리렌더링될 때마다 ScheduleBoard 컴포넌트의 내용이 재생성될 수 있다.
      • 즉, 이 말은 TodoList 자식 컴포넌트도 재렌더링될 가능성이 있다.
      • 참고로 현재는 부모컴포넌트가 재랜더링 수행하는 조건은 최적화를 적용 했기에 별로 없다. 아래 내용이 존재   있다. 
              
        // 부모컴포넌트의 랜더링 가능성 
        1. 초기 마운트시점 랜더링
        2.  함수 변경시 랜더링 (정확히는 todos 변경시 랜더링 되어짐)
        3. 부모컴포넌트 리랜더링 시점에 랜더링
        
    • 💡 forcUpdate 함수가 실행될 때
      • forceUpdate 함수가 실행되면 어떤 컴포넌트던 강제로 리렌더링된다.

최적화 완료(정리)

리스트 컴포넌트 최적화 (필수개념)
💡 (1). 부모 ScheduleBoard 컴포넌트의 onInsert, onRemove, onToggle 함수들은 모두 todos 상태가 변경될 때만 새로 생성된다.

  • 비 의존성 useCallback 훅을 사용하여 함수를 최적화하고, 함수 내부에서 참조하는 외부 변수(여기서는 todos)가 변경되었을 때만 새로운 함수를 생성하도록 설정했다.
  • 즉, todos 가 변경되지 않으면 함수는 랜더링되지 않는 멋진 상태로 만들어놨다.

💡 (2). 자식 TodoList 컴포넌트는 React.memo 으로 인해 프롭스가 변경되지 않는 한 재렌더링을 방지한다.

  • 그런데 1번에서 무분별한 함수생성 최적화를 미리 해둔 todos 가 현재 TodoList의 프롭스로 사용되고 있다.

💡 (3). 결론적으로 ScheduleBoard 컴포넌트에서 onInsert, onRemove, onToggle 함수가 호출되어도, 전달받은 todos가 변경되지 않는 한 TodoList 컴포넌트는 재렌더링을 안한다.

  • 변경되지 않은 녀석은 함수가 갱신되지 않을 것이고,
  • 함수가 갱신되지 않아서 자식컴포넌트에 전달된 프롭스도 그대로라서 갱신되지 않는다.

react-virtualized 최적화

React.memo 과정
💡 2500개의 할 일 리스트가 있지만 사용자 눈에 보이는 갯수는 9개 정도밖에 없다.
💡 그럼 나머지 2,491개의 컴포넌트는 스크롤하기 전까지 렌더링하지 않고 크기만 던져두게끔 하는 방법이다. 💡 변경이면 재랜더링 / 그대로면 재사용.

STEP1. react-virtualized 설치

yarn add react-virtualized

STEP2. 리스트로 뿌려질 각 항목의 크기 알아내기

  return (
    <List
      className="TodoList"
      width={512}   // 전체 크기
      height={513}  // 전체 높이
      rowCount={todos.length}  // 항목 개수
      rowHeight={57}           // 항목 높이
      rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
      list={todos} // 배열
      style= // List에 기본 적용되는 outline 스타일 제거
    />
  );

react-virtualized API의 CSS 정보
💡 List 컴포넌트를 이런식으로 호출한다.
💡 해당 리스트의 css 정보를 props 으로 적용 해야한다. 💡 그러면 이 컴포넌트가 전달 받은 props를 사용하여 자동으로 최적화 해준다.

STEP3. TodoList.js

import React, {useCallback} from "react";
import {List} from 'react-virtualized';
import TodoListItem from "./TodoListItem";
import '../scss/TodoList.scss';

const TodoList = ({ todos, onRemove, onToggle }) => {
    const rowRenderer = useCallback (
        ({ index, key, style }) => {
            const todo = todos[index];
            return (
                <TodoListItem 
                    todo={todo} 
                    key={key} 
                    onRemove={onRemove}
                    onToggle={onToggle}
                    style={style}
                />
            );
        },
        [onRemove, onToggle, todos],
    )
    return (
        <List
        className="TodoList"
        width={512}   // 전체 크기
        height={513}  // 전체 높이
        rowCount={todos.length}  // 항목 개수
        rowHeight={57}           // 항목 높이
        rowRenderer={rowRenderer} // 항목을 렌더링할 때 쓰는 함수
        list={todos} // 배열
        style= // List에 기본 적용되는 outline 스타일 제거
        />
    );
}

export default React.memo(TodoList);

React.memo 과정
💡 상단에서 임포트하면 List 컴포넌트를 불러와 사용할 수 있게 된다.
💡 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 [렌더링할 때 사용하는 함수], 그리고 [배열을 props]로 던져주면
💡 List 컴포넌트가 전달받은 props를 사용해 자동으로 최적화 해준다.

그리고 각 리스트를 나타내는 TodoListItem.js를 수정해주어야 한다.

STEP4. TodoListItem.js

import React from 'react';
import {
  MdCheckBoxOutlineBlank,
  MdCheckBox,
  MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import '../TodoListItem.scss';

const TodoListItem = ({ todo, onRemove, onToggle, style }) => {
  const { id, text, checked } = todo;

  return (
    <div className="TodoListItem-virtualized" style={style}>
      <div className="TodoListItem">
        <div
          className={cn('checkbox', { checked })}
          onClick={() => onToggle(id)}
        >
          {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
          <div className="text">{text}</div>
        </div>
        <div className="remove" onClick={() => onRemove(id)}>
          <MdRemoveCircleOutline />
        </div>
      </div>
    </div>
  );
};

export default React.memo(TodoListItem);

💡 style 프롭스가 추가되었다. 누락하지 말자!

STEP4. TodoListItem.scss

//추가
.TodoListItem-virtualized {
  & + & {
    border-top: 1px solid #dee2e6;
  }
  &:nth-child(even) {
    background: #f8f9fa;
  }
}

React.memo 과정
💡 상단에서 임포트하면 List 컴포넌트를 불러와 사용할 수 있게 된다.
💡 해당 리스트의 전체 크기와 각 항목의 높이, 각 항목을 [렌더링할 때 사용하는 함수], 그리고 [배열을 props]로 던져주면
💡 List 컴포넌트가 전달받은 props를 사용해 자동으로 최적화 해준다.

정리

최종 결과

사진1

💡v1 속도가 상대적으로 매우 느리다는것을 알 수 있다.
💡v2 와 v3 는 동일한 수준의 최적화를 보인다.
💡v4 는 보이는 부분만 렌더링해서 엄청나게 속도가 향상되었다.

Tags:

Categories:

Updated:

Leave a comment