
React 프로젝트를 개발하다 보면 성능 최적화를 위해 useMemo나 useCallback을 사용하는 경우가 많습니다.
특히 코드 리뷰에서 다음과 같은 코드를 자주 볼 수 있습니다.
const handleClick = useCallback(() => {
setCount(count + 1)
}, [count])
const total = useMemo(() => {
return price * quantity
}, [price, quantity])
언뜻 보면 성능을 고려한 좋은 코드처럼 보입니다.
하지만 실제로는 필요하지 않은 곳에서 useMemo, useCallback을 사용하는 것이 오히려 코드 복잡도를 높일 수 있습니다.
이번 글에서는 언제 사용해야 하고, 언제 사용하지 않는 것이 좋은지 정리해보겠습니다.
useMemo는 무엇을 하는가?
useMemo는 특정 연산의 결과를 저장해두고 dependency가 변경될 때만 다시 계산합니다.
const filteredList = useMemo(() => {
return list.filter(item => item.active)
}, [list])
list가 변경되지 않았다면 이전에 계산했던 filteredList를 재사용합니다.
따라서 계산 비용이 큰 작업에서는 도움이 될 수 있습니다.
예시)
const result = useMemo(() => {
return veryLargeArray
.filter(item => item.enabled)
.sort((a, b) => b.score - a.score)
.map(item => expensiveTransform(item))
}, [veryLargeArray])
데이터가 많고 계산 과정이 복잡하다면 useMemo를 사용하는 의미가 있습니다.
단순 계산에는 굳이 필요하지 않다
반대로 아래 코드는 최적화 효과가 거의 없습니다.
const totalPrice = useMemo(() => {
return price * quantity
}, [price, quantity])
곱셈 한 번을 줄이기 위해 React가 dependency를 비교하고 memo 값을 관리하게 됩니다.
이 경우에는 아래처럼 작성하는 것이 코드도 읽기 쉽고 관리하기도 좋습니다.
const totalPrice = price * quantity
계산 비용 << memoization 관리 비용
useCallback도 같은 문제를 가진다
useCallback은 함수를 메모이제이션합니다.
const handleClick = useCallback(() => {
setCount(prev => prev + 1)
}, [])
하지만 일반적인 이벤트 핸들러라면 다음처럼 작성해도 대부분 문제가 없습니다.
const handleClick = () => {
setCount(prev => prev + 1)
}
React 컴포넌트가 다시 렌더링되면 함수도 다시 생성됩니다.
하지만 대부분의 애플리케이션에서는 함수 하나 생성되는 비용이 성능에 영향을 줄 정도로 크지 않습니다.
useCallback이 실제로 필요한 경우
대표적인 경우는 React.memo와 함께 사용할 때입니다.
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>클릭</button>
})
부모 컴포넌트에서 다음처럼 함수를 작성하면
const handleClick = () => {
console.log('click')
}
return <Child onClick={handleClick} />
부모가 렌더링될 때마다 새로운 함수가 생성됩니다.
그러면 Child 입장에서는 onClick prop이 변경된 것으로 판단될 수 있습니다.
이런 경우에는 useCallback이 의미가 있습니다.
const handleClick = useCallback(() => {
console.log('click')
}, [])
함수 reference가 유지되기 때문에 불필요한 자식 컴포넌트 렌더링을 줄일 수 있습니다.
실무에서 판단하는 기준
저는 React 프로젝트에서 다음 기준으로 판단하는 것이 가장 현실적이라고 봅니다.
1. 계산량이 큰가?
단순 문자열 조합이나 사칙연산이라면 useMemo가 필요하지 않습니다.
const fullName = `${firstName} ${lastName}`
반대로 대량 배열 처리나 복잡한 연산이라면 고려할 수 있습니다.
2. React.memo 컴포넌트에 전달되는 값인가?
memoized component에 object나 function을 전달한다면 reference 유지가 중요할 수 있습니다.
예를 들어
const options = {
pageSize: 20
}
이 코드는 렌더링될 때마다 새로운 객체를 생성합니다.
필요하다면 다음과 같이 사용할 수 있습니다.
const options = useMemo(() => ({
pageSize: 20
}), [])
3. 실제 성능 문제가 있는가?
가장 중요한 기준입니다.
성능 문제가 확인되지 않았는데 미리 모든 코드를 최적화하면 오히려 코드가 복잡해집니다.
다음과 같은 순서가 좋습니다.
- 먼저 단순하게 구현한다.
- 실제 렌더링 문제가 있는지 확인한다.
- React DevTools Profiler로 병목을 확인한다.
- 필요한 부분만 최적화한다.
즉 측정 후 최적화하는 것이 좋습니다.
오히려 과도한 최적화가 유지보수를 어렵게 만든다
다음과 같은 코드가 프로젝트 전체에 반복되기 시작하면
const value = useMemo(() => {
return a + b
}, [a, b])
const handleChange = useCallback(() => {
setValue(...)
}, [...])
개발자는 계속 dependency를 관리해야 합니다.
dependency를 잘못 설정하면 stale closure 같은 버그가 발생할 수도 있습니다.
결과적으로 성능 개선보다 유지보수 비용이 더 커질 수 있습니다.
정리
useMemo와 useCallback은 React에서 유용한 최적화 도구입니다.
하지만 모든 곳에 사용하는 것이 좋은 것은 아닙니다.
실무에서는 다음 기준 정도로 생각하면 됩니다.
- 단순 계산 → 그냥 계산
- 일반 이벤트 핸들러 → 일반 함수
- 무거운 연산 → useMemo 고려
- React.memo 자식에 함수 전달 → useCallback 고려
- 실제 렌더링 병목 → Profiler 확인 후 최적화
React 성능 최적화에서 중요한 것은 hook을 많이 사용하는 것이 아니라,
필요한 곳에만 사용하는 것입니다.
'FrontEnd > React.js' 카테고리의 다른 글
| JavaScript에서 얕은 복사와 깊은 복사를 구분해야 하는 이유 (0) | 2026.09.03 |
|---|---|
| [React / scss] import 오류 해결하기 > use 쓰기 (0) | 2025.03.17 |
| [React-Query] 리액트 쿼리 사용해보기 - 캐싱을 통한 최적화 (0) | 2025.03.12 |
| [React.js] useContext / 테마색 바꾸기 (0) | 2025.03.11 |
| [React.js] useTransition & useDeferredValue 함께 사용하자 (0) | 2025.03.10 |
