TIL 22-04-29

thisisyjin·2022년 4월 29일
0

TIL 📝

목록 보기
31/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 11

  • 일정 관리 App 성능 최적화

To Do App 성능 최적화

성능 분석

  • 성능을 분석할 때는 정확히 몇 초가 걸리는지 확인해야함.
  • React Dev Tools를 사용하여 성능 분석을 할 수 있음.

  • 각 컴포넌트별로 렌더링하는데 걸린 시간이 나오고,
    우측에 Durations 탭에 Render은 - 리렌더링에 소요된 시간을 의미한다.

  • 상단에 Ranked를 클릭하면, 랭크 차트를 볼 수 있다.
    -> 리렌더링 된 컴포넌트를 오래걸린 순으로 정렬하여 나열해줌.

느려지는 원인 분석

컴포넌트가 리렌더링이 발생하는 경우

  1. 자신이 전달받은 props가 변경될 때
  2. 자신의 state가 변경될 때
  3. 부모 컴포넌트가 리렌더링 될 때
    (PureComponent나 React.memo를 이용)
  4. forceUpdate 함수가 실행될 때
  • 지금 상황에서는 체크박스를 체크할 경우, App 컴포넌트의 state인 todos가 변경되면서
    App 컴포넌트가 리렌더링된다.
    -> 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트 TodoList 컴포넌트가 리렌더링 되고,그 안에 많은 컴포넌트들도 리렌더링 된다.
const createBulkTodos = () => {
  const arr =[];
  for(let i = 1; i <= 2500 ; i++) {
    arr.push({
      id: i,
      text: '할일',
      checked: false
    });
  }
  return arr;
}

const App = () => {
  const [todos, setTodos] = useState(createBulkTodos);
  // 초기값으로 설정. createBulkTodos()가 아님. ()은 없애줘야 매번 호출안함. 
  // ()없이 함수 자체를 넣어줘야 첫 렌더링시 한번만 발생함.
  
  ...
}

예를 들면, 위와 같이 할일이 2500개가 있다고 가정하자.
체크를 하나만 했으므로 1개만 리렌더링 되면 되는데, 나머지 2499개도 리렌더링이 발생한다.
이럴때 성능의 저하가 발생하게 된다.

이럴 때는 불필요한 리렌더링을 방지해주면 된다.

React.memo

  • 컴포넌트의 리렌더링을 방지하려면, shouldComponentUpdate를 사용하면 된다.
  • 함수형에서는 라이프사이클 메서드를 사용할 수 없으므로, 대신 React.memo를 사용한다.
    -> props가 바뀌지 않았다면 리렌더링하지 않도록 설정.

방법 1) 컴포넌트 전체 감싸주기

const TodoListItem = React.memo( ({ todo, onRemove, onToggle}) => {
...
});

방법 2) 컴포넌트 작성 후, export시 감싸주기

const TodoListItem = ({ todo, onRemove, onToggle}) => {
...
}

export default React.memo(TodoListItem);
  • 두 방법 모두 완전히 같게 동작함.
    todo, onRemove, onToggle(props들)이 바뀌기 전까지는 렌더링 ❌

함수의 재생성 방지

  • React.memo를 쓰기만 해서는 최적화가 끝나지 않음.
  • App 컴포넌트의 todos 배열이 업데이트되면 - onRemove, onToggle 함수도 새롭게 바뀜.
    -> 두 함수는 함수 내에서 todos에 의존하는 함수이기 때문. (useCallback 두번째 인자에 의해 함수가 새로 생성됨.)

함수가 계속해서 생성되는 상황을 방지하는 방법

  1. 함수형 useState
  2. useReducer

1) 함수형 setState

기존에는 setState의 인자로 새로 업데이트할 값을 넣어줬었다.
setState를 사용할 때 상태 업데이트를 정의한 함수를 넣어줄 수 있음.

const onIncrease = useCallback(
  () => setNumber(prevNumber => prevNumber + 1),
  []
);

함수형 setState는 useState 내에서 이전 상태(state)를 참고해야 할 때 사용한다.

onInsert 수정

  • 수정 전
const onInsert = useCallback((text) => {
  const todo = {
    id: nextId.current,
    text,
    checked: false,
  }
  setTodos(todos.concat(todo));
  nextId.current += 1;
}, [todos]);
  • 수정 후
const onInsert = useCallback((text) => {
  const todo = {
    id: nextId.current,
    text,
    checked: false,
  }
  setTodos(todos => todos.concat(todo));
  nextId.current += 1;
}, []);
  • useCallback의 두번째 인자를 빈 배열로. (첫 렌더링시 한번만 실행)
  • setTodos를 함수형 setState로 변경.
    -> 더이상 state인 todos에 의존하지 않아도 됨. (todos가 바뀌어도 함수 새로 생성하지 X)

onRemove, onToggle 수정

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))
}, []);

다시한번 성능을 체크해보면, 이전보다 훨씬 속도가 빨라진 것을 알 수 있다.
아래에서 회색 빗금으로 표시된 부분은 React.memo로 리렌더링 되지 않은 컴포넌트를 의미한다.


2) useReducer

  • 함수형 setState를 이용하는 대신,
    useReducer 을 사용해도 함수가 계속 새로 생성되는 문제를 해결할 수 있다.

App.js

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


const todoReducer = (todos, action) => {
    switch (action.type) {
        case 'INSERT':
            return todos.concat(action.todo);
        case 'REMOVE':
            return todos.filter(todo => todo.id !== action.id);
        case 'TOGGLE':
            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);

  const nextId = useRef(4);

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false,
    }
      dispatch({ type: 'INSERT', todo });
    nextId.current += 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;
  • useReducer에는 두번째 인자로 initialState를 넣어줘야하지만, 여기서는 대량의 2500개의 데이터를 넣어주는 함수인 createBulkTodos를 전달한다.두번째 인자로는 undefined를 넣어줘야 한다.
    (맨 처음 렌더링 시에만 createBulkTodos가 실행되도록 하기 위해)

  • useReducer은 상태를 업데이트 하는 로직(=reducer)을 모아 App 컴포넌트 바깥에 둘 수 있다.


불변의 중요성

  • 상태 업데이트시 불변성을 지키는 것은 매우 중요.
  • 따라서, state에는 직접 push나 splice 등이 아닌,
    concat(+), filter(주로 -), map(새 배열 반환)등을 이용한다.
  • 업데이트가 필요한 곳에서는 아예 새로운 배열을 만드므로, React.memo 사용시 props가 바뀐 경우에만 리렌더링을 하여 성능 최적화가 가능하다.

  • 만약, 불변성을 지키지 않는다면? = 객체 내부의 값이 바뀌어도 변경된 것을 감지할 수 ❌

  • React.memo에서 서로 비교하여 최적화 할 수 없음.

참고

  • spread(...) 로 객체나 배열의 값을 복사할 때는 얕은 복사가 이루어짐.
  • 가장 바깥 쪽의 값만 복사됨.
  • 객체나 배열의 구조가 복잡해지면 불변성 유지가 어려워짐 -> immer 라이브러리 사용 가능!
  • ❗️ 만약 객체 안에 있는 객체라면 불변성을 지키기 위해 아래와 같이 내부도 펼쳐줘야함.
const copiedObj = {
  ...obj,
  objInside: {
    ...obj.objInside
  }
};

리스트 최적화

  • TodoList 도 React.memo로 감싸줌.
  • 리스트에 관련된 컴포넌트 최적화시, 리스트 내부에서 사용하는 컴포넌트도 최적화 해야함.
  • 리스트로 사용되는 컴포넌트 자체로 최적화 해주는것이 좋음.

✅ 중요
리스트 관련 컴포넌트 작성시 - 리스트 아이템 / 리스트 (두 컴포넌트) 모두 최적화 필요.


최적화

React-virtualized

만약 todo가 2500개가 있다면, 우리는 화면에 나타나는 todo는 9개임에도 불구하고
스크롤 아래에 가려져 보이지 않는 나머지 2491개의 컴포넌트도 리렌더링이 된다.

  • react-virtualized를 사용하면, 리스트 컴포넌트에서 스크롤되기 전 보이지 않는 컴포넌트를 렌더링하지 않는 기능을 구현할 수 있다.
  • 만약 스크롤되면 해당 스크롤 위치에서 보여줘야할 컴포넌트를 자연스럽게 렌더링시킴.
$ yarn add react-virtualized

최적화에 앞서, 우리는 각 항목의 실제 크기를 px 단위로 알아내야 함.

  • 개발자도구를 이용하여 각 항목의 크기를 알아냄. (512px * 57px)
  • 두번째 항목부터 테두리가 포함되어 있으므로 두번째 항목을 확인해야 함.

TodoList 수정

import React, { useCallback, memo } from 'react';
import { List } from 'react-virtualized';
import TodoListItem from "./TodoListItem";
import './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={{ outline: 'none' }}
        />
    );
};

export default memo(TodoList);

rowRenderer 함수 작성

  • react-virtualized의 List 컴포넌트에서 각 TodoListItem을 렌더링 할때 사용함.
  • 이 함수를 List 컴포넌트의 props로 설정해줘야 함.

List 컴포넌트

  • 리스트의 전체 크기
  • 각 항목의 높이
  • 각 항목을 렌더링시 사용할 함수
  • 배열 (todos)

위 항목들을 모두 props로 넣어줘야 함.

TodoListItem 수정

TodoList만 수정하면 스타일이 깨지게 되는데, todoListItem 컴포넌트도 일부 수정하면 해결된다.

  1. 전체를 div.TodoListItem-virtualized 로 감싸고
  2. props로 받아온 style을 적용해주면 됨.
import React, { memo } from 'react';
import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';


const TodoListItem = memo(({ 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 TodoListItem;

-> 컴포넌트 사이 테두리를 제대로 쳐주고, 홀수/짝수번째 배경색을 설정하기 위해서임.
(스타일이 깨졌던 이유)

  • 현재 배경색이 사라진 상태이므로, TodoListItem.scss에서 설정을 해줘야 함.

SCSS 설정

  1. 우선 배경색 + 테두리 스타일링을 위한 코드를 지워준다.
&:nth-child(even) {
    background: #f8f9fa;
  }
  
  ...
  
& + & {
    border-top: 1px solid #dee2e6;
  }
  1. scss 파일 최상단에 코드 삽입

.TodoListItem-virtualized {
  &:nth-child(even) {
    background: #f8f9fa;
  }
  & + & {
    border-top: 1px solid #dee2e6;
  }
}
  • TodoListItem-virtualized선택자 한정 스타일 적용.

RESULT

이제 줄무늬 배경 + 테두리가 잘 적용되었다!

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글