본문 바로가기
React

2021.07.30 React Todo-List 성능 최적화, immer

by 해맑은 코린이 2021. 7. 30.

2021.07.30_React Todo-List Component 성능 최적화,immer_정리노트



11장~12장 정리를 한꺼번에 해봅씨단.
10장에서 간단하게 만든 Todo-List 는 렌더링할 데이터가 많아지면 자동으로 리렌더링 되는 시간이 늘어남. 의도적으로 데이터를 많이 발생시키고 최적화 해볼 것임!
또 간단하게 객체의 구조가 복잡할 때 업데이트를 간단하게 해주는 immer 라이브러리도 간단하게 다뤄봅씨다!



src/App


이렇게 2500개의 데이터를 강제로 넣어주고,



state 기본값을 함수를 넣어주었다. 여기서 만약에 createBulkTodos() 라고 작성하면, 리렌더링될 때마다 createBulkTodos 함수가 호출되지만, createBulkTodos 의 형식으로 넣어주면, 컴포넌트가 처음으로 렌더링 될 때만 createBulkTodos 함수가 실행된다.


그리고 다음으로 추가를 한다면 id 가 2501 번째가 되어야 하므로, 해당 useRef 부분도 수정해줍씨단



실행화면



이렇게 2500개의 할일이 뜰 거고 해당 항목을 체크하면 한 2초..? 걸리는듯 쨋든 이전보다는 확실히 느린게 느껴진다.


정확하게 보기 위해서

개발자도구에서 performance 탭을 이용, 맨 위 녹화버튼을 누르고 항목 체크를 한 뒤에 stop 버튼을 누르면 성능 분석 결과가 나타남.
Timings 를 보면 각 시간대에 컴포넌트의 어떤 작업이 처리되었는지까지 확인이 가능하다. 오.. 처음 알았음

 

전체 시간이 2~3초 정도 걸리는데, 데이터가 2500개 밖에 안되는데 이렇게 걸리는건 성능이 매우 나쁘다는 의미래... ㅠ_ㅠ..
후 이제 최적화를 그러면 같이 정리 ㄱ ㄱ!

 

 

++ 08.02 추가

컴포넌트에서 걸리는 시간을 보고 싶으면, url 뒤에 ?react_perf 을 입력!

 

 

 

 

 

 

이렇게 react developer toos 에 있는 Profiler 탭에서 확인 가능..! 지금은 최적화를 전부 완료한 상태라서 성능이 좋게 뜨는데 컴포넌트별로 이렇게 걸리는 시간을 볼 수 있다!

 

https://reactjs.org/blog/2016/11/16/react-v15.4.0.html#profiling-components-with-chrome-timeline

 

React v15.4.0 – React Blog

Today we are releasing React 15.4.0. We didn’t announce the previous minor releases on the blog because most of the changes were bug fixes. However, 15.4.0 is a special release, and we would like to highlight a few notable changes in it. Separating React

reactjs.org

자세한 내용은 여기서 확인!




컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.

자신이 전달받은 props 가 변경될 때
자신의 state 가 바뀔 때
부모 컴포넌트가 리렌더링 될 때
forceUpdate 함수가 실행될 때

저번에 라이프사이클 할 때 업데이트 부분임!!

현재 내 Todo-List 가 리렌더링 되는 시점을 보면, '할일 1' 항목을 체크할 경우 App 컴포넌트의 state 가 변경되면서, App 컴포넌트가 리렌더링 된다. 부모 컴포넌트가 리렌더링 되었으니 밑의 자식컴포넌트인 TodoList 가 리렌더링되고, 그 안에 있는 자식컴포넌트들도 무수히 리렌더링 된다.
'할 일 1' 항목은 리렌더링 되어야 하는 것이 맞지만, 나머지 2부터 2500 까지는 리렌더링을 안해도 되는 상황인데 모두 리렌더링 되기 때문에 느린것!! 컴포넌트의 개수가 많지 않다면 괜찮은데 2000개가 넘어가는 시점부터는 현저히 성능이 저하되는 것을 볼 수 있다.


그래서 우리가 해야할 것은 리렌더링을 최적화해서 방지해 주어야 한다.


React.memo
클래스형 컴포넌트의 shouldComponentUpdate 메서드와 유사한 함수.
컴포넌트의 props 가 바뀌지 않았다면 리렌더링 하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 한다.


쓰는 방법's 매우 간단's

src/components/TodoListItem



이렇게 컴포넌트를 만들고 나서 감싸주면 됨. 이제 얘는 todo,onRemove,onToggle ( 해당 컴포넌트 props )이 바뀌지 않으면 리렌더링을 하지 않음.


얘네들 ㅇㅇ

하지만 이것만 해서는 최적화는 끝나지 않는다. 현재 프로젝트에서 todos 배열이 업데이트 되면 onRemove,onToggle 함수가 새롭게 바뀌기 때문.
이 두 함수는 배열 상태를 업데이트 하는 과정에서 최신 상태의 todos 를 참조하기 때문에 해당 배열이 바뀌면 함수가 새로 만들어진다.

이렇게 함수를 계속 만드는 상황을 방지하는 두 가지 방법을 정리!

useState 함수형 업데이트 기능 사용

src/App


기존 setTodos 를 사용할 때는 새로운 상태로 갈아끼웠다. setTodos를 사용할 때 새로운 상태를 파라미터로 넣지 않고, 상태 업데이트를 어떻게 할 지 정의해주는 업데이트 함수를 넣을 수도 있다. 이를 함수 업데이트라고 한다.


이렇게 앞부분에 todos=> 를 넣는다. 그러면 useCallback 에서 받는 두번째 파라미터에 [todos] 배열을 넣지 않아도 된다.

마찬가지로 onRemove, onToggle 함수에도 todos=> 로 함수형 업데이트를 추가해주고, 두번째 파라미터를 빈 배열로 만들어주면 된다.

이제 실행해보면,

오 1초대가 되었다. 훨씬 줄어듬!!!



useReducer 사용
useReducer 를 사용할 때는 원래 두번째 파라미터에 초기 상태를 넣어주어야 하는데, 우리는 대신에 undefined 를 넣고, 세번째 파라미터에 초기 상태를 만들어주는 함수인 createBulkTodos 를 넣어준다.
이렇게 하면 컴포넌트가 맨 처음 렌더링 될 때만 createBulkTodos 함수가 호출 된다.

src/App


상태 업데이트 로직 작성.


reducer를 사용해서 초기 상태와 상태를 만들어주는 함수를 인자로 입력.

그리고 타입에 맞는 것들을 각 함수에 작성.


실행화면


마찬가지로 성능 1초대 ㅎㅅㅎ!!!!

이 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트 하는 로직을 컴포넌트 바깥에다가 한번에 정의할 수 있다.
성능은 함수형 업데이트나 useReducer 나 비슷하기 때문에 둘 중 하나를 선택해서 쓰면 된다!

음 나는 아직 dispatch 의 개념이나 이런것들이 익숙하지 않으니 아마도 함수형 업데이트를 쓰면서 더 익숙해지지 않을까 싶다 ㅎㅁㅎ


잠깐, 책에서 계속 강조했던, 불변성의 중요성을 잠시 되짚어봅씨다

컴포넌트에서 상태를 업데이트 할 때는 불변성이 매우매우 중요하다.
왜?


여기서도 보면, 직접 수정하는 것이 아니라 ...todo 로 새로운 배열을 만든 다음에 새로운 객체를 만들어서 필요한 부분을 교체해주는 방식이다.

자바스크립트 개념으로 들어가서

https://korinkorin.tistory.com/53

 

2021.04.22 JavaScript Object 복사

2021.04.22_JavaScript Object 복사_정리노트  오늘은 많이는 안쓰이지만, 단순히 따라하는 방법이 나한테는 어려워서 따로 정리 ㅎㅁㅎ 또한 객체의 중요한 포인트를 짚고 넘어가고 싶어서 포스팅 쓰

korinkorin.tistory.com



내가 저번에 정리했던 것들을 싸악 읽어봤다..음 어렵다 ㅎ 쨋든 이게 왜 중요하냐면,

완전히 같은 배열이기 때문에 true 를 반환.


다른 배열이기 때문에 false.

같은 객체기 때문에 true.


다른 객체이기 때문에 false

이렇듯 불변성이 지켜지지 않으면, 변경사항이 생겨도 true 로 반환한다. 즉 변화를 감지를 하지 못한다.
그러면 비교해서 바뀐 값만 업데이트하는 최적화를 못함.
이는 얕은 복사에 해당하는 해당 예시도 , 내가 자바스크립트 따로 정리해놓은 저 포스팅에서 설명하는 깊은 복사 모두 해당한다.

const nextComplexObject = { ...complextObject, objectInside ;{ ...complextObject.objectInside, enabled:false } }; console.log(complextObject === nextComplexObject ) ; //false console.log(complextObject.objectInside === nextComplexObject.objectInside ) ; // false

만약 객체 안에 있는 객체라면 이런식으로 불변성을 지키면서 새 값을 할당하면 된다.



아까는 TodoListItem 도 최적화를 해주었으니, TodoList 컴포넌트도 최적화 해주자!


src/components/TodoList


똑같이 React.memo 로 최적화를 해주었는데, 지금 코드에서는 유일하게 리렌더링 되는 이유가 todos 배열이 업데이트 될 때 부모 컴포넌트인 App 이 업데이트 되기 때문에 당장 해당 컴포넌트에서는 불필요한 리렌더링이 필요하지는 않지만, 다른 state 값이 추가되면 불필요한 리렌더링이 발생할 수 있는 상황을 미연에 방지해준 것이다.
리스트가 100개가 넘지 않는다면 굳이 해줄 필요없지만, 리스트라는 컴포넌트가 있고 그 규모가 크다면, 리스트, 리스트 아이템 컴포넌트는 꼭 최적화해주자!!!



하나만 더 최적화해줘볼까?

지금


맨 처음 뜨는 컴포넌트 중 우리 눈에 당장 보이는 건 9개고, 나머지 2491 개의 컴포넌트는 보이지 않음에도 렌더링 되는 것이 비효율적이라 느껴진다면, react-virtualized 라는 라이브러리 사용으로 스크롤 되기 전에는 보이지 않는 컴포넌트는 렌더링 되지 않고 크기만 차지하게끔 할 수 있다!
스크롤이 되면 해당 스크롤 위치에서 보여주어야 할 컴포넌트를 자연스럽게 렌더링 시킬 수 있다.

라이브러리 설치 ㄱ

$ yarn add react-virtualized


해당 라이브러리를 설치해주고, 각 항목의 px 단위의 크기를 알아야한다.



개발자 도구에서 해당 파란색으로 표시되어 있는 탭을 눌러서


이렇게 해당 항목을 찍으면 512*56 사이즈라고 나온다.

그리고 첫번째는 border 가 없기 때문에 56 으로 나오고,


나머지는 border-top 으로 1px 씩 추가되어 57px로 나옴.





그리고 보이는 List 전체 가로 높이와 세로높이도 필요함



자 이제 설정 ㄱ ㄱ


src/components/TodoList

import React, { useCallback } from 'react';
// List import
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem';
import './TodoList.scss';
const TodoList = ({ todos, onRemove, onToggle }) => {
  // 각 TodoListItem 렌더링에 필요한 함수. 이 함수를 List 컴포넌트의 props 로 설정해주어야함. 이 함수는 index,key,style 의 값을 객체형태로 받아옴. 
  //  그러면 List 컴포넌트는 props 를 받아서 자동으로 최적화해줌
  const rowRendrer = useCallback(({ index, key, style }) => {
    const todo = todos[index];
    return (
      <TodoListItem
        todo={todo}
        key={key}
        onRemove={onRemove}
        onToggle={onToggle}
        style={style}
      />
    );
  },[onRemove,onToggle,todos]);
  // App 에서 props 로 넘겨준 부분을 받아와서 TodoListItem 으로 변환하여 렌더링 해줄 것임
  return (
    // <div className="TodoList">
    //   {/* map 을 사용하여 컴포넌트로 변환할 때는 key 값 필요 ( 고유의 id 값으로 설정해주었음. )*/}
    //   {/* todo 데이터는 통째로 props 로 TodoListItem 에 전달 => 객체 통째로 전달해주는게 나중에 여러 종류의 값을 전달해야 하는 경우에 최적화가 편함*/}
    //   {todos.map((todo) => (
    //     <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle}/>
    //   ))}
    // </div>
    <List
      className="TodoList" //classname
      width={512} // 전체 가로
      height={513} // 전체 높이
      rowCount={todos.length} // 항목 개수
      rowHeight={57} // 항목 높이
      rowRenderer={rowRendrer} // 항목을 렌더링할 때 쓰는 함수
      list={todos} // 배열
      sytle={{ outline: 'none' }} // 리스트에 기본으로 적용되는 outline 스타일 제거
    />

  );
};

export default React.memo(TodoList);


이렇게 작성해주고 style 이 깨져서 TodoListItem 에도 style props 설정해서 안깨지게 ㄱ ㄱ



새로 class를 감싸주고, 새로만든 클래스에 기존에 겹쳐지는 부분에 설정해주었던 border-top 이랑 짝수번째 배경까지 잘라내서 설정해주면!!!



와!!!!!!!!!!!!!!!!!!! 1초도 안걸림!!!!!!!!! ㅎㅅㅎ.. 나는 사실 2초나 1초가 별 상관없다고 생각했는데 항상 최악으로 만약 10만개의 데이터가 있다면 정말 눈에 띄는 성과다 후후




이까지는 저번에 만들었던 Todo-List 에서 최적화라면 분량 조절 실패긴 한데 immer 도 같이 알아보자... 따르륵...


객체 또는 배열의 구조가 아까 위에서 적었던 깊은 복사의 예시처럼 복잡해진다면, 굉장히 까다로워지는데 이때 immer 라는 라이브러리의 도움을 받으면 정말 편하게 작업이 가능하다.. 후.. 좋은 라이브러리가 많군 증맬루..

예를 들어


이이런.... 깊깊깊게 들어간 객체의 경우 값을 바꾸거나 추가할 때

이런식으로 들어가고 들어가고 해야함.. 하나만 바꾸는 건데도 말이다.
기존의 값을 유지하면서 바꾸려는 값만 새로 지정해주어야 하기 때문.


실제 프로젝트에도 이런 상태를 다룰 때가 있을 때 immer 라는 라이브러리를 사용하면, 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해서 불변성 유지하고, 업데이트를 해줄 수 있다네!!

투두리스트는 이제 뿌듯하게 남겨두고 ^*^ 얘를 위한 프로젝트 하나를 생성해주게쌈

$ yarn create react-app immer-tutorial $ cd immer-tutorial // 프로젝트 생성해서 immer 설치 $ yarn add immer


App 컴포넌트만 사용할거라 경로는 이제 안적게쑴.


src/App


import React, { useRef, useCallback, useState } from "react"; 



const App = () => { 
  const nextId = useRef(1); 
  const [form, setForm] = useState({ name: "", username: "" }); 
  const [data, setData] = useState({ 
    array: [], 
    uselessValue: null, }); 
  // input 수정을 위한 함수
  const onChange = useCallback( (e) => { 
    const { name, value } = e.target; 
    setForm({ 
      ...form, 
      [name]: [value], 
    }); }, [form] 
    ); // form 등록을 위한 함수 
    
  const onSubmit = useCallback( (e) => { 
      // submit 이벤트 새로고침 막음 
      e.preventDefault(); 
      const info = { 
        id: nextId.current, 
        name: form.name,
         username: form.username, }; 
         // array 에 새 항목 등록 
         setData({ 
          ...data, 
          array: data.array.concat(info), });
           // form 초기화 
           setForm({ 
             name: "",
              username: "", 
            }); 
              nextId.current += 1; 
            }, 
            [data, form.name, form.username] ); 
            // 항목을 삭제하는 함수 
      
      
  const onRemove = useCallback((id) => 
   { setData({ 
     ...data, 
     array: data.array.filter(
       (info) => info.id !== id),
       }); }, 
       [data]);
        return ( 
        <div> 
          <form onSubmit={onSubmit}>
             <input name="username" placeholder="아이디" value={form.username} onChange={onChange} /> 
             <input name="name" placeholder="이름" value={form.name} onChange={onChange} /> 
             <button type="submit">등록</button> 
             </form> 
             <div> 
               <ul> 
                 {data.array.map((info) => 
                 ( 
                 <li key={info.id} 
                 onClick={() => onRemove(info.id)}> 
                 {info.username} 
                 ({info.name}) </li> ))} 
                 
                 </ul> 
                 
                 </div> 
                 
                 </div> ); }; 
                 
                 
                 
export default App;


간단.....한.....맞어....그동안 다했던거지.....ㅎ 쨋든 이렇게 간단하게


인풋에 아이디와 이름을 입력해서 등록하고 , 리스트를 클릭하면 삭제되는 컴포넌트를 App 에다가 만들어줌.
form 에다가 아이디/ 이름을 입력하면 해당 타겟 벨류를 가져와서 array 에 추가되는 것!

위의 코드 내용처럼 지금까지 한 작업들이 익숙하다면 concat으로 불변성 유지 후 업데이트 하는 것이 어렵지 않지만 ( 물론 난 아직 익숙해지지 못함 )
지금도 헷갈리는데 좀 더 복잡하고 더 복잡한 경우라면 ... ?
immer 가 그 때 매우 유용하대

사용법 예시

import produce from 'immer'; const nextState = produce(originalState,draft =>{ 바꾸고 싶은 값 바꾸기 draft.somewhere.deep.inside =5; })


produce수정하고 싶은 상태를 첫번째 파라미터로 받고, 상태를 어떻게 업데이트할지 정의하는 함수를 두번째 파라미터로 받는다.
두번째 파라미터 함수에서 바꾸고 싶은 값을 변경하면, produce 함수가 불변성 유지를 대신 해주면서 새로운 상태를 생성해준다.

오 이러니까 그냥 불변성에 개의치 않고 막 값을 바꾸는 것 같은데 해당 함수 사용으로써 불변성 관리를 확실히 해준다니 너무나도.. 객체의 깊은 복사가 나오면서 무슨 소린지 몰랐던 나에게는 단비같은 라이브러리다...
단순히 배열을 처리할 때도 쉽대!!!!

예시코드

import produce from "immer"; const originalState = [ { id: 1, todo: "전개 연산자와 배열 내장 함수로 불변성 유지하기", checked: true, }, { id: 2, todo: "immer 는 불변성 유지하는 라이브러리", checked: false } ]; const nextState = produce(originalState,draft =>{ // id 2 인 항목을 찾아서 const todo =draft.find(i => i.id ===2); // checked 값 true 로 변경 todo.checked =true; // 배열에 새로운 데이터를 추가해줌 draft.push({ id:3, todo:'일정 관리 앱에 immer 적용하기', checked:false }); // splice 는 원본배열을 추가하거나 삭제할 때 쓰는 자바스크립트 함수. 여기서는 id 값이 1인 값을 제거함 draft.splice(draft.findIndex(i => i.id ===1),1) })

이렇게 뭔가 직관적이고 그냥 아무것도 상관 안쓰고 그냥 막 쓴 것 같아도 다 불변성을 지켜주면서 업데이트하고, 추가하고, 삭제까지 가능...캬 최고



이제 프로젝트에 적용해보기

 

 



비교하려고 일부러 주석 밑에 작성함 이렇게 해당 state 를 업데이트 하는 곳에 형식에 맡게 쓰면 성공! 똑같이 잘 작동한당



또한 useState 위에 봤던 함수형 업데이트로 쓸 수도 있다. 굿... 앞에 있었던 form 파라미터를 제거해주고, 함수를 파라미터로 전달해주면, 업데이트 함수를 반환한다!! 그리고 마찬가지로 useCallback 의 두번째 파라미터 배열에는 빈 값으로 넣어주면 됨!

 


나머지도 이렇게 똑같이 함수형 업데이트로 만들어주었다.


만약 복잡한 코드나 나중에 redux 에서 다룰 때도 매우 쉽게 다룰 수 있대..! 배워둬서 나쁠거 없지 . 그리고 개인적으로 어려웠던 객체 부분인 만큼 꼭 포스팅 하고 싶었다...! ㅠㅠㅠ 정말 생각 직관적이게 잘 도와줌... ㄱㅅ...



오늘 분량 조절 대대 실패... 최적화부분에다가 또 개념적인 부분이라서 줄줄줄 책따라서 친 느낌이 많이 강한데..... 흑흑.. 그래도 나는 이렇게 해서 또 읽을것을 알기에.. 열심히 적었다 .. 개인적으로 immer 도 나는 너무 맘에 들어서 열심히 추가로 적다보니 ... 아유 배고파 나는 이만 포스팅 끝끝!

'React' 카테고리의 다른 글

2021.08.07 React 외부 API 호출  (0) 2021.08.07
2021.08.03 리액트 라우터로 SPA 개발하기  (3) 2021.08.03
2021.07.28 React To-do List  (0) 2021.07.29
2021.07.24 React styling  (2) 2021.07.24
2021.07.23 React Hooks  (0) 2021.07.23

댓글