React.memo를 이용하면 컴포넌트의 props가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있다.
이 함수를 사용하면 컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링 하도록 설정해줄 수 있다.
React.memo의 인자로 컴포넌트(함수)를 넣는다.
export default React.memo(CreateUser);
const CreateUser = ({ username, email, onChange, onCreate }) => {
return (
<div>
<input
name="username"
placeholder="계정명"
onChange={onChange}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
</div>
);
};
export default React.memo(CreateUser);
const User = React.memo(function User({ user, onRemove, onToggle }) {
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>
</div>
);
});
function UserList({ users, onRemove, onToggle }) {
return (
<div>
{users.map(user => (
<User
user={user}
key={user.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
export default React.memo(UserList);
적용 후 input을 수정할 때 하단의 UserList가 리렌더링 되는지 확인해보았다.
🚨 그런데, User
중 하나라도 수정하면 모든 User
들이 리렌더링되고, CreateUser
도 리렌더링 되는 것을 볼 수 있었다.
👉 이유는 users
배열이 바뀔 때마다 onCreate
도 새로 만들어지고, onToggle
, onRemove
도 새로 만들어지기 때문이다.
const onCreate = useCallback(() => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user));
setInputs({
username: '',
email: ''
});
nextId.current += 1;
}, [users, username, email]);
const onRemove = useCallback(
id => {
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 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]
);
deps에 users
가 들어있기 때문에 배열이 바뀔 때마다 함수가 새로 만들어진다. 이걸 최적화하고 싶다면 어떻게 해야할까?
deps에서 users
를 지우고, 함수들에서 현재 useState로 관리하는 users
를 참조하지 않게 하는 것이다.
👉 함수형 업데이트!
함수형 업데이트를 하게 되면, setUsers
에 등록하는 콜백함수의 파라미터에서 최신 users
를 참조할 수 있기 때문에 deps 에 users
를 넣지 않아도 된다.
function App() {
const [inputs, setInputs] = useState({
username: '',
email: ''
});
const { username, email } = inputs;
const onChange = useCallback(e => {
const { name, value } = e.target;
setInputs(inputs => ({
...inputs,
[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 user = {
id: nextId.current,
username,
email
};
setUsers(users => users.concat(user));
setInputs({
username: '',
email: ''
});
nextId.current += 1;
}, [username, email]);
const onRemove = useCallback(id => {
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 id 인 것을 제거함
setUsers(users => users.filter(user => user.id !== id));
}, []);
const onToggle = useCallback(id => {
setUsers(users =>
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
}, []);
const count = useMemo(() => countActiveUsers(users), [users]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성사용자 수 : {count}</div>
</>
);
}
이렇게 해주면 특정 항목을 수정하게 될 때, 해당 항목만 리렌더링 될 것이다.
리액트를 개발할 때 useCallback, useMemo, React.memo는 컴포넌트의 성능을 실제로 개선할 수 있는 상황에서만 사용하는 것이 좋다.
예를 들어서User
컴포넌트에b
와button
에onClick
으로 설정해준 함수들은, 해당 함수들을 useCallback으로 재사용한다고해서 리렌더링을 막을 수 있는 것은 아니므로, 굳이 그렇게 할 필요는 없다.
추가적으로, 렌더링 최적화하지 않을 컴포넌트에 React.memo를 사용하는 것은, 불필요한 props 비교만 하는 것이기 때문에 실제로 렌더링을 방지할 수 있는 상황이 있는 경우에만 사용한다.
추가적으로, React.memo에서 두번째 파라미터에 propsAreEqual
이라는 함수를 넣어서 특정값들만 비교를 하는 것도 가능하다.
이 함수에서는 prevProps
(전 함수)와 nextProps
(후 함수)를 가져와서 비교한다.
두 함수를 비교해서 true
라면 리렌더링을 방지하고, false
가 나온다면 리렌더링 된다.
export default React.memo(
UserList,
(prevProps, nextProps) => prevProps.users === nextProps.users
);
하지만, 이걸 잘못 사용한다면 오히려 의도치 않은 버그들이 발생하기 쉽다.
예를 들어서, 함수형 업데이트로 전환을 안했는데 이렇게 users
만 비교를 하게 된다면, onToggle
과 onRemove
에서 최신 users
배열을 참조하지 않으므로 심각한 오류가 발생할 수 있다.
이런식으로 함수의 특정 props를 비교해서 리렌더링 방지를 할 수 있지만, 이 경우에 나머지 props들이 정말 고정적이기 때문에 비교가 할 필요가 없는 것인지 확인해야 한다.