TIL | #16 React | 렌더링 최적화React.memo, react-virtualized, immer

trevor1107·2021년 4월 11일
0

2021-04-06(화)

렌더링 최적화 | 리액트에서 무수히 많은 데이터들이 불필요하게 리렌더링 된다면 어떻게 해결 해야될까?

리액트에서 리렌더링 되는 조건

  • 부모 컴포넌트가 리렌더링 되었을 때
  • props가 변경될 때
  • state가 변경될 때
  • forceUpdate()를 실행할 때

컴포넌트 리렌더링 방지

클래스형 : shouldComponentUpdate
함수형 : React.memo
컴포넌트의 props가 바뀌지 않았다면 리렌더링을 하지 않도록 설정하여 리 렌더링 성능을 최적화 한다.

export default React.memo(TodoListItem);

리렌더링을 방지하기위해 useCallback부분을 아래와 같이 수정한다.

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

리듀서를 사용해서 바꾸기

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', todo: {id:1}}
            return todos.filter((todo) => todo.id !== action.id);
        case 'TOGGLE':
            // {type:'TOGGLE', todo: {id:1}}
            return todos.map((todo) =>
                todo.id === action.id
                    ? { ...todo, checked: !todo.checked }
                    : todo,
            );
        default:
            break;
    }
}

const [todos, dispatch] = useReducer(
    todoReducer,
    undefined,
    createBulkTodos,
);

const onInsert = useCallback(
        (text) => {
            const todo = {
                id: nextId.current,
                text,
                checked: false,
            };
            // 1. setTodos(todos.concat(todo));
            // setTodos((todos) => todos.concat(todo));
            dispatch({ type: 'INSERT', todo });
            nextId.current += 1;
        },
        // 1. [todos],
        [],
    );
    const onRemove = useCallback(
        (id) => {
            // setTodos(todos.filter((todo) => todo.id !== id));
            // setTodos((todos) => todos.filter((todo) => todo.id !== id));
            dispatch({ type: 'REMOVE', id });
        },
        // [todos],
        [],
    );

얕은 복사와 깊은 복사

리액트는 기존의 값과 새로운 값과 비교해서 렌더링 여부를 결정하는데, 불변성을 유지시키기 위해서는 깊은 복사가 필요하다.

const todos = [
    { id: 1, checked: true },
    { id: 2, checked: true },
];
const nextTodos = [...todos];
nextTodos[0].checked = false;

// 얕은복사로 똑같은 객체를 가리키고 있다.
console.log(todos[0] === nextTodos[0]); // true

// 새로운 객체를 할당
nextTodos[0] = {
    ...nextTodos[0],
    checked: false,
};
console.log(todos[0] === nextTodos[0]); // false

// 깊은복사. 오브젝트 안에 오브젝트를 새로운 객체로 받는 방법
const compleObject = [
    {
        objectInside: {
            checked: true,
        },
    },
    {
        objectInside: {
            checked: true,
        },
    },
];
const nextCompleObject = {
    ...compleObject,
    objectInside: {
        ...compleObject.objectInside,
        checked: false,
    },
};
console.log(compleObject === nextCompleObject); // false
console.log(compleObject.objectInside === nextCompleObject.objectInside); // false

불변성 유지를 위한 모듈 immer, 최적화 모듈 react-virtualized이 있다.

최적화 react-virtualized module

// TodoList.js
import React, { useCallback } from 'react';
import { List } from 'react-virtualized';
import TodoListItem from './TodoListItem';
import './TodoList.scss';

const TodoList = ({ todos, onRemove, onToggle }) => {
    const rowRenderer = useCallback(
        ({ index, key, style }) => {
            const todo = todos[index];
            return (
                <TodoListItem
                    todo={todo}
                    key={key}
                    onRemove={onRemove}
                    onToggle={onToggle}
                    style={style}
                />
            );
        },
        [onRemove, onToggle, todos],
    );

    return (
        <List
            className="TodoList"
            width={495}
            height={513}
            rowCount={todos.length}
            rowHeight={57}
            rowRenderer={rowRenderer}
            list={todos}
            style={{ outline: 'none' }}
        />
    );
};

export default React.memo(TodoList);
// TodoListItem.js

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

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

    return (
        <div className="TodoListItem-virtualized" style={style}>
            <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>
        </div>
    );
};

export default React.memo(
    TodoListItem,
    (prevProps, nextProps) => prevProps.todo === nextProps.todo,
);

불변성 유지 immer module

아래는 기본 사용 에제이다.

const originalState = [
    { id: 1, todo: '불변성 유지', cheked: false },
    { id: 2, todo: 'immer 라이브러리 사용하기', cheked: true },
];

// originalState : 수정하고 싶은 상태
// draft : 상태를 어떻게 업데이트할 것인지 정의하는 함수

// 두번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면
// produce함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해준다.
const nextState = produce(originalState, (draft) => {
    const todo = draft.find((t) => t.id === 2);
    todo.checked = true; // == draft[1].cheked = true;
    draft.push({
        id: 3,
        todo: '데이터 추가',
        cheked: false,
    });
    draft.splice(
        draft.findIndex((t) => t.id === 1),
        1
    );
});

immer 모듈의 produce를 사용한다면 달라지는 부분은 draft로 상태에 접근해서 바꿔주어도 불변성유지가 가능하다는 것이다.
그리고 useCallback에서 두번째 매개변수로 들어가는 의존상태의 리스트를 설정해주지 않아도 된다

// App.js

import produce from 'immer';
import React, { useState, useRef, useCallback } from 'react';

const App = () => {
    const nextId = useRef(1);
    const [form, setform] = useState({ name: '', username: '' });
    const [data, setData] = useState({
        array: [],
        uselessValue: null,
    });

    // input 수정
    const onChange = useCallback(
        (e) => {
            const { name, value } = e.target;
            // setform({
            //     ...form,
            //     [name]: [value],
            // });
            setform(
                produce(form, (draft) => {
                    draft[name] = value;
                })
            );
        },
        // [form]
        []
    );

    const onSubmit = useCallback(
        (e) => {
            e.preventDefault();
            const info = {
                id: nextId.current,
                name: form.name,
                username: form.username,
            };
            // array에 새 항목 등록
            // setData({
            //     ...data,
            //     array: data.array.concat(info),
            // });
            setData(
                produce(data, (draft) => {
                    draft.array.push(info);
                })
            );
            // form초기화
            // setform({
            //     name: '',
            //     username: '',
            // });
            setform(
                produce(form, (draft) => {
                    draft.name = '';
                    draft.username = '';
                })
            );
            nextId.current += 1;
        },
        // [data, form.name, form.username]
        []
    );

    const onRemove = useCallback(
        (id) => {
            // setData({
            //     ...data,
            //     array: data.array.filter((info) => info.id !== id),
            // });
            setData(
                produce(data, (draft) => {
                    draft.array.splice(
                        draft.array.findIndex((info) => info.id === id),
                        1
                    );
                })
            );
        },
        // [data]
        []
    );

    return (
        <div>
            <form onSubmit={onSubmit}>
                <input name="username" placeholder="아이디" 
			value={form.username} onChange={onChange} />
                <input name="name" placeholder="이름" 
			value={form.name} onChange={onChange} />
                <button type="submit">클릭</button>
            </form>
            <div>
                <ul>
                    {data.array.map((info) => (
                        <li key={info.id} 
			    onClick={() => onRemove(info.id)}>
                            {info.username}({info.name})
                        </li>
                    ))}
                </ul>
            </div>
        </div>
    );
};

export default App;
profile
프론트엔드 개발자

0개의 댓글