컴포넌트 렌더링 최적화(1)

오형근·2022년 3월 13일
0

React

목록 보기
3/5

이번 글 부터는 평소 궁금해하고 깊게 다뤄보고자 하였던 Rendering Time과 관련된 이야기를 써보고자 한다.

관련된 내용은 벨로퍼트님의 리액트를 다루는 기술에서 상당 부분을 가져왔고, 일부 추가적으로 궁금한 내용들은 구글링을 통해 해결하였다.

시작하기 전에 React의 Hooks인 Memo, useCallback에 대한 충분한 이해가 필요하다!


기본적인 공부 방법들이 모두 궁금증에서 시작되기 때문에, 이 글 또한 렌더링 최적화를 왜 하는것인지에 대한 이야기에서 시작해보고자 한다.

왜 렌더링 최적화를 진행하는가?

시간이 지날수록 더 업그레이드 되고 세분화된 정보를 다루다 보면 애플리케이션에서 다루고자 하는 데이터의 용량이 거대해지는 것은 시간문제일 것이다. 이러한 경우 애플리케이션의 렌더링 속도가 느려지는 것은 당연하고, 이는 곧 UX에 치명적인 문제점으로 다가오기 때문에 렌더링 최적화를 진행하는 것은 필수적이라고 할 수 있다.

책에서는 크롬 개발자 도구를 활용한 성능 모니터링을 진행했는데, 본래 데이터의 크기가 10개 이하일 때의 렌더링 속도와 2500개일 때의 렌더링 속도는 확연히 차이가 남을 알 수 있었다.

왜 느려지는가?

책에서 다룬 느려짐의 원인을 알아보도록 하자.

React의(물론 다른 프레임워크도 마찬가지) 컴포넌트들은 다음과 같은 네 가지 상황에서 리렌더링이 발생한다.

-자신이 전달받은 props가 변경될 때
-자신의 state가 바뀔 때
-부모 컴포넌트가 리렌더링될 때
-forceUpdate 함수가 실행될 때(자세한 설명은 안 하겠다. 강제 render()함수를 실행하게 하는 친구라고 하자)

특히 map과 같은 함수로 렌더링을 실행하면 배열의 길이에 따라 쉽게 컴포넌트의 개수가 증가하기 때문에 이런 상황에서 렌더링 최적화는 필수적이다.

어떻게 진행하는가?

이럴 때에는 state의 변화에 영향을 받는 컴포넌트를 제외한 나머지 컴포넌트들의 리렌더링을 방지해 주어야 하는데, 책에서는 이를 다양한 방법으로 소개하고 있다.

React.memo를 이용한 최적화

React 내장 함수인 React.memo를 사용하면 props가 바뀌지 않은 컴포넌트는 리렌더링하지 않도록 설정할 수 있다!

사용법은 그렇게 어렵지 않다. 간단한 예시를 살펴보자.

component.js

import React from 'react';

const Component = () => {
	return(...)
}

export default React.memo(Component);

위와 같이 React.memo 함수로 컴포넌트를 감싸주기만 하면 간단하게 리렌더링 방지를 구현할 수 있다.

useState를 이용한 최적화

안타깝게도 React.memo를 사용하는 것만으로는 state가 업데이트될 시 이를 재참조하는 함수들의 리렌더링을 방지하지는 못한다.

이렇게 함수가 매번 만들어지는 상황을 방지하기 위한 첫 번째 방법으로는 useState함수형 업데이트 기능을 사용하는 것이 있다.

함수형 업데이트?

기존에 setState함수를 사용하는 경우 새로운 상태 자체를 파라미터로 넣는 방법이 있었겠지만, 이 방법 대신 상태 업데이트 방법을 정의해주는 함수를 파라미터로 넣을 수 있다(실제로 권장되는 방법이다).

예시로 확인해 보자

component.js

const[ number, setNumber ] = useState(0);

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

위의 경우처럼 setNumber(number + 1)을 하는 것이 아니라 어떻게 업데이트할지 정의해주는 업데이트 함수를 넣는 방법이다. 이처럼 하면 number를 뭐로 바꿀지를 판단하는 것이 아닌 어떻게 바꿀지를 판단하기 때문에 어떤 number가 와도 상관이 없다. 따라서 state가 업데이트된다고 함수까지 리렌더링 될 이유가 없어지는 것이다(어떤 state가 와도 변경 로직을 알고 있기 때문이다)!

useReducer를 이용한 최적화

useReducer를 사용해도 비슷한 성능으로 최적화가 가능하다.

다음 예제에서는 onToggle, onRemove 함수를 최적화한다. 그 과정을 살펴보자.

App.js

import { useReducer, useRef, useCallback } from 'react';
(...)
 
(...)
function todoReducer(todos, action) {
	switch(action.type) {
      case 'INSERT' : //새로 추가
        // { type: 'INSERT', todo: { id: 1, text: 'todo', checked: false } }
        return todos.concat(action.todo);
      case 'REMOVE': // 제거
        // { type: 'REMOVE', id: 1 }
        return todos.filter(todo => todo.id !== action.id);
      case 'TOGGLE': // 토글
        // { type: 'REMOVE', id: 1 }
        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);
  (...)
   
  	dispatch({ type: 'INSERT', todo });
	(...)
}, []);

cosnt onRemove = useCallback(id => {
  dispatch({ type: 'REMOVE', id });
}, []);

cosnt onToggle = useCallback(id => {
  dispatch({ type: 'TOGGLE', id });
}, []);

return (...)
        
export default App;
    

위의 경우처럼 따로 action마다의 함수를 케이스별로 나눠주면 그 함수를 useCallback을 이용하여 첫 렌더링에서만 정의하고 추후의 재정의를 방지할 수 있게 된다.

위의 케이스에서 useReducer의 두 번째 파라미터에는 본래 초기 상태를 넣어주어야 한다. 지금은 그 대신 두 번째 파라미터에 undefined를 넣고, 세 번째 파라미터에 초기 상태를 만들어주는 함수인 createBulktodos를 넣어주었다. 이렇게 하면 컴포넌트가 맨 처음 렌더링될 때만 createBulktodos 함수가 호출된다.

useReducer를 사용하는 방법은 기존 코드를 많이 고치고 switch를 사용할 함수를 새로 정의해주어야한다는 단점이 있지만, 상태 업데이트 로직들을 모아서 따로 두고 관리하기 좋다는 장점이 있다.

함수 자체를 따로 모아 관리하는 것은 좋은 습관이라고 생각한다!


이쯤에서 내용을 마무리하고 다음 글에서 불변성에 대한 내용과 react-virtualized를 이용한 최적화 등을 다루려고 한다.

useMemo, useState의 함수 파라미터 대입, useReducer를 활용한 관리용 함수 제작 모두 장단점을 기억하도록 하자!

profile
https://nohv.site

0개의 댓글