useMemo 를 사용하여 연산값 재사용하기
useMemo
useMemo → Memoization
Memoization 이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말한다. memoization을 적절히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화 할 수 있다.
만약에, 함수가 내부적으로 매우 복잡한 연산을 수행해서 결과값을 리턴하는데 시간이 몇초 이상 오래 걸린다면 컴포넌트의 리렌더링이 필요할 때 마다 함수가 호출되므로 사용자는 지속적으로 UI에서 지연이 발생하는 경험하게 될 것이다.
이를 memorization 기법을 적용하면 React의 useMemo Hook 함수를 이용하여 개선시킬 수 있다.
사용법
첫번째 파라미터에는 (어떻게 연산할지 정의해주는)결과값을 생성해주는 팩토리 함수이고, 두번째는 기존 결과값 재활용 여부의 기준이 되는 입력값(deps) 배열이다. useMemo는 성능을 최적화 할 때 사용된다.
const count = useMemo(() => countActiveUsers(users), [users]);
배열 안에 내용이 바뀌게 되면 등록한 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용한다.
function MyComponent({ x, y }) {
const z = useMemo(() => compute(x, y), [x, y]);
return <div>{z}</div>;
}
x와 y 값이 이전에 렌더링했을 때와 동일할 경우, 이전 렌더링 때 저장해두었던 결과값을 재활용한다.
하지만, x와 y 값이 달라졌을 경우, ( ) => compute(x, y)
함수를 호출하여 결과값을 새롭게 구해 z 에 할당해준다.
예시
App.js
import React, { useRef, useState } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
function App() {
const [inputs, setInputs] = useState({
username: '',
email: ''
});
const { username, email } = inputs;
const onChange = e => {
const { name, value } = e.target;
setInputs({
...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 = () => {
const user = {
id: nextId.current,
username,
email
};
setUsers(users.concat(user));
setInputs({
username: '',
email: ''
});
nextId.current += 1;
};
const onRemove = id => {
// user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
// = user.id 가 id 인 것을 제거함
setUsers(users.filter(user => user.id !== id));
};
const onToggle = id => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
const count = countActiveUsers(users);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성사용자 수 : {count}</div>
</>
);
}
export default App;
countActiveUsers 함수에서 console.log 를 통해 함수가 호출 될 때 마다 확인할 수 있도록 했다.
function countActiveUsers(users) {
console.log('활성 사용자 수를 세는중...');
return users.filter(user => user.active).length;
}
여기서 다른 사용자명을 눌러서 활성화 시키면 활성 사용자수가 늘어나고, '활성 사용자 수를 세는중...' 로그가 기록된다.
여기서 발생하는 문제는 input 에 사용자를 추가하려고 작성할 때(바꿀때)에도 countActiveUsers 가 호출되면서 '활성 사용자 수를 세는중' 로그가 찍힌다는 것이다.
활성 사용자의 수를 세는 건 users에 변화가 있을 때만 세야되는 건데, input 값이 바뀔 때에 컴포넌트가 리렌더링 되므로 이렇게 불필요할 때에도 호출하여 자원이 낭비된다.
이러한 상황에 useMemo Hook 함수를 이용해서 성능을 최적화 시켜줄 수 있다.
const count = useMemo(() => countActiveUsers(users), [users]);
일반적으로 소프트웨어의 성능 최적화에는 그에 상응하는 대가가 따른다. 따라서 성능 최적화를 할 때는 얻을 수 있는 실제 성능 이점이 지불하는 대가에 비해서 미미하지 않은지에 대해서 반드시 따져보고 사용을 해야 한다.
예를 들어, useMemo hook 함수를 남용하면 컴포넌트의 복잡도가 올라가기 때문에 코드를 읽기도 어려워지고 유지보수성도 떨어지게 된다. 또한 useMemo 가 적용된 레퍼런스는 재활용을 위해서 가비지 컬렉션(garbage collection)에서 제외되기 때문에 메모리를 더 쓰게 된다.
실제 웹 프로젝트에서 useMemo Hook 함수를 사용할 일은 생각보다 그렇게 많지 않다.
왜냐하면 수초 이상 걸리는 로직이 프론트엔드에 존재한다는 것 자체가 일반적인 앱에서는 흔치 않은 일이고, 설사 그렇게 오래걸리는 로직이 있다고 해도 useEffect hook 함수 등을 이용해서 비동기로 처리하는 방안을 먼저 고려되기 때문이다.
따라서 useMemo 가 빛을 발휘할 수 있는 상황은 극히 제한적이며, 브라우저에서 React가 실행되는 속도도 워낙 빠르다보니 웬만한 컴포넌트가 여러번 렌더링이 일어난다고 해서 실제 심각한 성능 이슈로 이어지는 경우는 의외로 적다.
출처/참고
- https://react.vlpt.us/basic/17-useMemo.html (벨로퍼트 리액트)
- https://www.daleseo.com/react-hooks-use-memo/ (useMemo 참고 블로그)