React의 Hook 중에서도 성능 최적화를 위해 사용되는 useCallback
이전에 Hook들을 전부 다뤄본 적이 있음에도 막상 활용할 때에는 어려움이 있었다.
(관련 포스팅 📜 [REACT] React Hooks 탐구하기)
정확히 어떤 때에 적용하는건지 프로젝트에서는 잘 떠오르지 않는다고 해야 하나?
또 뭔가 성능 최적화를 위해 사용하는 것이다 보니까 '아 일단 일반 함수로 기능 만들고 나서 나중에 성능 챙기며 변환하자'라고 후순위로 밀리는 일이 많았다.
그러다 이번에, 성능에 집중된 어떤 프로젝트를 진행하면서 정확히는 채용 과제를 진행하면서...
성능 최적화를 위한 여러 가지를 도전하던 중 알게 된 useCallback에 대한 이야기를 기록해보고자 한다.
React의 상태 변화와 컴포넌트 렌더링
React에서 상태가 업데이트되면 그 상태를 소유하고 있는 컴포넌트가 다시 렌더링 된다.
예시로 내가 진행하고 있는 프로젝트는 App이라는 컴포넌트에 checkedBoxes라는 상태를 갖고 있는데
setCheckedBoxes가 호출되면, 즉 checkedBoxes가 변경되었다고 판단한 React는 App 컴포넌트 전체를 다시 렌더링 한다.
function App() {
const [checkedBoxes, setCheckedBoxes] = useState(new Set());
const handleCheckboxChange = (id) => {
setCheckedBoxes((prev) => {
const newCheckedBoxes = new Set(prev);
if (newCheckedBoxes.has(id)) newCheckedBoxes.delete(id);
else newCheckedBoxes.add(id);
return newCheckedBoxes;
});
};
return (
<div>
<Checkbox onChange={() => handleCheckboxChange("checkbox_1")} />
</div>
);
}
여기에서 다시 렌더링 된다는 것은 React가 App 컴포넌트의 함수(즉, App 함수)를 다시 실행한다는 것이다.
따라서 App 안의 모든 JSX 코드가 다시 평가되고, DOM 업데이트가 필요한지 검토하게 된다.
참고로 DOM 업데이트에 대해서 짚고 가자면,
React는 Virtual DOM을 활용하여 실제로 변경된 부분 만을 찾아 DOM을 업데이트하므로 이는 성능에 큰 영향이 없다.
(관련 포스팅 📜 [REACT] DOM과 Virtual DOM)
문제는 함수에 있다.
App 컴포넌트 내에 선언된 함수가 상태 변경마다 다시 선언된다는 사실이다.
이렇게 함수가 새롭게 선언될 때마다 참조가 달라지고, React는 이 덕에 '새로운 함수다'라고 인식하게 된다.
즉, 그 함수를 의존하는 하위 컴포넌트가 불필요하게 리렌더링 될 가능성이 높아진다.
이런 문제를 방어하기 위해서 사용되는 훅이 useCallback이다.
안녕하세요, 참조를 기억하는 useCallback이에요!
useCallback은 함수의 참조를 메모이제이션하는 훅이다.
의존성 배열을 통해 의존성 배열에 변화가 생길 때만 새로 함수를 생성하고, 그렇지 않으면 기존에 메모이제이션된 함수를 재사용한다.
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
이를 통해, 함수는 참조 동일성(referential equality)을 유지하게 되고
React가 자식 컴포넌트나 의존성으로 사용하는 다른 훅에 대해 불필요한 리렌더링이 발생하는 것을 방지할 수 있다.
앞선 예제에서 useCallback을 적용한다면 아래와 같다.
const handleCheckboxChange = useCallback((id) => {
setCheckedBoxes((prev) => {
const newCheckedBoxes = new Set(prev);
if (newCheckedBoxes.has(id)) newCheckedBoxes.delete(id);
else newCheckedBoxes.add(id);
return newCheckedBoxes;
});
}, [setCheckedBoxes]);
setCheckedBoxes가 변하지 않는 한, handleCheckboxChange 함수를 새로 생성하지 않고 기존 함수를 재사용한다.
이를 통해 부모 컴포넌트의 상태가 변경되어도 이 함수의 참조가 유지된다.
그럼 모든 함수에 useCallback 사용하는 게 좋을까요?
위 내용까지 학습하고 나니 든 생각은
'그렇다면 유틸함수를 포함해서 React 내에 정의되는 함수들은 다 useCallback 해주는 게 유리한 거 아닌가?'였다.
이 질문에 대한 답은 아니요다.
모든 함수에 useCallback을 적용할 필요는 없다.
오히려 지나치게 useCallback을 사용하게 된다면 메모리 사용이 증가하고 코드 가독성을 해칠 수 있다.
언제 useCallback을 사용해야 할까?
함수가 하위 컴포넌트로 전달되는 경우
위의 App 컴포넌트처럼, 부모 컴포넌트의 함수가 하위 컴포넌트에 props로 전달된다면 이는 useCallback으로 감싸는 것이 좋다. 이유는 하위 컴포넌트가 이 함수의 참조가 변하지 않았다고 인식하게 하여 불필요한 리렌더링을 막을 수 있기 때문이다.
컴포넌트가 빈번히 렌더링 되는 경우
상태가 자주 바뀌는 컴포넌트는 불필요한 함수 재생성이 성능에 영향을 줄 수 있다. 이런 컴포넌트에서 useCallback을 사용해 함수를 메모이제이션한다면 성능 향상에 도움이 될 수 있다.
함수가 비용이 큰 연산을 포함한 경우
함수가 복잡한 연산이나 시간이 오래 걸리는 로직을 포함하고 있다면, 이 연산이 불필요하게 다시 실행되지 않도록 useCallback 사용이 좋다.
언제 useCallback이 불필요할까?
유틸리티 함수
예를 들어 컴포넌트 내부에서만 사용하는 formatDate와 같이 간단한 유틸 함수들은 리렌더링을 통해 함수가 재생성된다고 해도 큰 비용이 들지 않기 때문에 굳이 useCallback으로 감싸지 않아도 괜찮다.
단순한 함수
마찬가지로 함수가 짧고, 렌더링의 부하가 크지 않은 컴포넌트에서 사용되는 경우에는 메모리와 성능의 이점이 적다.
'REACT' 카테고리의 다른 글
[REACT] 프론트엔드의 테스트 코드 이해하기 (0) | 2024.07.01 |
---|---|
[FIREBASE][REACT] Firestore 데이터베이스 가져오기. getDoc과 onSnapshot의 차이는? (1) | 2023.12.30 |
[REACT] 리액트 앱 성능 개선! React.lazy를 이용한 코드 스플리팅 (0) | 2023.12.19 |
[JAVASCRIPT][REACT] API 데이터 가져오기 (GET) (1) | 2023.11.26 |
[REACT] React Query(TanStack Query) 직접 적용해보며 알아가기 (0) | 2023.11.14 |