useMemo: 값 계산 최적화
React.memo: 컴포넌트 렌더링 최적화
useCallback: 함수 재생성 방지
useRef: DOM 접근, 값 저장
→ 성능 최적화 & 렌더링 제어에 쓰이는 핵심 훅과 컴포넌트
"use client";
import { useState, useMemo } from "react";
type User = {
id: number;
name: string;
email: string;
};
const dummyUsers: User[] = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`,
email: `user${i}@example.com`,
}));
export default function UserTable() {
const [search, setSearch] = useState("");
const filteredUsers = useMemo(() => {
console.log("🔍 사용자 목록 필터링 실행");
return dummyUsers.filter((user) =>
user.name.toLowerCase().includes(search.toLowerCase())
);
}, [search]);
return (
<div className="p-6 space-y-4">
<h2 className="text-xl font-bold">📋 사용자 목록</h2>
<input
type="text"
placeholder="이름으로 검색..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="border px-3 py-1 w-full"
/>
<ul className="h-[300px] overflow-y-scroll border rounded p-2 space-y-1">
{filteredUsers.slice(0, 50).map((user) => (
<li key={user.id} className="text-sm text-gray-700 dark:text-gray-300">
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}
코드 설명:
1. 10,000명의 유저 데이터를 메모리에 저장
2. 사용자가 입력한 search 키워드에 따라useMemo를 통해 결과를 캐싱해서 재사용
3. 필터된 유저 목록은 최대 50명만 보여줌filteredUsers.slice(0, 50)
4. search 값이 바뀔때만filteredUsers이 실행됨, 외에는 캐싱된filteredUsers사용
🤔 근데 검색할때마다 필터 함수 돌릴거면 캐싱해서 재사용하는 의미가 없지 않나?
→ react는 state 값이 변경될 때 마다 컴포넌트 함수 전체를 다시 실행하기 때문에 useMemo가 없으면 필터링 연산도 매번 다시 실행된다 그렇기 때문에 큰 계산이 있거나 결과가 자주 변하지 않을때는 useMemo를 사용하는 것이 좋다
"use client";
import { useState } from "react";
import React from "react";
export default function MemoExample() {
const [count, setCount] = useState(0);
const [input, setInput] = useState("");
return (
<div className="p-6 space-y-4">
<h2 className="text-xl font-bold">React.memo 사용 비교</h2>
<button
onClick={() => setCount((prev) => prev + 1)}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
숫자 증가 (count: {count})
</button>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="입력해보세요"
className="border px-2 py-1 rounded"
/>
<div className="flex gap-10 mt-6">
<NormalChild />
<MemoizedChild />
</div>
</div>
);
}
// 일반 컴포넌트
function NormalChild() {
console.log("❌ NormalChild 렌더링");
return <div className="p-4 bg-red-100 rounded">일반 자식 컴포넌트</div>;
}
// memo로 감싼 컴포넌트
const MemoizedChild = React.memo(function MemoChild() {
console.log("✅ MemoizedChild 렌더링");
return <div className="p-4 bg-green-100 rounded">React.memo 자식 컴포넌트</div>;
});
→ 부모가 리렌더링돼도 props가 같으면 해당 컴포넌트는 리렌더링되지 않음
실행해보면 초기 렌더링 console

숫자 증가 버튼 한번 클릭 시

input에 텍스트 입력 시

💡 초기 렌더링 이후 자식 컴포넌트 리렌더링이 되지 않는 것을 볼수 있다!
"use client";
import React, { useCallback, useState } from "react";
const ChildButton = ({ label, onClick }: { label: string; onClick: () => void }) => {
console.log(`🔄 ${label} 렌더링됨`);
return (
<button
onClick={onClick}
className="px-4 py-2 rounded text-white font-medium"
style={{
backgroundColor: label.includes("useCallback") ? "#10b981" : "#3b82f6",
}}
>
{label}
</button>
);
};
const MemoizedButton = React.memo(ChildButton);
export default function CallbackComparison() {
const [count, setCount] = useState(0);
// ❌ useCallback 없이: 매 렌더마다 함수 새로 생성됨
const wthoutCallback = () => {
console.log("클릭 (without useCallback)");
};
// ✅ useCallback 사용: 함수 메모이제이션됨
const withCallback = useCallback(() => {
console.log("클릭 (with useCallback)");
}, []);
return (
<div className="p-6 space-y-6">
<h2 className="text-xl font-bold">🔁 useCallback 사용 / 미사용 비교</h2>
<p className="text-gray-700">count: {count}</p>
<button
onClick={() => setCount((prev) => prev + 1)}
className="px-3 py-1 bg-gray-800 text-white rounded"
>
count 증가 (부모 컴포넌트 리렌더)
</button>
<div className="flex gap-4 mt-4">
<MemoizedButton label="❌ useCallback 미사용" onClick={wthoutCallback} />
<MemoizedButton label="✅ useCallback 사용" onClick={withCallback} />
</div>
</div>
);
}
→ 매번 새로운 함수 객체를 생성하지 않도록 방지
(특히 React.memo 자식에게 props로 넘길 때 유용)
실행해보면 초기 렌더링 console

count 증가 (부모 컴포넌트 리렌더) 버튼 한번 클릭 시

💡 useCallback 함수 사용 시 메모이제이션 된 값을 사용하는걸 볼 수 있다!
"use client";
import { useRef, useState } from "react";
export default function RefExample() {
const countRef = useRef(0); // 리렌더링 없이 값 저장
const [renderCount, setRenderCount] = useState(0); // 렌더링 확인용
const handleClick = () => {
countRef.current += 1; // 화면에는 안 보임
};
const triggerRender = () => {
setRenderCount((prev) => prev + 1); // 강제 리렌더링
};
return (
<div className="space-y-4 p-6">
<h2 className="text-lg font-semibold">🔄 useRef 리렌더링 테스트</h2>
<button
onClick={handleClick}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
useRef 값 +1 (렌더링 없음)
</button>
<button
onClick={triggerRender}
className="px-4 py-2 bg-gray-700 text-white rounded"
>
강제로 리렌더링
</button>
<div className="mt-4 text-gray-800">
<p>useRef 값: {countRef.current}</p>
<p>렌더링 횟수: {renderCount}</p>
</div>
</div>
);
}

useRef값 +1 버튼을 3번 클릭했을 때
강제로 리렌더링 버튼을 눌렀을 때