[React] useRef, useCallback, useMemo
개요
useRef, useCallback, useMemo는 성능 최적화와 DOM 접근을 위한 Hook입니다. 처음에는 useState와 useEffect만으로도 충분하지만, 앱이 커지면 이 Hook들이 필요합니다.
useRef
DOM 요소 직접 접근
import { useRef } from "react";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus(); // DOM 메서드 직접 호출
};
return (
<div>
<input ref={inputRef} placeholder="입력창" />
<button onClick={handleFocus}>포커스 이동</button>
</div>
);
}
리렌더링 없이 값 유지
useState와 다르게 값이 변경되어도 컴포넌트가 다시 렌더링되지 않습니다.
import { useState, useRef, useEffect } from "react";
function Timer() {
const [display, setDisplay] = useState("0.0s");
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const startTimeRef = useRef<number>(0);
const start = () => {
startTimeRef.current = Date.now();
intervalRef.current = setInterval(() => {
const elapsed = (Date.now() - startTimeRef.current) / 1000;
setDisplay(`${elapsed.toFixed(1)}s`);
}, 100);
};
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
return (
<div>
<p>{display}</p>
<button onClick={start}>시작</button>
<button onClick={stop}>정지</button>
</div>
);
}
useState vs useRef 비교
| 특징 | useState | useRef |
|---|---|---|
| 값 변경 시 리렌더링 | ✅ 발생 | ❌ 발생 안 함 |
| 렌더링에 값 반영 | ✅ | ❌ (직접 읽어야 함) |
| 주 용도 | UI에 표시할 상태 | DOM 접근, 렌더링 무관 값 |
useCallback
함수 메모이제이션
import { useState, useCallback } from "react";
import React from "react";
// React.memo로 감싼 자식 컴포넌트
const Button = React.memo(({ onClick, label }: { onClick: () => void; label: string }) => {
console.log(`${label} 버튼 렌더링`);
return <button onClick={onClick}>{label}</button>;
});
function App() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// ❌ useCallback 없이: 매 렌더링마다 새 함수 생성 → Button 항상 리렌더링
// const increment = () => setCount((c) => c + 1);
// ✅ useCallback: count가 변하지 않으면 같은 함수 재사용 → Button 리렌더링 없음
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []); // 의존성 없음 → 최초 1회만 생성
return (
<div>
<p>count: {count}, other: {other}</p>
<Button onClick={increment} label="카운트 증가" />
<button onClick={() => setOther((o) => o + 1)}>other 증가</button>
</div>
);
}
useMemo
계산 결과 메모이제이션
import { useState, useMemo } from "react";
function App() {
const [numbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const [filter, setFilter] = useState<"all" | "even" | "odd">("all");
const [other, setOther] = useState(0);
// ❌ useMemo 없이: other가 바뀔 때도 매번 계산 반복
// const filtered = numbers.filter(...)
// ✅ useMemo: filter가 변경될 때만 재계산
const filtered = useMemo(() => {
console.log("필터 계산 실행");
return numbers.filter((n) => {
if (filter === "even") return n % 2 === 0;
if (filter === "odd") return n % 2 !== 0;
return true;
});
}, [numbers, filter]);
return (
<div>
<div>
{(["all", "even", "odd"] as const).map((f) => (
<button key={f} onClick={() => setFilter(f)}>{f}</button>
))}
</div>
<p>{filtered.join(", ")}</p>
<button onClick={() => setOther((o) => o + 1)}>other: {other}</button>
</div>
);
}
객체/배열 안정화
import { useState, useMemo } from "react";
import React from "react";
const Child = React.memo(({ config }: { config: { color: string } }) => {
console.log("Child 렌더링");
return <div style=>텍스트</div>;
});
function Parent() {
const [text, setText] = useState("");
// ❌ 매 렌더링마다 새 객체 생성 → Child 항상 리렌더링
// const config = { color: "red" };
// ✅ useMemo로 동일 객체 유지 → text 변경 시 Child 리렌더링 안 함
const config = useMemo(() => ({ color: "red" }), []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<Child config={config} />
</div>
);
}
언제 사용해야 하나?
| Hook | 사용 상황 | 사용 안 해도 되는 경우 |
|---|---|---|
useRef |
DOM 직접 조작, 렌더링 무관 값 저장 | 일반 상태 관리 |
useCallback |
자식에게 함수 Props 전달 + React.memo 사용 |
자식이 React.memo 없을 때 |
useMemo |
계산 비용이 클 때, 객체/배열 Props 안정화 | 단순 계산 |
성능 최적화는 문제가 실제로 생겼을 때 적용하기. 모든 곳에 사용하면 오히려 코드가 복잡해집니다.