본문으로 건너뛰기
Previous
Next
React useMemo and useCallback: When They Pay Off

React useMemo and useCallback: When They Pay Off

React useMemo and useCallback: When They Pay Off

이 글의 핵심

useMemo and useCallback in React: reference stability and expensive work — when to use them, how to avoid premature optimization, and how to verify with Profiler.

Why Re-renders Happen

In React, a component re-renders when its state changes, its props change, or its parent re-renders. For most components, this is fine — renders are usually fast. The problem arises when:

  1. A computation inside the component is genuinely expensive (filtering 100k items, complex math)
  2. A new function or object reference is created on every render and passed to a memoized child — breaking that child’s memoization

useMemo and useCallback are tools to address these two specific cases. They are not a general-purpose performance boost.


useMemo: Memoize an Expensive Value

useMemo runs a function and caches the result. It only re-runs when the dependencies change:

import { useMemo, useState } from 'react';

type Item = { id: string; label: string; score: number };

function Leaderboard({ items }: { items: Item[] }) {
    const [query, setQuery] = useState('');
    const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');

    // Without useMemo: runs on every render, even when only sortDir changes
    // With useMemo: only re-runs when items or query changes
    const filtered = useMemo(() => {
        const q = query.trim().toLowerCase();
        if (!q) return items;
        return items.filter(item => item.label.toLowerCase().includes(q));
    }, [items, query]);  // dependencies

    // Second useMemo: only re-runs when filtered or sortDir changes
    const sorted = useMemo(() => {
        return [...filtered].sort((a, b) =>
            sortDir === 'desc' ? b.score - a.score : a.score - b.score
        );
    }, [filtered, sortDir]);

    return (
        <section>
            <input
                value={query}
                onChange={e => setQuery(e.target.value)}
                placeholder="Search..."
            />
            <button onClick={() => setSortDir(d => d === 'asc' ? 'desc' : 'asc')}>
                Sort {sortDir === 'desc' ? '↑' : '↓'}
            </button>
            <ul>
                {sorted.map(item => (
                    <li key={item.id}>{item.label}{item.score}</li>
                ))}
            </ul>
        </section>
    );
}

When the user toggles sort direction, filtered does not re-run — only sorted does. This pays off when items is large and the filter is slow.


useCallback: Stabilize a Function Reference

Every render creates new function objects. This matters when the function is passed to a child wrapped in React.memo — a new function reference breaks the memo comparison:

import { memo, useCallback, useState } from 'react';

// Wrapped in memo — only re-renders when props change by reference
const Row = memo(function Row({
    id,
    onSelect,
}: {
    id: string;
    onSelect: (id: string) => void;
}) {
    console.log(`Row ${id} rendered`);
    return <button onClick={() => onSelect(id)}>{id}</button>;
});

function Table({ ids }: { ids: string[] }) {
    const [active, setActive] = useState<string | null>(null);
    const [count, setCount] = useState(0);

    // Without useCallback: new function every render → Row always re-renders
    // With useCallback: same function reference → Row only re-renders if deps change
    const handleSelect = useCallback((id: string) => {
        setActive(id);
    }, []);  // no dependencies — never recreated

    return (
        <div>
            <p>Active: {active}</p>
            <button onClick={() => setCount(c => c + 1)}>
                Increment ({count})
            </button>
            {ids.map(id => (
                <Row key={id} id={id} onSelect={handleSelect} />
            ))}
        </div>
    );
}

When the user clicks “Increment”, count changes, Table re-renders — but handleSelect is the same reference, so Row does not re-render. Without useCallback, clicking increment would re-render all rows needlessly.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). It’s just syntax sugar for functions.


The Two Cases Where Memoization Helps

SituationHookWhen it pays off
Expensive pure computationuseMemoWhen the computation is measurably slow (>1ms) and runs on renders that don’t change its inputs
Stable reference for memo’d childuseCallback or useMemoWhen the child is wrapped in React.memo and the function/object is a prop
Creating a stable object for a Context valueuseMemoWhen the Context value is an object created inline and consumed by many components

Context Value Stability

A common source of unnecessary re-renders: creating a new object in the Context Provider on every render:

// BAD: new object every render → all consumers re-render
function AppProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<User | null>(null);
    const [theme, setTheme] = useState<'light' | 'dark'>('light');

    return (
        <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
            {children}
        </AppContext.Provider>
    );
}

// GOOD: stable object reference — consumers only re-render when user or theme changes
function AppProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<User | null>(null);
    const [theme, setTheme] = useState<'light' | 'dark'>('light');

    const value = useMemo(() => ({ user, setUser, theme, setTheme }), [user, theme]);

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

Or split the context into fast-changing and slow-changing parts:

// Fast-changing: theme, locale
const UIContext = createContext<UIState>(...);

// Slow-changing: auth state, user profile
const AuthContext = createContext<AuthState>(...);

// Components that only care about theme don't re-render on auth changes

Measuring with React DevTools Profiler

Never add useMemo/useCallback without measuring first. Use the React DevTools Profiler:

  1. Open browser DevTools → React tab → Profiler
  2. Click Record, interact with your component, click Stop
  3. Look for components with long render times (orange/red bars)
  4. Check the “Why did this render?” panel — if a component re-renders with “hook changed” on every parent render, useCallback might help

Typical findings:

  • Most components render in < 1ms — memoization adds more overhead than it saves
  • A few components with large lists or complex computations benefit significantly
  • Leaf components wrapped in memo benefit from stable callback references

When NOT to Use These Hooks

Cheap computations: a simple arithmetic expression, array slice, string concatenation — memoizing these is slower than just recomputing. The useMemo call itself has overhead.

Components that always re-render anyway: if a component renders when its state changes, and that state is the input to the useMemo, you’re not saving anything.

Non-memo children: useCallback only helps when the child uses React.memo (or a dependency array like useEffect). If the child always re-renders regardless, the stable reference doesn’t matter.

// No benefit: Child doesn't use memo, so it re-renders on every parent render anyway
function Parent() {
    const fn = useCallback(() => doSomething(), []);  // unnecessary
    return <Child onAction={fn} />;  // Child will re-render anyway
}

// Benefit: Child uses memo — the stable reference prevents unnecessary renders
const Child = memo(({ onAction }) => <button onClick={onAction}>Click</button>);

Dependency Array Gotchas

Stale closure: forgetting a dependency means the memoized value or callback captures an old value:

// BUG: userId is used inside but not listed as a dependency
const fetchUser = useCallback(async () => {
    const data = await api.get(`/users/${userId}`);  // always uses initial userId
    setUser(data);
}, []);  // should be [userId]

// CORRECT
const fetchUser = useCallback(async () => {
    const data = await api.get(`/users/${userId}`);
    setUser(data);
}, [userId]);  // re-created when userId changes

Use the eslint-plugin-react-hooks rule react-hooks/exhaustive-deps to catch missing dependencies automatically.

Object/array in dependencies: objects are compared by reference. If a parent creates options = { limit: 10 } inline, it’s a new object every render — which defeats the memoization:

// BAD: options is a new object every render → useMemo always re-runs
const results = useMemo(() => filter(data, options), [data, options]);

// GOOD: either memoize options separately, or list specific values
const results = useMemo(() => filter(data, { limit: 10 }), [data]);
// Or:
const { limit, offset } = options;
const results = useMemo(() => filter(data, { limit, offset }), [data, limit, offset]);

Practical Checklist

Before adding useMemo or useCallback, answer:

  1. Did you measure? Open the Profiler and confirm the component renders unnecessarily or slowly.
  2. Is the computation expensive? Filtering 10k items: yes. Adding two numbers: no.
  3. Is the child memoized? useCallback only helps if the child is wrapped in React.memo.
  4. Are all dependencies listed? Run react-hooks/exhaustive-deps and fix all warnings.
  5. Does it actually help? Re-measure with the Profiler after adding the hook.

Key Takeaways

  • useMemo caches an expensive computed value — re-runs only when dependencies change
  • useCallback caches a function reference — prevents re-creating it on every render
  • Both hooks help in two specific cases: expensive computation, or stable reference for React.memo children
  • useCallback(fn, deps) is syntactic sugar for useMemo(() => fn, deps)
  • Measure first with the Profiler — don’t add these hooks speculatively
  • Context providers should wrap the value object in useMemo to prevent all consumers from re-rendering on unrelated state changes
  • Missing dependencies cause stale closures; use eslint-plugin-react-hooks to catch them

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. useMemo and useCallback in React: reference stability and expensive work — when to use them, how to avoid premature opti… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

React, useMemo, useCallback, Performance, Rendering, Memoization, Hooks 등으로 검색하시면 이 글이 도움이 됩니다.