ReactReact 기초 · 6기초

useRef와 useMemo — DOM 참조와 성능 최적화

ReactuseRefuseMemouseCallback성능최적화

useRef: DOM 참조

import { useRef, useEffect } from "react";

function AutoFocusInput() {
    const inputRef = useRef<HTMLInputElement>(null);

    useEffect(() => {
        // 마운트 후 입력창에 포커스
        inputRef.current?.focus();
    }, []);

    return <input ref={inputRef} placeholder="자동 포커스" />;
}

useRef: 렌더링 없이 값 저장

렌더링을 유발하지 않고 값을 유지합니다.

function StopWatch() {
    const [time, setTime] = useState(0);
    const [running, setRunning] = useState(false);
    const intervalRef = useRef<number | null>(null);

    function start() {
        if (running) return;
        setRunning(true);
        intervalRef.current = window.setInterval(() => {
            setTime(t => t + 1);
        }, 1000);
    }

    function stop() {
        if (intervalRef.current) clearInterval(intervalRef.current);
        setRunning(false);
    }

    // intervalRef.current 변경은 리렌더링을 유발하지 않음
    return (
        <div>
            <p>{time}초</p>
            <button onClick={start}>시작</button>
            <button onClick={stop}>정지</button>
        </div>
    );
}

useMemo: 비싼 계산 캐싱

import { useMemo, useState } from "react";

function FilteredProducts({ products }: { products: Product[] }) {
    const [query, setQuery] = useState("");
    const [minPrice, setMinPrice] = useState(0);

    // query나 minPrice, products가 바뀔 때만 재계산
    const filtered = useMemo(() => {
        console.log("필터링 실행");  // 렌더링마다 실행되지 않음
        return products
            .filter(p => p.name.includes(query))
            .filter(p => p.price >= minPrice);
    }, [products, query, minPrice]);

    return (
        <div>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <input type="number" value={minPrice} onChange={e => setMinPrice(Number(e.target.value))} />
            <ul>
                {filtered.map(p => <li key={p.id}>{p.name}</li>)}
            </ul>
        </div>
    );
}

useCallback: 함수 참조 안정화

import { useCallback, memo } from "react";

// memo: props가 같으면 리렌더링 건너뜀
const ExpensiveChild = memo(({ onAction }: { onAction: () => void }) => {
    console.log("자식 렌더링");
    return <button onClick={onAction}>액션</button>;
});

function Parent() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState("");

    // useCallback 없이: 매 렌더링마다 새 함수 → 자식도 리렌더링
    // useCallback 사용: 함수 참조 유지 → 자식 리렌더링 없음
    const handleAction = useCallback(() => {
        console.log("액션 실행");
    }, []);  // 의존성 없으면 최초 1회만 생성

    return (
        <div>
            <input value={name} onChange={e => setName(e.target.value)} />
            <p>{count}</p>
            <ExpensiveChild onAction={handleAction} />
        </div>
    );
}

언제 최적화가 필요한가?

flowchart TB
    Q{"성능 문제가\n실제로 있는가?"}
    Q -->|"아니오"| SKIP["최적화 건너뜀\n코드 복잡도 증가"]
    Q -->|"예"| PROFILE["React DevTools로\n프로파일링"]
    PROFILE --> SLOW["느린 컴포넌트 발견"]
    SLOW --> MEMO["memo, useMemo,\nuseCallback 적용"]

성능 최적화 원칙:

  1. 먼저 동작하게 만들고
  2. 측정 후 필요할 때만 최적화

useId: 고유 ID 생성

import { useId } from "react";

function FormField({ label }: { label: string }) {
    const id = useId();  // 서버-클라이언트 일관된 고유 ID

    return (
        <div>
            <label htmlFor={id}>{label}</label>
            <input id={id} type="text" />
        </div>
    );
}

정리

Hook용도
useRefDOM 참조, 렌더링 없는 값 저장
useMemo비싼 계산 결과 캐싱
useCallback함수 참조 안정화
memo()props 변화 없으면 리렌더링 건너뜀
useId고유 ID 생성

다음 편에서는 Context API — prop drilling 없이 전역 상태를 공유하는 방법을 배웁니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.