
useMemo를 떡칠하면 앱이 느려지는 이유 (최적화의 함정)
습관적으로 모든 변수에 `useMemo`를 감싸고 있나요? 그게 오히려 성능을 망치고 있습니다. 메모이제이션 비용과 올바른 최적화 타이밍.

습관적으로 모든 변수에 `useMemo`를 감싸고 있나요? 그게 오히려 성능을 망치고 있습니다. 메모이제이션 비용과 올바른 최적화 타이밍.
매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

느리다고 느껴서 감으로 최적화했는데 오히려 더 느려졌다. 프로파일러로 병목을 정확히 찾는 법을 배운 이야기.

엄청난 데이터를 아주 적은 메모리로 검사하는 방법. 100% 정확도를 포기하고 99.9%의 효율을 얻는 확률적 자료구조의 세계. 비트코인 지갑과 스팸 필터는 왜 이것을 쓸까요?

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

코드 리뷰를 하다 보면 이런 코드를 자주 봅니다.
// ❌ 모든 변수에 useMemo를 쓴 강박적인 코드
const TaxCalculator = ({ price, taxRate }) => {
const calculatedTax = useMemo(() => price * taxRate, [price, taxRate]);
const formattedPrice = useMemo(() => `${price}`, [price]);
return <div>{formattedPrice} + {calculatedTax}</div>;
};
주니어 개발자분이 물어봅니다. "변수가 변경될 때만 계산하니까 더 빠른 거 아닌가요?"
하지만 프로파일러(Profiler)를 돌려보니, 이 컴포넌트의 렌더링 속도는 useMemo를 쓰기 전보다 1.5배 더 느려졌습니다.
단순 곱셈(*)을 피하겠다고, 더 비싼 메모이제이션 연산(배열 생성 + 의존성 비교 + 메모리 할당)을 수행했기 때문입니다.
저는 "재계산을 안 하면 무조건 이득이다"라고 단순하게 생각했습니다.
Dynamic Programming(동적 계획법)을 배울 때 메모이제이션은 마법의 도구였으니까요.
그래서 리액트 컴포넌트의 모든 객체, 함수, 변수를 useMemo와 useCallback으로 감쌌습니다.
하지만 React의 렌더링은 생각보다 엄청나게 빠릅니다.
자바스크립트 엔진(V8) 입장에서 price * taxRate 같은 단순 연산은 0.000001초도 안 걸립니다.
반면 useMemo를 쓰면?
[price, taxRate])을 생성하고배보다 배꼽이 더 큰(Overhead) 상황이 벌어진 겁니다.
이걸 "택배 주문"에 비유하니 이해가 됐습니다.
즉, "내가 아끼려는 연산 비용"이 "메모이제이션 자체 비용"보다 클 때만 useMemo를 써야 합니다.
다음과 같은 경우는 useMemo를 쓰면 손해입니다. 당장 지우세요.
const text = "Hi " + name;const style = { color: 'red' }; (이걸 props로 넘길 때만 주의하면 됨)filter, map은 그냥 매번 돌려도 티도 안 납니다.그럼 언제 써야 할까요? React 공식 문서는 "1ms 이상 걸리는 연산"을 기준으로 제시합니다.
const AnalyticsDashboard = ({ chaosData }) => {
// ✅ 1. 데이터가 수만 건이라 필터링에 50ms 이상 걸릴 때
const filteredData = useMemo(() => {
return chaosData.filter(d => heavyComputation(d));
}, [chaosData]);
// ✅ 2. 참조 동일성(Referential Equality)이 필요할 때
// 이 객체가 useEffect의 의존성으로 들어가거나, React.memo 컴포넌트의 props로 갈 때
const config = useMemo(() => ({ theme: 'dark' }), []);
return <ComplexChart data={filteredData} config={config} />;
};
useMemo와 useCallback의 진짜 가치는 하위 컴포넌트의 불필요한 리렌더링을 막을 때 빛을 발합니다.
// 자식 컴포넌트가 React.memo로 감싸져 있다면
const Child = React.memo(({ onClick }) => {
console.log("Child Rendered");
return <button onClick={onClick}>Click</button>;
});
const Parent = () => {
// ❌ useCallback 없이 넘기면, Parent가 그려질 때마다 함수가 새로 생성됨
// -> Child 입장에선 props가 바뀌었으니 React.memo가 깨지고 리렌더링됨!
const handleClick = () => {};
// ✅ useCallback으로 감싸야 함수 참조가 유지됨 -> Child 리렌더링 방지 성공
const handleMemoClick = useCallback(() => {}, []);
return <Child onClick={handleMemoClick} />;
};
즉, React.memo를 쓴 컴포넌트에 props를 넘길 때만 useCallback/useMemo가 성능 최적화의 의미를 가집니다. 그 외에는 그냥 가독성만 해치는 노이즈입니다.
이제 이 논쟁도 곧 끝날지 모릅니다. React Compiler (코드명 Forget)가 도입되었기 때문입니다.
이 컴파일러는 빌드 타임에 코드를 분석해서, 알아서 useMemo, useCallback을 자동으로 삽입해 줍니다.
개발자가 수동으로 의존성 배열(deps array)을 관리하느라 실수할 필요도 없고, 어디에 메모이제이션을 적용할지 고민할 필요도 없습니다. 컴파일러가 "여기 값 안 바뀌었네? 그럼 그냥 재사용해"라고 최적화해버립니다.
우리가 지금 배워야 할 것은 useMemo 문법이 아니라, "변하지 않는 값과 변하는 값을 구분하는 데이터 흐름 설계 능력"입니다. 도구는 자동화되지만, 설계는 여전히 인간의 몫이니까요.
"이게 무거운 연산인지 어떻게 알아요?" 감으로 찍지 말고 측정하세요.
console.time('filter');
const result = heavyFilter(data);
console.timeEnd('filter'); // "filter: 0.2ms" -> useMemo 필요 없음!
혹은 React DevTools의 Profiler 탭에서 "Record"를 누르고 앱을 조작해 보세요. 특정 컴포넌트가 "Why did this render?"에 "Parent props changed"라고 뜨는지 확인하면 됩니다.
useMemo는 캐시 보장(Semantic Guarantee)이 아니다 자세히 살펴보기React 공식 문서를 보면 무서운 말이 있습니다. "React는 메모리 확보를 위해 캐시된 값을 버릴 수도 있습니다."
즉, useMemo를 로직의 정합성을 위해 쓰면 안 됩니다.
"이 변수는 useMemo로 감쌌으니까 절대 useEffect를 트리거하지 않겠지?" -> 틀렸습니다.
React가 내부적으로 메모리가 부족하면 (혹은 미래의 어떤 최적화를 위해) 캐시를 지우고 새로 계산해버릴 수 있습니다.
데이터가 일치하는지 보장하려면 useRef를 써야 합니다. useMemo는 오직 성능을 위한 힌트일 뿐입니다.
useMemo의 함정전역 상태 관리를 위해 Context를 만들 때가 useMemo가 가장 빛나는 순간입니다.
// ❌ 나쁜 예
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
// value 객체가 렌더링마다 새로 만들어짐!
// -> 하위의 모든 Consumer들이 불필요하게 리렌더링됨
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
이 코드는 UserProvider가 리렌더링 될 때마다 value 객체가 {...}로 새로 생성됩니다.
참조 주소가 바뀌니, 이걸 구독하는 모든 하위 컴포넌트가 "어? 값 바뀌었네?" 하고 다 같이 리렌더링 됩니다. (Context Hell).
// ✅ 좋은 예
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
여기서는 연산 비용({})이 문제가 아니라, 참조 동일성(Referential Equality)을 지켜서 하위 트리를 보호하는 게 목적입니다. 이때는 useMemo가 필수입니다.
useRef로 변수 고정하기 (Stable Value)가끔은 useMemo도 사치일 때가 있습니다.
"처음 한 번만 계산하고, 영원히 안 바꾸고 싶은 값"이 있다면 useRef가 더 가볍습니다.
// 컴포넌트 생명주기 내내 상수처럼 쓰임
const id = useRef(uuidv4()).current;
의존성 배열 []조차 필요 없습니다. 그냥 완벽한 상수입니다.
useMemo는 공짜가 아니다. 단순 변수나 함수에 쓰지 마라. 수천 개의 데이터를 다루거나 React.memo 자식에게 Props를 넘길 때만 써라. 섣부른 최적화는 만악의 근원이다.