본문 바로가기
Project/[클론 코딩] TODO LIST

[클론 코딩] Todo List 컴포넌트 성능 최적화

by Bam_t 2021. 12. 23.
728x90

지난번 포스팅을 통해서 Todo List 앱을 완성시켰습니다. 하지만 최적화 과정이 없이 기능만을 구현했기에 성능 퍼포먼스는 좋다고는 할 수 없습니다. 비록 클론 코딩이지만 실제 개발에서 요구사항을 충족하는 개발에 성능 또한 중요시 여기므로 성능을 최적화 시키는 방법도 알아보겠습니다.


1. 컴포넌트 리렌더링

최적화를 해야하는 이유는 컴포넌트 리렌더링 때문입니다.

웹사이트에서 로딩이 발생하는 이유는 인터넷 대역폭 문제도 있겠지만, 우리가 겪는 로딩이 긴 사이트의 문제점은 웹 사이트의 구성 요소를 불러오는 시간 때문입니다. 즉, 화면에 그려야하는 요소가 많을수록 로딩이 길어지는 것입니다. 리액트를 처음 이야기 할 때, 컴포넌트들을 레고 조립하듯이 만든다고 소개했는데요. 이렇게 조립시킨 컴포넌트가 많아서 렌더링, 리렌더링 되는 일이 많다면 당연히 로딩이 길어지기 때문에 우리는 리렌더링을 필요한 부분에서만 하는 방법이 필요합니다.

그러면 컴포넌트의 리렌더링 조건을 다시 확인하고 가겠습니다.

  • 전달받은 props가 변경될 때
  • 자신의 state가 변경될 때
  • 부모 컴포넌트가 리렌더링 될 때
  • forceUpdate 함수가 실행될 때

 

const TodoList = ({todos, onRemove, onToggle}) => {
    return (
        <div className={"TodoList"}>
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle}/>
            ))}
        </div>
    );
};

앞서 만든 프로그램을 보면 <TodoList> 컴포넌트 내부에서 <TodoListItem>컴포넌트들이 들어있고 동작을 하게 됩니다. 그러면 우리가 Insert나 Toggle, Remove를 하면 TodoList가 리렌더링 되고, 그 여하에 있던 <TodoListItem>또한 리렌더링이 됩니다.

무엇이 문제인지 알게 되었나요? 만약 Todo가 1억개 있다면, id:1의 todo를 수정해도 1억개의 모든 컴포넌트들을 리렌더링 하게 됩니다. 이렇게하면 당연히 리렌더링 시간이 늘어나고 사용자 경험을 크게 저하하게 됩니다.

그렇다면 여기서는 변경된 컴포넌트만 리렌더링 하도록 하고, 나머지 TodoListItem들은 리렌더링을 하지 않도록 해주어야 합니다.

 

 

2. React.memo로 TodoListItem 최적화

위에서 말한 원리대로 성능을 최적화해보도록 하겠습니다. React.memo()를 이용하는데요. React.memo함수는 컴포넌트가 동일한 props를 이용한다면, 다시말해서 props의 변경이 없다면, 리렌더링 하지 않고 가장 최근의 결과를 재사용합니다. 이렇게한다면 1억개의 todos에서 id:1번만 수정해도, id:1번의 todo만 리렌더링되고 나머지는 이전 결과를 재사용하므로 성능 최적화를 해줄 수 있습니다. 주의점은 props 변화만 감지하므로 state 변화에 대해서는 무용지물 입니다.

React.memo의 사용법은 아주 간단합니다. 최적화를 원하는 컴포넌트를 감싸기만 하면 됩니다.

import React from 'react';
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md';
import cn from 'classnames';
import './TodoListItem.scss';

const TodoListItem = ({todo, onRemove, onToggle}) => {
    const {id, text, checked} = todo;

    return (
        <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>
    );
};

export default React.memo(TodoListItem);
export default React.memo(TodoListItem);

 

 

3. onRemove, onToggle 함수의 생성 막기

또 다른 최적화가 필요한 부분이 아직 존재합니다. App.js파일의 onRemove와 onToggle함수가 그 문제입니다.

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

두 함수의 동작을 다시보면 todos 배열을 가지고 동작을 합니다. 이 과정에서 todos가 변하게 되면, 당연히 새로운 todos를 받아서 동작해야하므로 그때마다 두 함수는 새로 만들어지게 됩니다. 이 부분도 개선해 준다면 성능적으로 좋게 될 것 입니다.

함수를 새롭게 계속 만드는 것을 방지하는 방식은 useState 함수형 업데이트 방식이 있습니다.

useState를 보면 다음과 같이 state와 state를 조작하는 setState가 있습니다.

const [todos, setTodos] = useState();

우리가 todos의 변경을 위해 setTodos를 이용했는데, 기존 코드를 보면 setTodos의 인자로 새로운 상태를 직접 넣어주었습니다. 이때 새로운 상태를 직접 넣는 대신에 상태를 어떻게 변화시킬지 정의하는 함수를 넣는 방식이 있는데, 이 방식을 함수형 업데이트라고 합니다. 즉, 값을 직접 전달하는 것보다, 함수를 통해 전달하는 것이 더 좋다는 것입니다. (이 부분에 대해서는 React 카테고리에서 다시 다뤄보도록 하겠습니다.) 자 그럼 아래 코드를 함수형 업데이트로 바꿔보겠습니다.

    const onInsert = useCallback(text => {
        const todo = {
            id: nextId.current,
            text,
            checked: false,
        };
        setTodos(todos.concat(todo));
        nextId.current++;
    }, [todos]);
    const onRemove = useCallback(id => {
        setTodos(todos.filter(
            todo => todo.id !== id
        ));
    }, [todos]);
    const onToggle = useCallback(id => {
        setTodos(todos.map(todo =>
                todo.id === id ? {...todo, checked: !todo.checked} : todo,
            ),
        );
    }, [todos]);

 

함수형 업데이트를 이용한 방식은 간단하게도 setTodos의 todos 배열을 인자로 하는 함수를 전달하고 그 함수 내부에 기존 동작을 작성하면 됩니다. 또한 useCallback의 두번째 파라미터인 배열을 빈 배열로 두면됩니다. 이 배열은 배열 내부로 준 값이 바뀌었을 때 함수를 새로 생성하도록 명시한 값이었지만, 함수형 업데이트를 통해 이 부분을 해결했으므로 비워두면 됩니다.

    const onInsert = useCallback(text => {
        const todo = {
            id: nextId.current,
            text,
            checked: false,
        };
        setTodos(todos => todos.concat(todo));
        nextId.current++;
    }, []);
    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,
                )
            },
        );
    }, []);

참조

https://ko.reactjs.org/docs/react-api.html

 

React 최상위 API – React

A JavaScript library for building user interfaces

ko.reactjs.org

728x90

'Project > [클론 코딩] TODO LIST' 카테고리의 다른 글

[클론 코딩] TODO LIST 기능 구현  (0) 2021.12.21
[클론 코딩] TODO LIST  (0) 2021.12.20

댓글