[웹 프론트엔드 인터뷰] #4. useCallback과 useMemo는 언제 어떻게 사용하나요?
오늘은 많은 분들이 사용은 하지만 헷갈리기 쉬운 useCallback
과 useMemo
에 대해서 이야기를 해볼까 합니다. 실제로 웹 프론트엔드 개발자 면접 질문에서 신입/경력 구분없이 단골로 나오는 인터뷰 주제 중 하나이기도 해요.
먼저 React 공식 문서에는 useCallback과 useMemo가 다음과 같이 설명이 나와 있어요.
- useCallback : Returns a memoized callback. 메모이제이션된 콜백을 반환한다.
- useMemo : Returns a memoized value. 메모이제이션된 값을 반환한다.
이 문장만 읽어서는 이해가 쉽지가 않습니다. 조금 더 살펴보도록 하겠습니다.
useCallback과 useMemo 둘 다 공통적으로 가지고 있는 속성은 메모이제이션(Memoization)
과 의존성(Dependency)
입니다. 메모이제이션은 메모리에 어떤 연산을 통해 반환된 값을 저장해 놓았다가 나중에 그대로 다시 가져와서 사용하는 개념으로, 동적 계획법(Dynamic Programming)에서 많이 사용되는 기법입니다. 그리고 useCallback과 useMemo는 배열 형태의 의존성을 가져서 해당 값들이 변할 때 useCallback은 함수를, useMemo는 값을 반환하는 것이에요.
자바스크립트에서 함수는 1급 객체이기 때문에, 다음 두 코드는 실질적으로 동일한 동작을 수행합니다.
useCallback(fn, deps);
useMemo(() => fn, deps);
그렇다면 우리는 useCallback과 useMemo를 언제 사용해야 할까요? 렌더링 사이사이에 동치 비교(referential equality)
에 의존하는 경우 useCallback과 useMemo를 사용한다. 여기서 동치 비교란 ==
는 성립하지만 ===
는 성립하지 않는, 다시 말해 값은 같아 보이지만, 참조하는 메모리가 다른 경우를 의미합니다.
const greeting = 'hello';
const otherGreeting = 'hello';
function foo() {
return 'bar';
}
const otherFoo = function() {
return `bar`;
};
const anotherFoo = () => 'bar';
function sameFoo() {
return 'bar';
}
const fooReference = foo;
'hello' === 'hello'; // true
greeting === otherGreeting; // true
foo === foo; // true
foo === otherFoo; // false
foo === anotherFoo; // false
foo === sameFoo; // false
foo === fooReference; // true
컴포넌트가 리렌더링할 때마다 새로운 함수를 매번 생성을 하는데 겉으로 보기에는 값이 같지만 그 값이 참조하는 메모리가 달라서 ===
이 비교는 false가 나오기 때문에 계속 리렌더링을 하는 경우가 발생합니다. useCallback과 useMemo는 해당하는 불필요한 리렌더링을 방지하는 역할을 도와주는 훅입니다.
예를 들어 어떤 API fetch를 수행하는 함수가 있다고 가정했을 때, 일반적으로 다음과 같이 작성을 해볼 수 있습니다.
useEffect(() => {
const fetchUser = async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser); // Triggers re-render, but ...
};
fetchUser();
}, [userId]);
이 경우 userId가 바뀔 때 마다 useEffect 훅이 실행되어 fetchUser() 함수를 실행합니다. 여기에 useCallback 훅을 적용해 볼 수 있습니다.
const fetchUser = useCallback(async () => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const newUser = await res.json();
setUser(newUser);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]);
이렇게 되면 useEffect 훅에서 무한 루프가 발생하지 않고 fetchUser가 userId가 바뀔 때 마다 실행이 됨을 알 수 있습니다.
UserList에서 이 유저 목록을 필터해서 불러오는 부분에서도 일반적으로는 다음과 같이 작성을 할 수 있겠지만,
function UserList({ query, users }) {
// 🔴 Recalculated on every render
const filteredUsers = filter(nameIncludes(query), users);
// ...
}
이 방법은 매번 렌더링을 할 때마다 filter 함수가 실행이 되고 계산을 반복합니다. 렌더링은 query나 users가 바뀌지 않아도 될 수가 있는데 말이지요.
이 부분도 useMemo를 통해 최적화를 해볼 수가 있습니다.
function UserList({ query, users }) {
// ✅ Recalculated when query or users change
const filteredUsers = useMemo(
() => filter(nameIncludes(query), users),
[query, users]
);
// ...
}
다음과 같이 작성을 하면 오직 query, users가 값이 바뀔 때만 함수를 실행해서 필터링된 유저 리스트를 받아오게 되고 불필요한 렌더링을 줄일 수가 있습니다.
참고자료
https://medium.com/@jan.hesters/usecallback-vs-usememo-c23ad1dc60