React 컴포넌트 성능 최적화 하기

욱잔·2022년 7월 22일
0

React

목록 보기
1/1
post-thumbnail

💡 느려지는 원인 분석

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

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

컴포넌트의 개수가 많지 않다면 모든 컴포넌트를 리렌더링해도 성능 저하를 체감하기 어렵다.

그러나, 수천개가 넘는 등의 상황이라면 성능 저하가 심할 것이다.

이럴 때는 컴포넌트 리렌더링 성능을 최적화해 주는 작업이 필요하다.

즉, 리렌더링이 불필요할 때는 리렌더링을 방지해 주어야 한다.

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

컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있다.

다음과 같이 컴포넌트를 만들고 나서 React.memo로 감싸 주면된다.

import '...';

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

export default React.memo(TodoListItem);

이렇게하면 TodoListItem 컴포넌트는 자신의 props인 todo, onRemove, onToggle 이 바뀌지 않으면 리렌더링을 하지 않을 것이다.

그러나 React.memo를 사용하는 것만으로 컴포넌트 최적화를 완벽히 했다고 볼 수는 없다.

todos 배열이 업데이트되면 onRemove와 onToggle 함수도 새롭게 바뀌기 때문이다.

💡 useState의 함수형 업데이트

useState 를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣어줄 수도 있다. 이를 함수형 업데이트라고 부른다.

// 임의의 Todos 배열을 생성해주는 함수
const createBulkTodos = () => {
	const array = [];
	  for (let i = 1; i <= 2500; i++) {
	    array.push({
	      id: i,
	      text: `할 일 ${i}`,
	      checked: false,
	    });
	  }
	
  return array;
};

const [todos, setTodos] = useState(createBulkTodos);

// ❌ 새로운 상태를 파라미터로 넣어주는 업데이트
const onRemove = useCallback(
	(id) => setTodos(todos.filter(todo => todo.id !== id));
, []);

// ✅ 함수형 업데이트
const onRemove_ = useCallback(
	// prevTodos는 현재 todos 값을 가리킨다.
	(id) => setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
, []);

onRemove 함수는 배열 상태를 업데이트하는 과정에서 최신 상태의 todos를 참조하기 때문에 todos 배열이 바뀔 때마다 함수가 새로 만들어진다.

함수형 업데이트를 이용하여 이렇게 함수가 계속 만들어지는 상황을 방지하는 것이다.

💡 useReducer 를 이용한 state 관리

useState의 함수형 업데이트를 사용하는 대신, useReducer를 사용해도 onRemove가 계속 새로워지는 문제를 해결할 수 있다.

const todoReducer = (todos, action) => {
  switch (action.type) {
    case 'REMOVE': // 제거
      return todos.filter((todo) => todo.id !== action.id);
    default:
      return todos;
  }
};

function App() {
	//...

	const [todos, setTodos] = useReducer(todoReducer, undefined, createBulkTodos);
	
	const onRemove = useCallback((id) => {
    dispatch({ type: 'REMOVE', id });
  }, []);

	return (
		//...
	)

}

export default App;

useReducer를 사용할 때는 원래 두 번째 파라미터에 초기 상태를 넣어 주어야 한다.

대신에 두 번째 파라미터에 undefined를 넣어주고, 세 번째 파라미터에 초기 상태를 만들어주는createBulkTodos를 넣어 주었다.

이렇게하면 컴포넌트가 맨 처음 렌더링될 때만 createBulkTodos 함수가 호출된다.

useReducer를 사용하는 방법은 기존 코드를 많이 고쳐야 한다는 단점이 있지만, 상태를 업데이트 하는 로직을 모아서 컴포넌트 바깥에 둘 수 있다는 장점이 있다.

성능상으로는 함수형 업데이트와 비슷하기 때문에 두 방식은 취향에 따라 결정하면 될 것 같다.


[참고내용 출처] 리액트를 다루는 기술 write by 김민준

profile
📒 나의 컴포넌트 일기장

0개의 댓글