컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있는 React.memo 라는 함수에 대해 알아보자.
이 함수를 사용하면 컴포넌트가 리렌더링이 필요한 상황에서만 리렌더링이 되도록 설정해 줄 수 있다.
사용법은 간단하다. React.memo 로 감싸주기만 하면 된다.
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);
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 를 참조할 수 있음.
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 컴포넌트에 b
와 button
에 onClick
으로 설정해준 함수들은 해당 함수들을 useCallback
으로 재사용한다고 해서 리렌더링을 막을 수 있는 것은 아니므로, 굳이 그렇게 할 필요가 없다.
추가적으로, 렌더링 최적화 하지 않을 컴포넌트에 React.memo 를 사용하는 것은 불필요한 props 비교만 하는 것이다. 따라서 실제로 렌더링을 방지할 수 있는 상황인 경우에만 사용해야 한다.