1. 성능 향상을 위한 Memoization
React.memo와 useCallback(), useMemo의 개념을 이해하기 위해서는 Memoization의 정의를 알아야 한다. 정의는 다음과 같다.
- 결과를 캐싱하고, 다음 작업에서 캐싱한 것을 재사용 하는 비싼 작업의 속도를 높이는 자바스크립트 기술
- 이전 값을 메모리에 저장해 동일한 계산의 반복을 제거해 빠른 처리를 가능하게 하는 기술
- 캐시에 초기 작업 결과를 저장하여 사용함으로 써 최적화 할 수 있다. 만약 작업을 다시 수행해야 한다면, 어딘가에 저장되어진 동일한 결과를 단순히 반환 해준다.
💡 메모이제이션(Memoization)
자주 사용되는 값을 받아오기 위해 반복적으로 계산을 해야 하는 상황에서, 특정 값을 캐싱하는 것을 말한다. 해당 값이 또 필요할 때마다 메모리에서 꺼내서 재사용한다.
2. React.memo
React.memo는 Higher-Order Components(HOC)이다. Higher-Order Components(HOC)란 컴포넌트를 인자로 받아 새로운 컴포넌트롤 다시 return해주는 함수이다.
1) 첫번째 방식
const MyComponent = React.memo((props) => {
return (/*컴포넌트 렌더링 코드*/)}
);
만약 컴포넌트가 같은 props를 받을 때 같은 결과를 렌더링한다면 React.memo를 사용하여 불필요한 컴포넌트 렌더링을 방지할 수 있다. 즉, 컴포넌트에 같은 props가 들어온다면 리액트는 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용한다.
React.memo는 오직 props가 변경됐는지 아닌지만 체크한다. 만약 React.memo에 감싸진 함수형 컴포넌트가 함수 내부에서 useState나 useContext같은 훅을 사용하고 있다면, state나 context가 변경될 때마다 리렌더링된다.
위의 경우 넘겨받은 props의 변경 여부는 shallow compare로 비교되므로, object의 경우 같은 값을 참조하고 있는지를 비교한다.
function MyComponent(props) {
/* 컴포넌트 로직 */
}
function areEqual(prevProps, nextProps) {
/*
전달되는 nextProps가 prevProps와 같다면 true를 반환, 같지 않다면 false를 반환해 준다.
*/
}
export default React.memo(MyComponent, areEqual);
위의 코드를 사용하여 비교방식을 커스텀하여 React.memo의 두번째 인자로 넣어주면 된다.
2) 두번째 방식
export default React.memo(component);
기본적으로 export 시켜줄 때 컴포넌트명을 React.memo로 감싸준다. shouldComponentUpdate를 내장하고 있어 shallow copy를 실행하여 리렌더링을 방지한다.
위처럼 모듈화 시키는 컴포넌트를 export할때 React.memo로 감싸주면 된다.
3. useMemo
useMemo는 메모이즈된 값을 return하는 hook이다.
useMemo는 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화하는 용도로 사용된다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
인자로 함수와 Dependencies를 넘겨 받는다. 이 때, 2번째 인자로 넘겨준 의존 인자 중에 하나라도 값이 변경되면 1번째 인자의 함수를 재실행한다. 이를 통해 매 렌더링 할때마다 소요되는 불필요한 계산을 피할 수 있다. 만약 Dependencies 인자를 전달하지 않는다면 매번 새롭게 계산하여 return한다.
이걸 봤을 때 바로 useEffect()가 생각났다. useMemo()는 []안에 있는 값을 기억하여 값이 바뀌면 재렌더링해주고, useEffect()의 경우 []안의 값을 계속적으로 재렌더링해준다. 이는 값을 기억 못하기 때문이다.
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;
→ 이 코드의 경우 활성사용자의 수를 받으면 똑같은 함수가 계속적으로 반복된다. 이러한 경우 useMemo를 사용한다. 그리고 console.log아래는 바뀌지만, console.log는 바뀌지 않기에 이는 한번 출력하고 더 이상은 출력하지 않는다. 메모리의 최적화를 위해 사용하는 것이기에 이가 아니라면 사용하지 않는 것을 권한다.
useMemo()와 React.memo의 차이점
- React.memo는 HOC이고, useMemo와 useCallback은 hook이다.
- React.memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능하다.
4. useCallback
useCallback()은 함수를 메모이제이션(memoization)하기 위해서 사용되는 hook 함수이다. 첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해준다.
const memoizedCallback = useCallback(함수, 배열);
하지만 useCallback()을 사용하면, 해당 컴포넌트가 랜더링되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다. 즉, x 또는 y 값이 바뀌면 새로운 함수가 생성되어 add 변수에 할당되고, x와 y 값이 동일하다면 다음 랜더링 때 이 함수를 재사용한다.
const add = useCallback(() => x + y, [x, y]);
→ useMemo와 같이 사용된다. 둘다 메모이제이션의 개념이 설명되어야 사용되는 개념들이다. 또한 이를 과용하면 오히려 성능 저하가 올 수 있으니 최적화할려고 너무 안 해도 된다. 안한 것만 못 하다.
useCallback, useMemo를 통해 특정 props에 dependency를 걸어줌으로써 렌더링 횟수를 줄일 수 있다.
useMemo가 일반 값(숫자, 문자열, 객체)을 재사용할 때 사용하는 훅이라면 useCallback은 함수를 재사용하기 위해 사용하는 훅이다.
예제
useCallback을 사용해서 컴포넌트가 재렌더링이 되더라도 myfunc가 바뀌지 않도록 해보자.
리액트 앱에서 state가 변경되면 컴포넌트가 다시 렌더링되므로, num이 변경될 때마다 컴포넌트가 다시 렌더링되면서 myfunc 함수에도 새로운 함수 객체가 할당된다. 함수의 모양은 같아도 객체의 주소값은 다르므로 useEffect가 불린다.
import { useState, useEffect, useCallback } from 'react';
function App() {
const [num, setNum] = useState(0);
// useCallback 사용
const myfunc = useCallback(() => {
console.log(`myfunc: num: ${num}`);
return;
}, []);
// myfunc가 바뀔 때만 콘솔이 찍힌다
useEffect(()=>{
console.log('myfunc가 변경됨✨');
}, [myfunc]);
return (
<div>
<input type="number" value={num} onChange={e => setNum(e.target.value)} />
<button onClick={myfunc}> Call myfunc </button>
</div>
);
}
export default App;
myfunc를 useCallback으로 감싸주었다. 의존성 배열에 아무것도 넣지 않았으므로 인자로 들어간 콜백 함수는 컴포넌트가 처음 렌더링 될 때 만들어져서 메모이제이션될 것이다. 그리고 myfunc 안에는 메모이제이션된 함수의 주소가 들어간다. 이후에 컴포넌트가 재렌더링이 되어도 myfunc에는 새로운 객체가 만들어져서 할당되지 않는다.
결과를 확인해보면 num이 변경되어도 useEffect가 불리지 않는다. 즉 myfunc가 변하지 않았다는 것을 알 수 있다. 그런데 버튼을 통해 콘솔에 num값을 찍어보면, 숫자를 증가시켰음에도 0이 찍힌다. 우리가 함수를 메모이제이션할 당시의 num state는 0이었기 때문이다.
메모이제이션을 사용하면 성능이 좋아져 input창이 보일 때 버벅이면서 숫자가 변화가 있는 것이 아니라, 좀 더 수월하게 보이게 된다.