React.memo Example(Hook)

 

React.memo로 컴포넌트의 리렌더링을 방지하는 예제
React.memo??
컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링을 하도록 하는 함수

state가 변경될 때나 props가 변경될 때 비교하는 방식은 얕은 비교이다.
이는 상위 컴포넌트의 state가 변경될 때 상위 컴포넌트가 리렌더링되며 하위 컴포넌트에 넘기는 props가 재생성되고 props가 동일 값이더라도 동일 참조 값이 아니므로 새로운 값으로 판단하여 리렌더링을 실행한다.

이 문제점을 해결하기 위해 useCallback과 React.memo를 사용한다.

React 성능 최적화
useMemo Example는 특정 결과값을 재사용
useCallback Example 특정 함수를 재사용

send me email if you have any questions.


React.memo

React.memo는 컴포넌트의 props가 바뀌지 않은 경우 리렌더링을 방지하여 성능 최적화를 도와주는 함수이다.
사용법은 React.memo()로 감싸주면 된다.

import React from 'react';

function CreateUser({ username, phonenumber, onChange, onCreate }) {
  return (
    <div>
      <input
        name="username"
        placeholder="input name..."
        onChange={onChange}
        value={username}
      />
      <br />
      <input
        name="phonenumber"
        placeholder="input phonenumber..."
        onChange={onChange}
        value={phonenumber}
      />
      <br />
      <button onClick={onCreate}>submit!!</button>
    </div>
  );
}

export default React.memo(CreateUser);

CreateUser.js


import React, {useEffect} from 'react';

const User = React.memo(function User({ user, onRemove, onUpdate }) {
    useEffect(() => {
        console.log("컴포넌트 등장!");
        console.log(user);
        return () =>{
            console.log("컴포넌트 퇴장..");
            console.log(user);
        }
    }, []);
    return (
        <div>
            <b>{user.username}</b> <span>({user.phonenumber})</span>
            <button onClick={() => onRemove(user.id)}>삭제</button>
            <button onClick={() => onUpdate(user.id)}>수정</button>
        </div>
    )
});

function UserList({ users, onRemove, onUpdate }) {
    return (
        <div>
            {users.map(user => (
                <User user={user} 
                key={user.id} 
                onRemove={onRemove} 
                onUpdate={onUpdate} />
            ))}
        </div>
    );
}

export default React.memo(UserList);

UserList.js

이렇게 수정하면 input을 수정할 때 UserList는 리렌더링 되지 않게 최적화 할 수 있다.
하지만 하나라도 User를 추가하거나 제거, 수정등의 작업을 하면 UserList와 CreateUser도 리렌더링된다.

Why?

const onCreate = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      phonenumber
    };
    setUsers(users.concat(user))

    setInputs({
      username: '',
      phonenumber: ''
    });

    nextId.current += 1;
  }, [users, username, phonenumber]);

  const onRemove = useCallback(
    id => {
      setUsers(users.filter(user => user.id !== id));
    },
    [users]
  );

users의 배열이 바뀔 때 마다 on…함수들이 새로 만들어지기 때문이다.
useCallback의 deps에 users가 있기 때문
이해가 안간다면 useCallback참조

함수형 업데이트를 통한 최적화

기존의 state값 업데이트는 useState가 반환하는 두 번째 원소인 Setter함수에 업데이트 할 값을 파라미터로 넣는다.
함수형 업데이트는 기존 값을 어떻게 업데이트 할 지에 대한 함수를 파라미터로 넣는 방식이다.

위에서는 input을 수정할 때 UserList가 리렌더링되지 않게 하였다.
하지만 UserList가 변하는 경우 다른 컴포넌트들도 함께 리렌더링 되는 문제를 해결하지 못하였다.

이것을 최적화 할 수 있는것이 함수형 업데이트이다.
useCallback의 deps에서 users를 빼버리면 리렌더링을 막을 순 있으나 해당 함수에서 참조하는 users가 최신임을 보장할 수 없다.
함수형 업데이트를 하면 setUsers에 넣는 함수의 파라미터에서 최신 users를 참조할 수 있다.
따라서 deps에 users를 넣지 않아도 된다.

import React, { useRef, useState, useMemo, useCallback } from 'react';
import UserList from './components/UserList';
import CreateUser from './components/CreateUser';

function countUsers(users) {
  console.log("counting users...");

  return users.length;
}

function App() {
  const [users, setUsers] = useState([
    {
      id: 1,
      username: '임준호',
      phonenumber: '010-0000-0000'
    },
    {
      id: 2,
      username: 'testuser1',
      phonenumber: '010-1234-1234'
    },
    {
      id: 3,
      username: 'testuser2',
      phonenumber: '010-2222-4444'
    }
  ]);

  const [inputs, setInputs] = useState({
    username: '',
    phonenumber: ''
  });
  const { username, phonenumber } = inputs;

  const onChange = useCallback(
    e => {
      const { name, value } = e.target;
      setInputs({
        ...inputs,
        [name]: value
      });
    },
    [inputs]
  );

  const nextId = useRef(4);

  const onCreate = useCallback(() => {
    const user = {
      id: nextId.current,
      username,
      phonenumber
    };
    setUsers(users => users.concat(user));

    setInputs({
      username: '',
      phonenumber: ''
    });

    nextId.current += 1;
  }, [username, phonenumber]);

  const onRemove = useCallback(
    id => {
      setUsers(users => users.filter(user => user.id !== id));
    },
    []
  );

  const onUpdate = useCallback(
    id => {
      setUsers(
        users => users.map(user =>
          user.id === id ?
            { id: id, username: username, phonenumber: phonenumber }
            : user
        )
      );

      setInputs({
        username: '',
        phonenumber: ''
      });

    },
    [username, phonenumber]
  );

  const count = useMemo(() => countUsers(users), [users]);

  return (
    <div>
      <CreateUser
        username={username}
        phonenumber={phonenumber}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} onRemove={onRemove} onUpdate={onUpdate} />
      <div>사용자 수 : {count}</div>
    </div>
  );
};

export default App;

App.js
함수형 업데이트를 적용한 코드