Communitygithub.com

React 성능 최적화

Vercel 엔지니어링 팀이 선별한 React 및 Next.js 성능 최적화 패턴

React 성능 최적화란 무엇인가요?

이 스킬은 React 18/19와 Next.js 애플리케이션의 성능을 향상시키기 위한 포괄적인 가이드입니다. Vercel Labs의 `react-best-practices` 저장소에서 가져온 이 규칙은 우선순위별로 구성되어 있으며, 워터폴 제거, 번들 크기 최적화, 서버 사이드 성능, 클라이언트 데이터 페칭, 리렌더링 최적화 등 8가지 주요 범주를 다룹니다. 코드 리뷰와 리팩토링 시 의사 결정 트리를 제공하여 개발자가 특정 성능 문제를 효율적으로 진단하고 해결할 수 있도록 돕습니다. Next.js 13.5+, React 19, SWR, TanStack Query 등 주요 도구와 라이브러리의 최신 기능을 활용한 실용적인 코드 예제와 구체적인 지침을 포함하고 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/react-performance

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

React 성능

React 18/19와 Next.js를 위한 성능 최적화 패턴입니다. Vercel Labs react-best-practices (MIT, v1.0.0)에서 가져왔습니다. 이 스킬은 규칙을 우선순위별로 정리하고, 능동적인 코드 리뷰와 리팩토링을 위한 의사 결정 트리를 제공합니다.

활성화 시점

  • 성능을 위해 React/Next.js 코드를 작성하거나 검토할 때
  • 느린 페이지 로드, 느린 상호 작용, 또는 클라이언트의 높은 CPU 사용을 진단할 때
  • 번들 크기 또는 Lighthouse Core Web Vitals 회귀를 감사할 때
  • 서버 컴포넌트/API 라우트에서 워터폴을 제거할 때
  • 클라이언트 측 리렌더링을 줄일 때
  • 긴 목록, 애니메이션, 또는 하이드레이션을 최적화할 때
  • app/, pages/, components/, 또는 데이터 레이어를 다루는 PR에서 최적화 선택을 감사할 때

우선순위 인덱스

우선순위범주접두사적용 시점
1 — 치명적워터폴 제거async-await 다음에 독립적인 await가 올 때마다
2 — 치명적번들 크기 최적화bundle-초기 로드 JS, 라우트 수준 임포트, 써드파티 라이브러리
3 — 높음서버 사이드 성능server-RSC, 서버 액션, API 라우트, SSR
4 — 중간-높음클라이언트 데이터 페칭client-SWR / TanStack Query / 훅의 원시 fetch
5 — 중간리렌더링 최적화rerender-빈번한 상태 업데이트, 부모-자식 팬아웃
6 — 중간렌더링 성능rendering-긴 목록, 애니메이션, 하이드레이션
7 — 낮음-중간JavaScript 성능js-핫 루프, 빈번한 할당
8 — 낮음고급 패턴advanced-이펙트-이벤트 통합, 안정적인 refs

1. 워터폴 제거 (치명적)

"워터폴은 성능의 가장 큰 적입니다" — 모든 순차적인 await는 전체 네트워크 지연 시간을 추가합니다.

await 전에 저렴한 조건 확인

원격 데이터를 기다리기 전에 동기 조건(프롭, 환경 변수, 하드코딩된 플래그)을 확인하세요.

// 올바르지 않음
async function Page({ id }: { id: string }) {
  const flag = await getFlag("show-page");
  if (!flag || !id) return null;
  const data = await getData(id);
  // ...
}

// 올바름 — 저렴한 동기 조건으로 먼저 단락 평가
async function Page({ id }: { id: string }) {
  if (!id) return null;
  const flag = await getFlag("show-page");
  if (!flag) return null;
  const data = await getData(id);
}

사용될 때까지 await 지연

await를 그것을 사용하는 분기 안으로 옮기세요.

// 올바르지 않음 — 데이터가 필요하다고 결정하기 전에 await
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);

// 올바름
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);

독립적인 작업에는 Promise.all 사용

// 올바르지 않음 — 순차적
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);

// 올바름 — 병렬
const [user, posts, followers] = await Promise.all([
  getUser(id),
  getPosts(id),
  getFollowers(id),
]);

부분 의존성 — 일찍 시작하고, 늦게 await

// 올바름 — 모든 프로미스를 시작하고, 각 결과가 필요할 때만 await
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);

스트리밍을 위한 Suspense

<Suspense> 경계를 데이터 가까이에 배치하여 느린 하위 트리가 스트리밍되는 동안 페이지가 가능한 부분을 먼저 그리도록 합니다. 단점: 콘텐츠가 도착하면 레이아웃 이동이 발생합니다 — 공간을 예약하세요 (스켈레톤 또는 min-height).

서버 컴포넌트: 구성을 통한 병렬 처리

// 올바르지 않음 — 형제 await가 하나의 컴포넌트 내에서 순차적으로 실행됨
export default async function Page() {
  const user = await getUser();
  const cart = await getCart();
  return <View user={user} cart={cart} />;
}

// 올바름 — 자식으로 분할, React가 병렬로 실행

export default async function Page() {
  return (
    <View>
      <UserSection />
      <CartSection />
    </View>
  );
}

2. 번들 크기 최적화 (치명적)

직접 임포트, 배럴 사용 금지

배럴 index.ts 파일은 트리 쉐이킹이 대부분의 코드를 제거하더라도 번들러가 전체 모듈 그래프를 탐색하도록 강제합니다. 직접 임포트는 많은 실제 앱에서 초기 로드 JS를 200-800ms 절약합니다.

// 올바르지 않음
import { Button, Card, Modal } from "@/components";

// 올바름
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";

Next.js 13.5+에는 나열된 패키지에 대해 이를 자동화하는 Optimize Package Imports가 있습니다 — 사용하세요; 목록에 없는 라이브러리는 여전히 수동 직접 임포트가 필요합니다.

정적으로 분석 가능한 경로

// 올바르지 않음 — 번들러/추적 분석을 무력화
const mod = await import(`./pages/${name}`);

// 올바름 — 분기별로 명시적
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");

무거운 컴포넌트를 위한 동적 임포트

import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <Skeleton />,
  ssr: false, // 클라이언트 전용일 때
});

써드파티 스크립트 지연

분석, 로깅, 위젯 지원은 하이드레이션 이후에 로드하세요. strategy="afterInteractive" (기본값) 또는 "lazyOnload"와 함께 next/script를 사용하세요.

조건부 모듈 로딩

if (user.role === "admin") {
  const { AdminPanel } = await import("./admin/AdminPanel");
  // ...
}

호버/포커스 시 사전 로드

사용자가 클릭할 때까지 번들이 캐시에 있도록 호버 시 <link rel="preload"> 또는 import()를 트리거하세요.

3. 서버 사이드 성능 (높음)

API 라우트처럼 서버 액션 인증

모든 "use server" 함수는 공개 엔드포인트입니다. 액션 내부에서 인증 AND 권한 부여를 수행하세요 — 절대 호출하는 클라이언트 컴포넌트의 게이팅에 의존하지 마세요.

"use server";
export async function deleteUser(formData: FormData) {
  const session = await getSession();
  if (!session?.user) throw new Error("Unauthorized");
  const targetId = String(formData.get("id"));
  if (session.user.role !== "admin" && session.user.id !== targetId) {
    throw new Error("Forbidden");
  }
  await db.user.delete({ where: { id: targetId } });
}

React.cache()로 요청별 중복 제거

import { cache } from "react";

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

React.cache는 단일 요청 내에서 중복을 제거합니다. 같은 렌더링에서 세 개의 서버 컴포넌트가 getUser("1")을 호출하면 = 하나의 DB 쿼리.

교차 요청 데이터를 위한 LRU 캐시

요청별로 변경되지 않는 데이터(설정, 조회 테이블)의 경우, React 외부에서 LRU 캐시 또는 unstable_cache로 캐싱하세요.

RSC 프롭에서 중복 직렬화 방지

서버 컴포넌트가 동일한 데이터를 여러 클라이언트 컴포넌트에 렌더링할 때, 데이터는 소비자당 한 번씩 직렬화됩니다. 클라이언트 컴포넌트를 끌어올리고 자식을 전달하세요.

정적 I/O를 모듈 스코프로 호이스팅

// 올바름 — 모듈 로드 시 한 번 실행
const fontData = readFileSync(fontPath);

export async function Page() {
  return <Banner font={fontData} />;
}

RSC/SSR에서 가변 모듈 수준 상태 사용 금지

서버의 모듈 상태는 모든 요청 간에 공유됩니다 — 사용자 간 경쟁 조건입니다. 대신 요청 범위 저장소(headers(), cookies(), async context)를 사용하세요.

클라이언트 컴포넌트에 전달되는 데이터 최소화

클라이언트가 필요한 것만 직렬화하세요. 필드를 제거하고, 페이지네이션하고, DB 계층에서 열을 프로젝션하세요.

항목별로 Promise.all로 중첩된 페치 병렬화

const users = await getUsers();
const enriched = await Promise.all(
  users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);

비차단 작업에 after() 사용

Next.js 15 after()는 응답이 전송된 후 작업을 실행합니다 — 로깅, 캐시 워밍, 분석.

import { after } from "next/server";
export async function GET() {
  const data = await getData();
  after(() => logAnalytics(data));
  return Response.json(data);
}

4. 클라이언트 데이터 페칭 (중간-높음)

SWR / TanStack Query로 중복 제거

여러 컴포넌트가 useUser(id)를 호출하면 하나의 네트워크 요청과 하나의 캐시 항목을 공유해야 합니다. SWR 또는 TanStack Query를 사용하세요 — 공유 데이터를 위해 직접 useEffect + fetch를 절대 만들지 마세요.

전역 이벤트 리스너 중복 제거

// 올바르지 않음 — 모든 컴포넌트가 자체 리스너를 추가
useEffect(() => {
  window.addEventListener("scroll", handler);
  return () => window.removeEventListener("scroll", handler);
}, []);

// 올바름 — 훅 + 전역 subject를 통한 단일 공유 리스너
const useScroll = createScrollHook(); // 내부적으로 싱글톤 subject

스크롤을 위한 수동 리스너

window.addEventListener("scroll", handler, { passive: true });

스크롤 부드러움 향상; 리스너는 preventDefault()를 할 수 없습니다.

localStorage: 버전 + 최소화

  • 항상 version 필드를 저장하세요; 스키마 변경 시 버전을 올리고 이전 데이터를 마이그레이션하거나 폐기하세요
  • 페이로드를 작게 유지하세요 — localStorage는 동기적이며 메인 스레드를 차단합니다

5. 리렌더링 최적화 (중간)

콜백에서만 사용되는 상태를 구독하지 마세요

// 올바르지 않음 — count가 변경될 때마다 리렌더링
const count = useStore((s) => s.count);
const handler = () => doSomething(count);

// 올바름 — 호출 시 한 번 읽기
const handler = () => {
  const count = useStore.getState().count;
  doSomething(count);
};

비용이 많이 드는 작업을 메모화된 컴포넌트로 추출

// 올바름 — `items`가 변경될 때만 자식 리렌더링
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
  return <Chart data={transform(items)} />;
});

기본 비원시 프롭을 호이스팅

// 올바르지 않음 — 렌더링마다 새 배열이 만들어져 memo가 깨짐
<List items={items ?? []} />

// 올바름
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />

이펙트의 원시 의존성

// 올바르지 않음 — 렌더링마다 새 객체 식별
useEffect(() => {}, [{ id, name }]);

// 올바름 — 원시값
useEffect(() => {}, [id, name]);

파생된 불리언에 구독, 원시 값에는 구독하지 마세요

// 올바르지 않음 — 모든 장바구니 변경에 리렌더링
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;

// 올바름 — 비어 있음 상태가 변경될 때만 리렌더링
const hasItems = useStore((s) => s.cart.length > 0);

useEffect를 통하지 않고 렌더링 중에 파생

// 올바르지 않음
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);

// 올바름
const full = `${first} ${last}`;

안정적인 콜백을 위한 함수형 setState

// 올바름
const increment = useCallback(() => setCount((c) => c + 1), []);

비용이 많이 드는 값을 위한 지연 상태 초기화

const [tree] = useState(() => parseTree(largeInput));

단순한 원시값에 memo 사용 방지

useMemo(() => x + 1, [x])는 오버헤드입니다. Memo는 객체 식별과 비용이 많이 드는 계산에 사용하세요.

독립적인 의존성을 가진 훅 분리

// 올바르지 않음 — 두 소스 중 하나가 변경되면 두 selector가 모두 다시 실행됨
const { a, b } = useSomething(source1, source2);

// 올바름
const a = useA(source1);
const b = useB(source2);

상호작용 로직을 이벤트 핸들러로 이동

이벤트 핸들러는 사용자 작업 시에만 실행됩니다 — useEffect는 의존성이 변경될 때마다 다시 실행됩니다.

긴급하지 않은 업데이트에 startTransition

const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));

비용이 많이 드는 렌더링에 useDeferredValue

const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);

일시적이고 빈번한 값을 위한 useRef

자주 변경되지만 리렌더링을 트리거해서는 안 되는 값(타임스탬프, 마지막 키, 누산기)에 사용합니다.

컴포넌트 내부에 컴포넌트를 정의하지 마세요

// 올바르지 않음 — Inner는 Outer가 렌더링될 때마다 새 컴포넌트
function Outer() {
  const Inner = () => <span />;
  return <Inner />;
}

각 렌더링은 새로운 Inner 타입을 만들어 재조정을 방해하고 자식을 마운트 해제합니다.

6. 렌더링 성능 (중간)

SVG가 아닌 래퍼에 애니메이션 적용

SVG 주변의 <div> 래퍼를 변환하는 것은 GPU 가속이 됩니다; SVG 자체를 변환하면 페인트가 트리거됩니다.

긴 목록에 content-visibility: auto

.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }

브라우저가 오프스크린 렌더링을 건너뜁니다 — 수백 개의 행이 있는 목록에 큰 이점.

정적 JSX 호이스팅

const STATIC_HEADER = <h1>Title</h1>;
function Page() {
  return <>{STATIC_HEADER}<Body /></>;
}

SVG: 좌표 정밀도 줄이기

d="M10.123456,20.654321" → d="M10.12,20.65". 각 숫자는 바이트를 소비합니다; 시각적 차이는 서브픽셀입니다.

인라인 스크립트를 통한 하이드레이션 플리커 방지

하이드레이션 전에 필요한 값(테마, 로케일)에 대해, React가 마운트되기 전에 document.documentElement.dataset.*를 설정하는 <script>를 인라인하세요.

예상되는 하이드레이션 불일치를 좁게 억제

<time suppressHydrationWarning>{new Date().toLocaleString()}</time>

알려진 차이가 있는 리프 노드에만 사용하세요 — 다른 자식을 포함하는 트리에는 절대 사용하지 마세요.

마운트/언마운트 대신 <Activity>로 표시/숨기기

React 19 <Activity mode="visible|hidden">는 트리 상태와 이펙트를 마운트된 상태로 유지하지만 숨깁니다 — 탭과 아코디언의 경우 언마운트/리마운트보다 저렴합니다.

조건부 렌더링에 && 대신 삼항 연산자

// 올바르지 않음 — `0`이 텍스트 노드로 렌더링됨
{count && <Badge>{count}</Badge>}

// 올바름
{count > 0 ? <Badge>{count}</Badge> : null}

로딩 상태에 useTransition

startTransition을 액션과 쌍으로 사용하세요; React는 다음 상태가 계산되는 동안 isPending으로 이전 UI를 표시합니다.

React DOM 리소스 힌트

import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")

…

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬