메모제이션은 기존에 수행한 연산의 결괏값을 어딘가에 저장해 두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법이다. 이것을 적절하게 활용하면 중복 연산을 피할 수 있기 때문에 메모리를 조금 더 쓰더라도 애플리케이션의 성능을 최적화할 수 있다.
useMemo는 메모제이션된 값을 반환하는 함수다.
useMemo(() => fn, [deps])
여기서 deps로 지정한 값이 변하게 되면 () => fn 함수를 실행하고, 그 함수의 반환값을 반환해준다.
const [ex, setEx] = useState(0);
const [why, setWhy] = useState(0);
useMemo(() => {console.log(ex)}, [ex]);
return (
<>
<button onClick={() => setEx((curr) => (curr + 1))}X</button>
<button onClick={() => setWhy((curr2) => (curr2 + 1))}Y</button>
</>
);
위와 같이 useMemo를 사용할 수 있다.
여기서 X라는 버튼을 클릭했을 때 setEx에 의해서 ex의 값이 1씩 증가하는데, ex의 값이 변하기 때문에 useMemo에서 의존성으로 등록한 ex가 변화된 것을 감지해 지정한 함수가 실행되고 console.log로 ex의 값이 출력된다.
위 예제에서 useMemo를 사용하지 않고 그냥 console.log(ex)만 작성한다면 X버튼, Y버튼, 혹은 컴포넌트가 부모 컴포넌트에 의해서 리렌더링 될 경우 상태 값과 관계없이 console.log가 실행한다. 단순한 console.log가 아닌 복잡한 연산이라고 가정하면 답답하다.
따라서ex값이 변할 경우에만 연산을 실행할 수 있도록 useMemo를 사용해 ex라는 변수에 의존하도록 등록하는 것이다.
그 결과 리렌더링이 발생할 경우 특정 변수가 변할 때만 useMemo에 등록한 함수가 실행되도록 처리하면 불필요한 연산을 하지 않게 된다.
useCallback은 메모제이션된 함수를 반환한다.
useCallback(fn, [deps])
useCallback 또한 deps, 의존성이 있는 값이 변하면 fn에 등록한 함수를 반환하는 기능을 가지고 있다.
useMemo(() => console.log(), [test])
const memoizedCallback = useCallback(() => console.log(), [test])
useCallback은 함수를 반환하기 때문에 그 함수를 가지는 const 변수에 초기화하는 것이 일반적인 모양이다.
useMemo같은 경우는 deps값이 변하면 이 함수를 실행하라는 느낌으로 활용이 가능하다.
useCallback은 다음과 같을때 사용한다.
1. 자식 컴포넌트에 props로 함수를 전달하는 경우
2. 외부에서 값을 가져오는 api를 호출할 경우
먼저 함수는 값이 아닌 참조로 비교된다.
const functionOne = function() {
return 5;
};
const functionTwo = function() {
return 5;
};
console.log(functionOne === functionTwo); // false
위 결괏값은 동일한 값을 반환하지만 참조가 다르기 때문에 false가 나온다.
위와 같이 컴포넌트에서 특정 함수를 정의할 경우 각각의 함수들은 모두 고유한 함수가 된다.
이런 고유한 함수가 생성될 경우, 부모를 통해 props에 함수를 전달받는 자식 컴포넌트에서는 props가 변경되었다고 판단해 리렌더링이 발생하게 된다.
const [name, setName] = useState('');
const onSave = () => {};
return (
<div className="App">
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<Profile onSave={onSave} />
</div>
);
useCallback을 사용하지 않을 경우, name이 변경되어 리렌더링이 발생하면 onSave함수가 새로 만들어지고, Profile 컴포넌트의 props로 onSave함수가 새로 전달되게 된다.
이때 Profile 컴포넌트에서 useMemo를 사용해도 이전 onSave와 이후 onSave가 같은 값을 반환하지만 참조가 다른 함수가 되어버리기 때문에 리렌더링이 일어나게 된다.
부모 컴포넌트만 수정하려고 했지만 연쇄적으로 하위 컴포넌트들 모두 렌더링이 일어나게 된다.
따라서 아래와 같이
const [name, setName] = useState('');
const onSave = useCallback(() => {
console.log(name);
}, []);
return (
<div className="App">
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<Profile onSave={onSave} />
</div>
);
useCallback을 사용해서 onSave라는 함수를 재사용하는 것으로 자식 컴포넌트의 리렌더링을 방지할 수 있다.
const [user, setUser] = useState(null);
function Profile({ userId }) {
const fetchUser = () => {
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({user}) => user);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
};
};
위 코드는 fetchUser 함수가 변경될 때만 외부에서 api를 가져와 useEffect가 실행된다.
하지만 Profile 컴포넌트가 리렌더링이 발생할 경우 fetchUser 함수에는 새로운 함수가 할당되게 된다. 그러면 useEffect()함수가 호출되어 user 상태 값이 바뀌고, state 값이 바뀌었기 때문에 다시 리렌더링이 일어난다.
무한 루프에 빠져버리게 된다.
이때 useCallback을 사용해서 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있다.
const fetchUser = useCallback(
() =>
fetch(`https://your-api.com/users/{$userId}`)
.then((response) => response.json())
.then(({ user }) => user),
[userId]
);
api 옵션으로 사용되는 userID가 변동될 때만 fetchUser에 새로운 함수가 할당되도록 설정하고, 그것이 아니면 동일한 함수가 실행하게 돼서 무한 루프에 빠지지 않는다.