본문 바로가기

Computer Programming/[리액트를 다루는 기술]

[리액트를 다루는 기술] 11. 컴포넌트 성능 최적화

1. React DevTools

 

 

성능 분석 툴로 몇 초 걸렸는지 측정할 수 있다. 리렌더링은 4.2ms 정도가 걸렸고

랭크 아이콘을 눌러보면 리렌더링된 컴포넌트를 오래 걸린 순으로 정렬해서 보여준다. 

 

그리고 아래쪽을 보면 변화를 일으킨 컴포넌트랑 관계 없는 컴포넌트들도 리렌더링된 것을 볼 수 있다. 이것들을 선택하면 어디부터 어디까지 렌더링이 몇 초 걸렸는지도 확인할 수 있다. 단순히 '느리다'라는 표현을 쓰기보다는 정확히 몇 초에서 몇 초로 단축되었는지 분석하고 해결하는 것이 중요하다..!!

 

 

불필요한 리렌더링을 어떻게 방지할 수 있을까?

 

 

2. React.memo

-React.memo를 통해 props가 변경되지 않았다면 리렌더링 하지 않도록 설정할 수 있다.

 

import React from 'react'

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

export default React.memo(TodoListItem);

export에 감싸주면 이제 props인 todo, onRemove, onToggle이 변화하지 않으면 리렌더링되지 않는다.

 

그러나 App 컴포넌트에서 전달하는 onRemove 와 onToggle 함수는 App 컴포넌트의 state가 바뀌면(todos 배열이 업데이트되거나 삭제되면) 리렌더링이 되고, App 컴포넌트 안의 두 함수는 모두 새로 만들어진다. 이에 따라 React.memo만 사용한다고 해서 함수의 재생성을 막을 수 있는 것은 아니다.

 

 

 

3. useState의 함수형 업데이트

따라서 추가로 useState 에서 함수형으로 업데이트를 하면 좋다.

 

함수형 업데이트는 아래와 같이 하면 된다.

  const [number, setNumber] = useState(0);
  const onIncrease = useCallback(() => {
    setNumber(prev => prev + 1 )
  }, [])

이렇게 하면 useCallback 의 두번재 인자로 넣는 dependency에 number 을 추가하지 않아도 된다. 이러면 첫 렌더링 시에만 함수가 생성되고 함수가 계속해서 만들어지는 상황을 막을 수 있다.

 

이제 App.js 컴포넌트의 onRemove, onInsert, onToggle 함수를 이 함수형 업데이트를 통해 setState을 사용해주면 된다.

 

function App() {
  const [todos, setTodos]= useState(createBulkTodos)
  const nextId = useRef(5)

  const onInsert = useCallback((text) => {
    const nextTodo = {
      id: nextId.current,
      text,
      checked: false,
    }
    setTodos(todos => todos.concat(nextTodo))
    nextId.current += 1
  },[])

  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 (...)
  
}

 

onInsert, onRemove, onToggle의 setTodos 를 함수형으로 업데이트를 해줌으로서 dependency를 제거하고 성능 측정을 다시 해봤다.

데이터 개수를 2500개로 바꿨는데도 불필요한 렌더링은 없어지니 성능이 크게 향상된 것이라고 볼 수 있다.

 

 

컴포넌트 성능 최적화 : React.memo를 통해 props가 바뀌지 않으면 렌더링을 하지 않게 하고, props 중 함수인 경우 useState 함수형 업데이트를 통해 항상 최신의 상태를 참조하는 것이 아닌, 이전의 prev 상태로 업데이트를 정의해준다.

 

 

OR

4. 불변성의 중요성

 

  const onToggle = useCallback((id) => {
    setTodos(todos => todos.map(todo => 
      todo.id === id ? {...todo, checked: !todo.checked } : todo
    ))
  }, [])

check를 toggle 해주는 함수를 살펴보면 기존 데이터를 직접 수정하지 않고, 새로운 배열을 만든 다음 새로운 객체를 만들어 필요한 부분을 교체해주는 방식으로 구현한다.

 

이렇게 값을 직접 수정하지 않고 새로운 값을 만들어 내는 것을 '불변성을 지킨다'라고 함.

 

 

=> 새로운 값을 만들어내기 때문에 React.memo에서 props 가 바뀌었다고 인식할 수 있는 것이다. 그렇지 않으면 인식을 못하므로 최적화도 어렵다.

 

 

+) spread 연산자는 얕은복사만 하게 되므로 주의할 것. 내부의 값 또한 따로 복사해줘야 한다.

 

nextTodos[0] = {
   ...nextTodos[0],
   checked: false
}

만약 객체 안에 있는 객체까지 복사한다면 복잡해질 수 있으므로 immer 이라는 라이브러리의 도움을 받을 수 있다.

 

 

* 리스트에 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트를 모두 최적화하는 것이 좋다. 즉, TodoList와 TodoListItem 두개이다. (미래 추가될 기능 대비와 유지보수를 위해)

-> 리스트 관련 코드는 리스트 아이템과 리스트 모두 최적화하자!