Communitygithub.com

React 성능 최적화

Vercel 엔지니어링에서 발췌한 React 및 Next.js 성능 최적화 패턴입니다.

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

이 스킬은 Vercel Labs의 `react-best-practices` 저장소에서 가져온 React 18/19 및 Next.js를 위한 종합적인 성능 최적화 패턴을 제공합니다. 주요 목표는 워터폴(waterfall) 제거, 번들 크기 최적화, 서버 사이드 성능 향상, 그리고 클라이언트 리렌더링 최소화입니다. 스킬은 8개의 우선순위 수준으로 구성된 규칙 체계를 포함하며, 각 규칙은 구체적인 코드 예제와 함께 '올바른/잘못된' 사용 방식을 제시합니다. Claude Code, Cursor, Codex와 같은 AI 에이전트와 호환되며, PR 리뷰 시 성능 관련 문제를 자동으로 진단하고 수정 제안을 제공하는 데 사용할 수 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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-Effect-event 통합, 안정적인 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> 경계를 데이터에 가깝게 배치하여 느린 하위 트리가 스트리밍되는 동안 페이지가 가능한 부분을 먼저 그리게 합니다. 단점: 콘텐츠가 도착할 때 레이아웃 이동이 발생합니다 — 공간(skeleton 또는 min-height)을 예약하세요.

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

// 잘못된 예 — 형제 await이 하나의 컴포넌트 내에서 순차적으로 실행됨
export default async function Page() {
  const user = await getUser();
  const cart = await getCart();
  return <View user={user} cart={cart} />;
}

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

2. 번들 크기 최적화 (중요)

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

배럴 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, // 클라이언트 전용인 경우
});

서드파티 스크립트 지연 로드

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

조건부 모듈 로딩

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 캐시

요청마다 변경되지 않는 데이터(구성, 조회 테이블)의 경우 LRU 캐시 또는 unstable_cache를 사용하여 React 외부에서 캐시하세요.

RSC 속성에서 중복 직렬화 방지

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

정적 I/O를 모듈 범위로 이동

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

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

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

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

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

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

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

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

스크롤을 위한 수동(passive) 리스너

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} />

Effect에서 원시 종속성 사용

// 잘못된 예 — 매 렌더링에서 새로운 객체 식별성
useEffect(() => {}, [{ id, name }]);

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

원시 값이 아닌 파생된 boolean 구독

// 잘못된 예 — 모든 카트 변경에 대해 리렌더링
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">는 트리 상태와 effect를 마운트된 상태로 유지하지만 숨깁니다 — 탭과 아코디언의 경우 언마운트/리마운트보다 저렴합니다.

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

// 잘못된 예 — `0`이 텍스트 노드로 렌더링됨
{count && <Badge>{count}</Badge>}

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

로딩 상태를 위한 useTransition

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

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬