React.js - 컴포넌트 최적화

Gyu·2022년 5월 25일
0

React.js

목록 보기
12/20
post-thumbnail

이 글은 react todo-list 프로젝트 기준으로 작성되었습니다.

컴포넌트가 느려지는 원인

  • 아래의 상황에서 컴포넌트 리랜더링이 발생한다.
    1. 자신이 전달받은 props가 변경될 때
    2. 자신의 state가 변경될 때
    3. 부모 컴포넌트가 리렌더링 될 때
    4. forceUpdate() 함수가 실행될 때
  • todo list 리랜더링 과정
    • '할 일 1' 항목 체크 → App 컴포넌트의 state 변경 → App 컴포넌트 리렌더링 → App 컴포넌트의 모든 자식 컴포넌트 리렌더링
  • 만약 항목이 3000개 라고 가정할 때, '할 일 1'을 체크하면 해당 항목은 리렌더링 되는 것이 맞지만 나머지 2999개의 항목이 리렌더링 되면 속도가 느려진다.
  • 이럴 때는 컴포넌트 리렌더링 성능을 최적화해 주는 작업을 해야한다. 즉 불필요한 리렌더링을 방지하는 작업이 필요하다.

React.memo 사용

  • 클래스 컴포넌트에서는 shouldComponentUpdate() 라이프사이클을 사용하여 리렌더링을 방지할 수 있다. 그러나 함수형 컴포넌트에서는 라이프사이클 훅을 사용할 수 없다. 때문에 함수형 컴포넌트에서는 React.memo 함수를 사용한다.
  • React.memo 함수를 사용하면 해당 컴포넌트의 props가 바뀌지 않는다면 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 할 수 있다.
  • TodoItem 컴포넌트에 React.memo 적용
    import React, { useState, useCallback } from 'react';
    import cn from 'classnames';
    import './TodoItem.scss'
    
    const TodoItem = ({ todo, onChangeTodo, onRemoveTodo }) => {
        ...
    };
    
    export default React.memo(TodoItem); // 컴포넌트를 React.memo로 감싸주면, 적용 완료
    
    // todo, onChangeTodo, onRemoveTodo가 변경되지 않으면 리렌더링을 하지 않는다.
  • 리스트와 관련된 컴포넌트를 최적화할 때는 리스트 내부에서 사용하는 컴포넌트(TodoItem)도 최적화해야하고, 리스트로 사용되는 컴포넌트(TodoList) 자체도 최적화 시켜주는 것이 좋다.
  • TodoList는 todos 배열이 업데이트 될 때 뿐만 아니라 filterOption이 변경 될 때도 리렌더링을 하기 때문에 최적화가 필요하다.
    import React from 'react';
    import TodoItem from './TodoItem';
    
    const TodoList = ({ todos, onChangeTodo, onRemoveTodo }) => {
        ...
    };
    
    export default React.memo(TodoList);

함수 최적화

  • React.memo를 사용하는 것만으로 컴포넌트 최적화가 끝나지 않는다. 현재 프로젝트에서는 todos 배열이 업데이트되면 onChangeTodo, onRemoveTodo 함수도 새롭게 바뀌기 때문이다.
  • onChangeTodo, onRemoveTodo 함수는 todos 배열을 변경 할 때 최신 todos를 참조하기 때문에 todos가 변경될 때 마다 함수가 새로 만들어진다. 이렇게 함수가 계속 새롭게 만들어지는 상황을 방지하는 두 가지 방법이 있다.
    1. useState() 의 함수형 업데이트 사용
    2. useReducer() 사용

useState()의 함수형 업데이트 사용

  • setState() 함수의 매개변수로 새로운 상태 값을 넣을 수 있지만, 상태 업데이트를 어떻게할지 정의해주는 업데이트 함수를 넣을 수 있다. 이를 함수형 업데이트라고 한다.( React - state 참조)
  • setState() 함수의 매개변수가 함수인 경우 이전 state 값을 사용할 수 있다.
    // 함수형 업데이트 예제
    const [ number, setNumber ] = useState(0);
    
    const onIncrease = useCallback(
    	() => setNumber(prevNumber => prevNumber + 1), // 이전값에 접근 가능
    	[]
    );
    /*
    setNumber(number + 1)이 아니라 위 코트처럼 어떻게 number를 업데이트할지 정의해주는
    업데이트 함수를 넣어주게 되면 useCallback을 사용할 때 두 번째 매개변수로 넣는 배열에
    number를 넣지 않아도 된다.
    */ 
  • 함수형 업데이트를 onAddTodo, onChangeTodo, onRemoveTodo에 적용하기
    const onAddTodo = useCallback((text) => {
      if(text.trim() === '') return;
    
      const newTodo = {
        id: nextId.current,
        text: text,
        isDone: false,
        isImportant: false
      }
    
      // const nextTodos = [...todos, newTodo];
      // setTodos(nextTodos);
      setTodos(todos => [...todos, newTodo]);
      nextId.current++;
    }, []);
    
    const onChangeTodo = useCallback((id, prop, value) => {
      setTodos(todos =>
        todos.map(todo => { // 불변성 때문에.... 이렇게 수정해야함..
            return todo.id === id ? {...todo, [prop]: value} : todo;
          }
        )
      );
    }, []);
    
    const onRemoveTodo = useCallback((id) => {
      setTodos(todos => todos.filter(todo => todo.id !== id));
    }, [])

useReducer 사용하기

  • useReducer를 사용해도 함수가 새롭게 생기는 문제를 해결할 수 있다.
function todoReducer(todos, action) { // reducer 함수
  switch (action.type) {
    case 'INSERT':
      return todos.concat(action.todo);
    case 'UPDATE':
      return todos.map(todo => 
          todo.id === action.id ? {...todo, [action.prop]: action.value} : todo
      )
    case 'REMOVE': 
      return todos.filter(todo => todo.id !== action.id);
    default:
      return todos;
  }
}

const App = () => {
	const [ todos, dispatch ] = useReducer(todoReducer, [
      { id: 1, text: '집가기', isDone: false, isImportant: false },
      { id: 2, text: '영화 보기', isDone: false, isImportant: false },
      { id: 3, text: '아무것도 안하기', isDone: true, isImportant: true },
    ]
  );

	const onAddTodo = useCallback((text) => {
    if(text.trim() === '') return;

    const newTodo = {
      id: nextId.current,
      text: text,
      isDone: false,
      isImportant: false
    }

    dispatch({type: 'INSERT', todo: newTodo});
    nextId.current++;
  }, []);

  const onChangeTodo = useCallback((id, prop, value) => {
    dispatch({type: 'UPDATE', id, prop, value});
  }, []);

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

불변성의 중요성

  • 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 '불변성을 지킨다.'라고 한다.
  • 리액트 컴포넌트에서 상태를 업데이트 할 때 불변성을 지키는 것은 매우 중요하다. 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.
  • 전개 연산자를 사용하여 복사할 경우 얕은 복사가 된다. immer 같은 라이브러리를 사용하면 구조가 복잡한 객체를 매우 짧은 코드로 불변성을 유지하며 업데이트할 수 있다.
profile
애기 프론트 엔드 개발자

0개의 댓글