[React] 컴포넌트 성능 최적화

soyeon·2022년 2월 15일
0
post-thumbnail

🎈 들어가며

<리액트를 다루는 기술> 11장 컴포넌트 성능 최적화를 공부하면서 정리한 내용입니다.📚
중간에 모르는 것들은 따로 서치를 해서 공부했습니다.

🎈 많은 데이터 렌더링하기

App.js

function createBulkTodos() {
  const array = [];
  for (let i = 1; i <= 2500; i++) {
    array.push({
      id: i,
      text: `할일 ${i}`,
      checked: false,
    });
  }
  return array;
}
  • 최적화 테스트를 위해 반복문을 이용해서 할 일을 2500개까지 늘립니다.

🎈 React DevTools를 통한 성능 모니터링

  • React DevTools를 크롬 확장 프로그램 스토어에서 다운 받으세요
  • F12을 누르면 탭란 끝 부분에 Profiler라는 탭이 있습니다.
  • 파란색 녹화 버튼을 누르고 ‘할 일 1’ 항목을 체크한 다음 체크 표시가 되면 녹화 버튼을 눌러서 성능을 체크합니다.

🎈 느려지는 원인 분석

  • 성능을 체크하면 Render duration가 장난 아니게 길게 나오는 걸 볼 수 있습니다. (제 컴은 900ms 정도 나왔네요)
  • 느린 이유는 할 일1 항목과 상관없는 컴포넌트들까지 리렌더링 됐기 때문입니다.

🍧 컴포넌트가 리렌더링 되는 경우
1. 자신이 전달받은 props가 변경될 때
2. 자신의 state가 바뀔 때
3. 부모 컴포넌트가 리렌더링될 때
4. ForceUpdate 함수가 실행될 때

🎈 React.memo를 사용하여 컴포넌트 성능 최적화

🍧 react.memo(함수)

  • 넘어온 props가 변경되지 않으면 리렌더링 x
  • 여기서는 todo, onRemove, onToggle이 바뀌지 않으면 리렌더링 되지 않음.

ToDoListItem.js

.
.
.
export default React.memo(ToDoListItem);

🎈 onToggle, onRemove 함수가 바뀌지 않게 하기

  • onToggle, onRemove 함수는 배열 상태를 바꾸는(업데이트 하는) 과정에서 최신 상태의 todos를 참조합니다.
  • 그래서 todos 배열이 업데이트 되면 onRemove, onToggle 함수도 새롭게 바뀝니다.

useState의 함수형 업데이트

  • useState의 modifier 함수인 setXXX현재 값을 파라미터로 넣어서 현재 값을 업데이트 하는 형식으로 바꿔줍니다.
  • 함수형 업데이트를 쓰면 useCallback의 두번째 파라미터에 해당 state를 넣지 않아도 됩니다.
  • 예시)
const [number, setNumber] = useState(0);
const onIncrease = useCallback(
	()=> setNumber(prevNumber => prevNumber + 1),
  []
)
// 현재의 number = prevNumber
  • setTodos를 사용할 때 todos =>를 넣어서 현재의 값을 알려줍니다.
  • 전 따로 수정하기 기능을 추가했기 때문에 수정하기 함수도 업데이트 해줬습니다.
    App.js
function App() {
  const [todos, setTodos] = useState(createBulkTodos);
  const [selectedTodo, setSelectedTodo] = useState(null);
  const [insertToggle, setInsertToggle] = useState(false);

  const nextId = useRef(4);
  const onInsertToggle = useCallback(() => {
    if (selectedTodo) {
      setSelectedTodo((selectedTodo) => null);
    }
    setInsertToggle((prev) => !prev);
  }, [selectedTodo]);

  const onChangeSelectedTodo = (todo) => {
    setSelectedTodo((selectedTodo) => todo);
  };

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    };
    setTodos((todos) => todos.concat(todo)); //concat(): 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열 반환
    nextId.current++; //nextId 1씩 더하기
  }, []);

  const onRemove = useCallback((id) => {
    setTodos((todos) => todos.filter((todo) => todo.id !== id));
  }, []);
  const onUpdate = useCallback(
    (id, text) => {
      onInsertToggle();

      setTodos((todos) =>
        todos.map((todo) => (todo.id === id ? { ...todo, text } : todo)),
      );
    },
    [onInsertToggle],
  );
  const onToggle = useCallback((id) => {
    setTodos((todos) =>
      todos.map((todo) =>
        todo.id === id ? { ...todo, checked: !todo.checked } : todo,
      ),
    );
  }, []);
  • 성능 확인을 해볼까요~~?
    before

    render duration : 701.3ms
    after

    render duration : 684.4ms
    (어..? 별로 달라진 게 없는데?)
    암튼 memo가 잘 적용된 것 같습니다...? 리렌더링된 컴포넌트의 수도 많이 줄었네요.

🎈 불변성의 중요성

불변성이란?

🍧불변성: 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것

  • 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못합니다.

이게 무슨 소린가 싶죠? 이를 이해할려면 얕은 복사가 무엇인지 알아야합니다.

  const onToggle = useCallback((id) => {
    setTodos((todos) =>
      todos.map((todo) =>
        todo.id === id ? { ...todo, checked: !todo.checked } : todo,
      ),
    );
  }, []);
  • 기존 데이터를 수정할 때 직접 todo를 건드리지 않고, 새로운 배열을 복사한 다음{...todo} 새로운 객체를 만들어서checked: !todo.checked 필요한 부분을 교체해줬습니다.
  • 이렇게 스프레드 문법(... 문법)를 사용하여 객체나 배열 내부의 값을 복사하는 게 얕은 복사입니다.

✨얕은 복사

🍧얕은 복사: 바깥쪽에 있는 값만 복사되고 내부의 값ex)객체의 안에 있는 객체의 값주소값만 가져온다.
(복사값과 참조값이 같이 있을 수 있음. 완전한 복사본이 아님)

예시)

const todos = [{id: 1, checked: true}, {id: 2, checked: true}];
const nextTodos = [...todos]; //얕은 복사

이 코드의 메모리 상태(?)를 대강 그림으로 그려봤습니다.

  • 객체는 별도의 공간에 저장되기 때문에 todos 내부 안의 객체 값은 각각 다른 메모리 공간에 저장됐습니다.
  • todos 배열은 각 객체를 참조하고 있습니다.
  • nextTodos도 todos와 마찬가지로 두개의 원소를 가진 인스턴스가 만들어졌습니다.
  • 그러나 객체 내부의 값들(id,checked)은 복사하지 않고 원본의 주소값을 가져왔습니다.
  • 그렇다면 아래 코드를 입력하면 어떻게 될까요?
nextTodos[0].checked = false;
console.log(todos[0] === nextTodos[0])

  • nextTodos 안의 객체 값들은 원본의 주소값을 가져오는 것이기 때문에 원본값이 false로 바뀌었습니다.
  • 이로 인해 todos[0] === nextTodos[0]true임을 알았습니다.
  • 여기서 기존(원본) 값을 바꾸지 않고 불변성을 지키려면 새 값을 할당해야합니다.
nextTodos[0] = {
  ...nextTodos[0], //기존 값들은 유지
  checked: false // 바꿔줄 값만 새로 할당
} // 새 객체를 할당
console.log(todos[0] === nextTodos[0]); //false

🎈 TodoList 컴포넌트 최적화하기

리스트 관련 컴포넌트를 작성할 때는 리스트 아이템과 리스트를 React.memo로 최적화해주는 게 좋습니다.

  • 그러나 내부 데이터가 100개를 넘지 않거나 업데이트가 자주 안된다면 반드시해줄 필요는 없습니다.

🎈 react-virtualized를 사용한 렌더링 최적화

🍧 react-virtualized : 스크롤 되기 전에 보이지 않는 컴포넌트는 렌더링하지 않고 크기만 차지하게 해줌.

설치하기

npm install react-virtualized --save

근데 제 쪽에선 의존성 에러가 나더라고요ㅠㅠ 다른 분 포스트를 보고 아래 명령어로 설치했습니다.

npm install react-virtualized --legacy-peer-deps

import

import {List} from 'react-virtualized'

ToDoList 수정

function TodoList({ todos, onRemove, onToggle, onChangeSelectedTodo, onInsertToggle }) {
  const rowRender = useCallback(
    ({index,key,style}) => {
      const todo = todos[index];
      return(
        <ToDoListItem
        todo={todo}
        key={key}
        onToggle={onToggle}
        onRemove={onRemove}
        onInsertToggle={onInsertToggle}
        onChangeSelectedTodo={onChangeSelectedTodo}
        style={style}
      />
      )
    },
    [ todos, onRemove, onToggle, onChangeSelectedTodo, onInsertToggle ]
  )
  
  return (
    <List 
      className='TodoList'
      width={512} // 전체너비
      height={513}// 전체 높이
      rowCount={todos.length}//항목갯수
      rowHeight={57} // 항목 높이(픽셀)
      rowRenderer={rowRender} //항목을 렌더링할 때 쓰는 함수
      list={todos}//배열
      style={{outline:'none'}} //List에 기본 적용되는 outline 스타일 제거
    />
  );
}
  • rowRenderer:
    react-virtualized의 List 컴포넌트에서 각 TodoItem를 렌더링할 때 사용
    List 컴포넌트의 props로 설정해야함!
    인자로 index, key, style 값을 객체 타입으로 받아와서 사용.

  • List 컴포넌트
    해당 리스트의 전체크기 & 각 항목의 높이 * 각 항목을 렌더링할 때 사용해야하는 함수(rowRenderer)을 props로 넣어줘야 함!!

TodoListItem 수정

function ToDoListItem({
  todo,
  onRemove,
  onToggle,
  onChangeSelectedTodo,
  onInsertToggle,
  style
}) {
  const { id, text, checked } = todo;
  return (
    <div className="TodoListItem-virtualized" style={style}>
      <li className="TodoListItem">
        <div
          className={cn('checkbox', { checked: checked })}
          onClick={() => onToggle(id)}
        >
          {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
          <div className="text">{text}</div>
        </div>
        <div
          className="edit"
          onClick={() => {
            onChangeSelectedTodo(todo);
            onInsertToggle();
          }}
        >
          <MdModeEditOutline />
        </div>
        <div className="remove" onClick={() => onRemove(id)}>
          <MdRemoveCircleOutline />
        </div>
      </li>
    </div>
  );
}
  • 기존에 보여주던 내용을 <div className="TodoListItem-virtualized" style={style}>로 감쌉니다.

성능 측정을 해볼까요~~??

render duration이 21.6ms까지 줄어들었습니다!

🎈마치며

리액트 공부 진도를 빨리 빼야할텐데..ㅋㅋㅋ
틀린 부분 있으면 지적 부탁드립니다😄 감사합니다~

📎참고

<리액트를 다루는 기술> - 김민준(벨로퍼트), 길벗
React-virtualized 설치방법
{즉문즉설:자바스크립트} Object의 깊은 복사 vs 얕은 복사 - 시니어코딩 유튜브

profile
공부중

0개의 댓글