1. Memoization
useMemo에 대해 설명하기에 앞서, memoization에 대한 선행 지식이 필요한데요. memoization이란 기존에 수행한 연산의 결과값을 어딘가에 저장해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법을 말합니다. memoization을 절적히 적용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화할 수 있습니다.
2. React 랜더링 구조
일반적으로 React의 함수형 컴포넌트는 다음과 같은 구조로 작성이 됩니다.
function MyComponent(props) {
// 어떤 로직 (JavaScript)
return; /* 어떤 화면 (JSX) */
}
이렇게 작성된 컴포넌트 함수는 React 앱에서 랜더링(rendering)이 일어날 때마다 호출이 됩니다. 컴포넌트 함수가 호출이 되면 그 안에 자바스크립트 로직들이 수행되고, 이를 기반으로 JSX로 마크업된 UI가 리턴되는 기본 구조를 가지고 있습니다.
React에서 컴포넌트의 랜더링은 한 번 일어나고 끝이 아니라 수시로 계속 일어날 수 있습니다. 대표적인 예로 컴포넌트의 자신의 상태 변경(state update)이 일어날 수 있고, 아니면 부모 컴포넌트의 상태 변경이 일어나 덩달아 함께 랜더링되야 하는 경우도 있습니다. React에는 수동으로 다시 랜더링을 해주는 API도 있고, 사용자가 브라우저에서 새로고침을 할 때도 컴포넌트의 재랜더링은 불가피 합니다.
3. 함수에 Memoization 적용
아래 컴포넌트는 prop으로 넘어온 x와 y값을 compute 함수에 인자로 넘겨서 z값을 구한 후, 그 결과값을 div 엘리먼트로 감싸 출력해줍니다.
function MyComponent({ x, y }) {
const z = compute(x, y);
return <div>{z}</div>;
}
만약, compute 함수가 내부적으로 매우 복잡한 연산을 수행하게 된다면, 컴포넌트의 리랜더링이 필요할 때 마다 이 함수가 호출이 되므로 사용자는 지속적으로 UI에서 지연이 발생하는 경험을 하게 될 것입니다.
만약 compute 함수의 인자로 넘어오는 x와 y값이 항상 바뀌지 않고, 특정 상황에서만 바뀐다면 렌더링 될 때마다 compute 함수를 호출할 필요가 있을까요?
위와 같은 문제는 memoization 기법을 적용하면 개선할 수 있습니다. 랜더링이 발생했을 때, 이전 랜더링과 현재 랜더링 간에 x와 y 값이 동일한 경우, 다시 함수를 호출을 하여 z 값을 구하는 대신, 기존에 메모리의 어딘가에 저장해두었던 z 값을 그대로 사용하는 것입니다.
이러한 상황에서 memoization 로직을 직접 구현할 수도 있겠지만, 대신에 간편하게 사용할 수 있는 것이 바로 React의 useMemo hook 함수입니다. useMemo 함수는 2개의 인자를 받는데, 첫번째는 결과값을 생성해주는 팩토리 함수이고, 두번째는 기존 결과값 재활용 여부의 기준이되는 입력값 배열입니다. 예를 들어, 다음과 같이 위에서 작성한 컴포넌트를 재작성하면,
function MyComponent({ x, y }) {
const z = useMemo(() => compute(x, y), [x, y]);
return <div>{z}</div>;
}
x와 y 값이 이 전에 랜더링했을 때와 동일할 경우, 이 전 랜더링 때 저장해두었던 결과값을 재활용합니다. 하지만, x와 y 값이 이 전에 랜더링했을 때와 달라졌을 경우, () => compute(x, y) 함수를 호출하여 결과값을 새롭게 구해 z에 할당해줍니다.
4. useMemo 사용예
import React, { useRef, useState, useMemo } 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 = useMemo(() => countActiveUsers(users), [users]);
return (
<>
<CreateUser
username={username}
email={email}
onChange={onChange}
onCreate={onCreate}
/>
<UserList users={users} onRemove={onRemove} onToggle={onToggle} />
<div>활성사용자 수 : {count}</div>
</>
);
}
export default App;
참고
'FrontEnd > React 기본' 카테고리의 다른 글
[React] React.js 강좌 17. 불변성 (0) | 2022.03.08 |
---|---|
[React] React.js 강좌 16. useCallback (1) | 2022.03.08 |
[React] React.js 강좌 14. useEffect (0) | 2022.03.07 |
[React] React.js 강좌 13. useState (0) | 2022.03.07 |
[React] React.js 강좌 12. useRef (0) | 2021.12.10 |