| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- RxJS 결합 오퍼레이터
- 알고리즘
- React useMemo 사용법
- contains duplicate
- RxJS 에러 처리
- React hooks 남용 사례
- React 리렌더링 최적화
- React 성능 최적화 방법
- React useEffect 안티패턴
- 알고리즘스터디
- Climbing Stairs
- React useCallback 사용법
- useCallback 성능 최적화
- RxJS 멀티캐스팅
- leedcode
- Observable vs Array
- DaleStudy
- 협업문화
- 개발자커뮤니케이션
- leetcode
- RxJS 오퍼레이터
- 자바스크립트 고차 함수 vs Observable
- 달래스터디
- RxJS 함수형 프로그래밍
- RxJS 생성 오퍼레이터
- RxJS 마블 다이어그램
- 스쿼드조직
- RxJS 변환 오퍼레이터
- useMemo 성능 최적화
- Blind75
- Today
- Total
수쿵의 IT월드
useMemo, useCallback 남발해서 사용하시나요? 본문

안녕하세요. 오늘은 제가 실무에서 직접 경험한 상황을 바탕으로, 많은 분들이 “성능 최적화”라는 이름으로 무심코 사용하고 있는 useMemo, useCallback 에 대해 이야기해보려 합니다.
- 0. 글을 쓰게 된 배경
- 1. React는 왜 등장하게 되었을까?
- 2. useMemo, useCallback은 왜 생겼을까?
- 3. 무분별한 사용이 성능을 저해하는 이유
- 4. useMemo, useCallback을 올바르게 사용하는 방법
- 5. 성능 최적화 효과: 실제로 얼마나?
- 사례: useCallback 남용으로 유지보수성 악화
0. 글을 쓰게 된 배경
현재 제가 속한 팀은, 초기에는 사이드 프로젝트로 시작했던 서비스가 정식 비즈니스로 확장되어 생긴 조직입니다.
자연스럽게 기존 코드의 대부분은 MVP 시절의 코드 그대로 남아 있었고, 그 안에는 수많은 useMemo 와 useCallback이 사용되고 있었습니다.
그런데, 그 대부분이 굳이 쓰지 않아도 되는 곳에서 사용되고 있었습니다.
이 글에서는 그런 경험을 바탕으로 왜 useMemo와 useCallback이 등장하게 되었는지, 올바른 사용법은 무엇인지 등의 내용을 정리해보겠습니다.
1. React는 왜 등장하게 되었을까?
React 이전의 프론트엔드 개발은 대부분 jQuery 기반 DOM 조작에 의존하고 있었습니다.
하지만 이 방식은 몇 가지 근본적인 문제를 가지고 있습니다.
- 상태가 바뀔 때 어떤 DOM을 어떻게 조작해야 할지를 직접 다뤄야하고
- 작은 변경에도 전체 DOM을 다시 그리는 비효율이 발생하며
- 코드의 흐름이 복잡하고 예측 불가능해졌습니다.
if (user.isLoggedIn) {
$('#loginBtn').hide();
$('#logoutBtn').show();
}
위처럼 상태 기반이 아니라 명령형 방식으로 DOM을 조작하다 보니, 점점 규모가 커질수록 UI와 상태의 불일치, 성능 문제, 협업 어려움이 발생하게 된 거죠.
React의 제안: 선언형 UI + Virtual DOM
React는 여기서 아주 강력한 패러다임 전환을 제시합니다.
UI는 상태의 함수다.
즉, 상태만 주면 그에 맞는 UI를 그려줄 테니, 개발자는 DOM을 신경 쓰지 말고 상태만 관리하라는 철학이였습니다.
그리고, 실제 DOM을 다시 그리는 대신, Virtual DOM을 통해 변화된 부분만 똑똑하게 반영하게 되죠.
이 철학 덕분에 개발자들은 복잡한 DOM 업데이트 로직에서 해방될 수 있었고, React는 프론트엔드 개발의 표준처럼 자리 잡게 됩니다.
💡 참고: Virtual DOM은 본질적으로 "성능 최적화 자체"가 목적이라기보다는, 선언형 UI 모델을 구현하기 위한 수단이었습니다. 다만 특정 상황에서는 성능상의 이점도 제공하게 되었습니다.
2. useMemo, useCallback은 왜 생겼을까?
React는 기본적으로 상태가 바뀌면 해당 컴포넌트를 다시 렌더링합니다. 이 자체는 자연스럽고 좋은 철학이지만, 컴포넌트를 작게 나누고 props로 함수나 객체를 전달하면서 문제가 생기기 시작합니다.
예를 들어 아래처럼 자식 컴포넌트에 함수를 넘긴다고 가정해봅시다:
<MyComponent onClick={() => doSomething()} />
이 코드는 부모가 렌더링될 때마다 새로운 함수가 생성되기 때문에, 자식이 React.memo()를 써도 불필요한 리렌더링이 발생합니다.
객체도 마찬가지입니다:
<MyComponent config={{ darkMode: true }} />
이런 상황에서 참조가 매번 달라지면, 불변성을 기반으로 최적화된 컴포넌트 구조가 무력화됩니다.
그래서 등장한 게 바로:
useCallback: 함수를 메모이제이션하며 참조를 유지useMemo: 무거운 계산 결과나 객체 생성을 메모이제이션
그럼, useCallback와 useMemo을 사용하면 성능에 좋은게 아닌가요? 하며 사용할 수도 있습니다.
3. 무분별한 사용이 성능을 저해하는 이유
많은 개발자들이 “성능 최적화”를 이유로 모든 함수, 모든 객체, 모든 계산에 useMemo, useCallback 을 씌우는 실수를 합니다. 하지만 이 훅들은 비용없는 최적화가 아닙니다. 오히려 잘못 쓰면 성능을 저하시킬 뿐 아니라, 디버깅까지 어렵게 만들 수 있습니다.
메모이제이션 자체도 비용이 듭니다
useMemo,useCallback은 내부적으로 의존성 배열(deps)을 비교하고, 참조를 캐싱하는 구조입니다.- 계산 로직이 가볍거나 리렌더 빈도가 낮은 컴포넌트에서는 이 메모이제이션 오버헤드가 더 클 수 있습니다.
- 즉, "최적화"를 위해 도입했는데 실제로는 GC 비용 + deps 비교 연산 때문에 성능이 나빠질 수도 있습니다.
디버깅과 버그 추적이 어려워집니다
useMemo,useCallback이 적용된 값은 의존성 배열이 바뀌지 않으면 재계산되지 않습니다.- 그 결과, 실제로는 업데이트가 필요한 값인데도 렌더링이 되지 않거나, 상태가 stale한 채로 남는 문제가 발생합니다.
const filteredList = useMemo(() => filterItems(data), []);
위처럼, 의존성 배열을 하나라도 빠뜨리면 조용히 버그가 발생합니다. 이런 버그는 타입 시스템에도 잡히지 않고, 디버깅도 굉장히 까다롭습니다.
구조적으로 점검이 필요한 코드의 신호일 수 있습니다
어떤 컴포넌트에서 useMemo, useCallback 없이 리렌더링이 지나치게 발생한다면,
우리는 잠시 멈추고 생각해봐야 합니다:
"혹시 지금 컴포넌트 구조나 상태 분리가 잘못된 건 아닐까?"
예를 들어:
- 상태가 너무 상위에 몰려 있진 않은지?
- 자식 컴포넌트가
props변경에 너무 예민하진 않은지? - 불필요한 콜백을
props로 넘기고 있진 않은지? - 데이터 로직(필터링, 정렬, 파싱 등)과 UI 렌더링 로직이 강하게 결합되어 있진 않은지
useMemo, useCallback으로 그걸 “임시로 덮기” 전에, 리렌더링이 자주 발생하는 진짜 원인부터 파악하는 게 먼저입니다.
useMemo와 useCallback은 구조 개선이 끝난 뒤에 적용하는 미세 조정 도구입니다
4. useMemo, useCallback을 올바르게 사용하는 방법
제가 사용하는 방식이 100% 옳은 방식이라고 생각하지는 않지만, 제가 사용하는 방식은 아래와 같습니다.
사용을 피해야하는 경우
- 원시값을 감싸는 메모이제이션 (string, number, boolean 등)
- 렌더링 비용보다 메모이제이션 비용이 더 큰 경우
- 무조건 "써야 할 것 같아서" 습관적으로 사용하는 경우
사용해야 하는 경우
filter,sort,reduce등 무거운 계산이 있을 때- 렌더링 성능이 실제로 병목이 되고 있음이 프로파일링으로 확인된 경우
- 자식 컴포넌트에 함수나 객체를 넘기고, 그 컴포넌트가
React.memo또는useEffect에 의존하는 경우
코드 예시
// Parent.tsx
const Parent = () => {
const [value, setValue] = useState(0);
const config = {
color: 'blue',
};
return (
<>
<button onClick={() => setValue(v => v + 1)}>Update</button>
<Child config={config} />
</>
);
};
// Child.tsx
const Child = ({ config }: { config: { color: string } }) => {
useEffect(() => {
console.log('Config changed:', config); // props를 통해 useEffect가 일어남
}, [config]);
return <div>Child</div>;
};
Parent가 리렌더링될 때마다 config 객체가 새로 만들어지기 때문에, Child의 useEffect는 불필요하게 매번 실행됩니다.
const config = useMemo(() => ({ color: 'blue' }), []);
이렇게 하면 config는 참조가 고정되고, useEffect도 정말 변경이 있을 때만 실행됩니다.
P.S.
하지만… useEffect도 남발하는 건 React에서 대표적인 안티패턴입니다.
무언가 변경될 때마다 useEffect로 대응하고 있다면,그 전에 한 번은 꼭 이렇게 자문해보세요:
"이건 정말 effect가 필요한가? 그냥 연산으로도 해결 가능한 건 아닌가?"
불필요한 effect는 코드 복잡도도 높이고, 버그 원인도 되기 쉽습니다.useEffect는 마지막 수단이어야 하고, 가능한 한 의존성이 명확한 경우에만 사용하는 게 좋습니다.
5. 성능 최적화 효과: 실제로 얼마나?
마지막으로, useCallback, useMemo를 사용하면 실제로 얼마나 성능 최적화가 되는지, 남발해서 사용한다면 어떤 문제가 있을 수 있을 지 직접 코드를 보며 확인해보겠습니다.
먼저, useMemo를 사용하지 않은 코드를 확인해보겠습니다.
// useMemo 사용전
function App() {
const [count, setCount] = useState(0);
const start = performance.now();
const value = haveComputation(10_000_000);
console.log("value:", performance.now() - start); // value: 367.19999998807907
return (
<>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>{value}</p>
</div>
</>
);
}
const haveComputation = (size: number) => {
const arr = Array.from({ length: size }, (_, i) => i);
return arr.reduce((acc, curr) => acc + curr, 0);
};
export default App;

count 버튼을 클릭하면 haveComputation 함수가 매번 호출되며, 전체 작업 시간이 805.19ms에 달하는 것을 확인할 수 있습니다.
반면, useMemo를 적용한 경우를 살펴보면 상황이 크게 달라집니다.
// useMemo 사용후
function App() {
const [count, setCount] = useState(0);
const start = performance.now();
const value = useMemo(() => haveComputation(10_000_000), []);
console.log("useMemo execution time:", performance.now() - start);
// useMemo execution time: 0.10000002384185791
return (
<>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>{value}</p>
</div>
</>
);
}
const haveComputation = (size: number) => {
const arr = Array.from({ length: size }, (_, i) => i);
return arr.reduce((acc, curr) => acc + curr, 0);
};

동일하게 count 버튼을 클릭했을 때 haveComputation은 재호출되지 않고, 전체 작업 시간은 불과 1.42ms로 줄어드는 것을 확인할 수 있습니다.
즉, 불필요하게 반복 계산이 일어나던 부분을 useMemo로 메모이제이션하여, 동일한 입력에 대해서는 이미 계산된 값을 재사용함으로써 성능을 극적으로 개선할 수 있었습니다.
⚠️ 여기서의 성능 측정은 haveComputation 처럼 연산량이 큰 순수 함수에만 해당됩니다. 실제 React 앱 성능은 렌더링, reconciliation, 브라우저 페인트 비용까지 포함되므로, 항상 프로파일링 도구(React Profiler, Performance 탭 등)로 확인해야 합니다.
그렇다면, 이렇게 많은 성능 효과가 있는데, 안 사용할 이유가 없지 않냐고 물어보시는 분들도 있을 수 있습니다. 아마도 그 이유로 많은 분들이 일단 useMemo와 useCallback으로 감싸는 경우가 있다고 생각됩니다.
그렇다면, useMemo와 useCallback을 남용해서 사용할 경우, 어떤 일이 벌어지는 지 확인해보도록 하겠습니다. 코드는 회사에서 실제로 문제가 되었던 코드를 각색한 것입니다.
코드 예시
const onWatchEventHandler = useCallback(
(messageEvent: MessageEvent) => {
try {
const { origin, source, data } = messageEvent;
const { action, payload } = JSON.parse(data);
if (action === MESSAGE_ACTIONS.CONFIG) {
setConfig(payload);
setHistory((old) => [...old, payload]);
return;
} else if (action === watch) {
setSource(source);
setOrigin(origin);
setHistory((old) => [...old, payload]);
if (typeof eventHandler === 'function') {
eventHandler(sendToSender, payload);
}
}
} catch (error) {
console.debug('[useMessage/onWatchEventHandler] error:', error);
}
},
[watch, eventHandler, setSource, setOrigin]
);
useEffect(() => {
const messageEvent = 'message';
addEventListener(messageEvent, onWatchEventHandler);
return () => removeEventListener(messageEvent, onWatchEventHandler);
}, [watch, source, origin, onWatchEventHandler]);
이 코드는 제가 입사하기 전부터 있었던 코드인데, 리팩토링을 하기 위해 커스텀 훅에 들어가보니 구조를 파악하는데 더 시간이 오래 걸리게 되었습니다. useCallback의 의존성 배열과 useEffect의 의존성 배열이 서로 달라서, 정확히 언제 핸들러가 재생성되고 언제 리스너가 재등록되는지 파악하는데 상당한 시간을 소비했습니다.
특히 eventHandler는 useCallback의 deps에만 있고, source와 origin은 useEffect의 deps에만 있어서, 이들이 변경될 때 어떤 동작이 일어나는지 예측하기 어려웠습니다.
이는 단순히 useCallback을 쓴 것이 문제라기보다는, "무조건 useCallback으로 감싸자"는 습관이 deps 관리까지 꼬이게 만든 것이 근본 원인이었습니다.
💡 교훈: useCallback과 useMemo는 성능 최적화 도구라기보다, 컴포넌트 구조 개선 후 마지막 단계에서 적용하는 미세 조정 도구
로 쓰는 것이 가장 안전합니다.
오늘은 useCallback과 useMemo에 대해 이야기해봤습니다.
다음에는 제가 회사에서 직접 경험했던 또 다른 사례를 바탕으로, 팀과 코드에 도움이 되었던 (혹은 오히려 발목을 잡았던) 이야기들을 나눠보려고 합니다.
조금 더 현실적인 경험담을 기대해 주세요 🙌