React.memo 를 사용한 컴포넌트 리렌더링

Hyun·2021년 9월 28일
0

리액트 기초

목록 보기
12/18
post-thumbnail

컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있는 React.memo 라는 함수에 대해 알아보자.

이 함수를 사용하면 컴포넌트가 리렌더링이 필요한 상황에서만 리렌더링이 되도록 설정해 줄 수 있다.

사용법은 간단하다. React.memo 로 감싸주기만 하면 된다.

CreateUser.js

import React from "react";

function CreateUser({username, email, onChange, onCreate, onUpdate}){
  return(
    <div>
      <input 
      name="username"
      placeholder="계정명"
      onChange={onChange}
      value={username}
      />
      <input 
      name="email"
      placeholder="이메일"
      onChange={onChange}
      value={email}
      />
      <button onClick={onCreate}>등록</button>
      <button onClick={onUpdate}>업데이트</button>
    </div>
  )
}

export default React.memo(CreateUser);

UserList.js

import React from 'react';

const User = React.memo(function User({user, onRemove, onToggle, onModify}){
  return(
    <div>
      <b
      style={{
        cursor: 'pointer',
        color: user.active ? 'green' : 'black'
      }} 
      onClick={()=> onToggle(user.id)}>{user.username}</b> <span>{user.email}</span>
      <button onClick={() => onRemove(user.id)}>삭제</button>
      <button onClick={() => onModify(user)}>수정</button>
    </div>
  )
})

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

export default React.memo(UserList);

적용을 다 마친 후, input 을 수정할 때 하단의 UserList 가 리렌더링이 되지 않는 것을 확인할 수 있다.

그런데, User 중 하나라도 수정하면 모든 User 들이 리렌더링되고, CreateUser 도 리렌더링이 되는 것을 볼 수 있다.

이유는 간단하다. users 배열이 바뀔 때마다 onCreate, onToggle, onRemove, onModify, onUpdate 함수들이 새로 만들어지기 때문이다. (deps 에 users 가 들어있기 때문)
따라서 이 함수들을 참조하는 컴포넌트들은 리렌더링 될 수밖에 없다.

const onCreate = useCallback(() => {
    const newUser ={
      id: nextId.current,
      username:username,
      email:email
    }
    setUsers([
      ...users, newUser//users=객체모음, newUser=추가힐 객체
    ])
    setInput({
      username: '',
      email: ''
    })

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

  const onUpdate = useCallback(() => {
    setUsers(
      users.map(user => user.id === id ? {...user, username: username, email : email} : user)
    )
    setInput({
      username: '',
      email: '',
      id: '',
    })
  },[users, id, username, email])

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

  const onToggle = useCallback((id) => {
    setUsers(
      users.map(user => user.id === id? {...user, active : !user.active} : user)
    )
  }, [users])
  //파라미터 id를 useCallback의 두번째 인자에 안써도 되는 이유는 useCallback함수를 사용할때마다 인자에 id가 입력받기 때문이다. 우리는 useCallback안에서 사용되는 상태 or props만 신경쓰면 된다.

  const onModify = useCallback((user) => {
    setInput({
      username: user.username,
      email: user.email,
      id: user.id
    })
  }, [])

deps 에 users 가 들어있기 때문에 배열이 바뀔 때마다 함수가 새로 만들어지는건 당연하다. 그렇다면 이것을 최적화하고 싶다면 어떻게 해야할까?

바로 deps 에서 users 를 지우고, 함수들에서 현재 useState 로 관리하는 users 를 참조하지 않게 하는것이다.

(중요!!)
이것은 함수형 업데이트로 해결할 수 있는데, 함수형 업데이트를 하게 되면, setUsers 에 등록하는 콜백함수의 파라미터에서 항상 최신 users 를 참조할 수 있기 때문에 users 배열이 바뀔 때마다 함수를 재선언하지 않아도 되고, 따라서 deps 에 users 를 넣지 않아도 된다.

그럼 각 함수들을 업데이트 해보자 (onChange 의 경우, 함수형 업데이트를 해도 영향은 가지 않지만, 연습삼아 해주었다).

3줄 요약
1. React.memo 를 사용하여 컴포넌트의 리렌더링이 필요없는 경우 리렌더링 하지 않도록 설정한다.
2. User 중 하나만 수정해도 결국 users 가 변경되기 때문에 users 를 deps 로 가지는 함수들이 리렌더링되고, 따라서 이 함수를 참조하는 컴포넌트들도 리렌더링된다.
3. 현재 useState 로 관리하는 users 를 참조하지 않고 setUser 에 등록하는 콜백함수의 파라미터에서 users 를 참조한다 => 재선언하지 않고도 항상 최신 users 를 참조할 수 있음.

App.js

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

const countActiveUsers = (users) => {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active === true).length;
}

const App = () => {

  const [input, setInput] = useState({
    username: '',
    email: '',
    id: ''
  }) 

  const {username, email, id} = input;

  const onChange = useCallback((e)=>{
    const {name, value} = e.target;
    setInput(input => ({ //<- Point!
      ...input,
      [name]:value
    }))
  }, [])

  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]);
  
  const nextId = useRef(4);

  const onCreate = useCallback(() => {
    const newUser ={
      id: nextId.current,
      username:username,
      email:email
    }
    setUsers(users => ([ //<- Point!
      ...users, newUser//users=객체모음, newUser=추가힐 객체
    ]))
    setInput({
      username: '',
      email: ''
    })

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

  const onUpdate = useCallback(() => {
    setUsers(users => ( //<- Point!
      users.map(user => user.id === id ? {...user, username: username, email : email} : user)
    ))
    setInput({
      username: '',
      email: '',
      id: '',
    })
  },[id, username, email])
  //id를 인자로 받아오는게 아니기 때문에 id가 바뀔 때마다 함수를 재선언해주어야 한다.

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

  const onToggle = useCallback((id) => {
    setUsers(users => //<- Point!
      users.map(user => user.id === id? {...user, active : !user.active} : user)
    )
  }, [])
  //파라미터 id를 useCallback의 두번째 인자에 안써도 되는 이유는 useCallback함수를 사용할때마다 인자로 id를 입력받기 때문이다. 우리는 useCallback안에서 사용되는 상태 or props만 신경쓰면 된다.

  const onModify = useCallback((user) => {
    setInput({
      username: user.username,
      email: user.email,
      id: user.id
    })
  }, [])
  
  const count = useMemo(() => countActiveUsers(users), [users]);

  return(
    <div>
      <CreateUser username={username} email={email} onChange={onChange} onCreate={onCreate} onUpdate={onUpdate}/>
      <UserList users={users} onRemove={onRemove} onToggle={onToggle} onModify={onModify}/>
      <div>활성사용자 수: {count}</div>
    </div>
  )
}

export default App;

이렇게 해주면, 특정 항목을 수정하게 될 때, 아래의 예시처럼 해당 항목만 리렌더링 될것이다.
*CreateUser 도 렌더링 되는것처럼 보이는데, 실제로 console.log 를 찍었을 때 렌더링이 되지 않는 것을 확인할 수 있다.

//CreateUser.js
function CreateUser({username, email, onChange, onCreate, onUpdate}){
  console.log("CreateUser RENDERING");
  return(
    ...

이렇게 되면 최적화가 끝난 것이다. 리액트 개발을 할 때, useCallback, useMemo, React.memo컴포넌트의 성능을 실제로 개선할 수 있는 상황에서만 해야한다. 예를 들어서, User 컴포넌트에 bbuttononClick 으로 설정해준 함수들은 해당 함수들을 useCallback 으로 재사용한다고 해서 리렌더링을 막을 수 있는 것은 아니므로, 굳이 그렇게 할 필요가 없다.

추가적으로, 렌더링 최적화 하지 않을 컴포넌트에 React.memo 를 사용하는 것은 불필요한 props 비교만 하는 것이다. 따라서 실제로 렌더링을 방지할 수 있는 상황인 경우에만 사용해야 한다.

profile
better than yesterday

0개의 댓글