본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: 성능 최적화 전략과 베스트 프랙티스
2026년 2월 8일·웹 개발·

9장: 성능 최적화 전략과 베스트 프랙티스

React 19 애플리케이션의 성능을 극대화하는 전략을 다룹니다. 번들 최적화, 렌더링 성능, Core Web Vitals 개선, 측정 도구 활용법을 배웁���다.

15분824자7개 섹션
reactnextjsperformancefrontendtypescript
공유
react19-rsc9 / 11
1234567891011
이전8장: ref 개선, 메타데이터, 리소스 로딩 API다음10장: React 18에서 19로 마이그레이션

지금까지 React 19의 핵심 기능들을 하나씩 살펴보았습니다. 이번 장에서는 이 기능들을 성능이라는 관점에서 종합합니다. Server Components로 번들을 줄이고, Suspense로 로딩을 최적화하고, React Compiler로 렌더링을 개선하는 실전 전략을 다룹니다.

번들 크기 최적화

Server Components로 번들 줄이기

React 19 성능 최적화의 첫 번째 축은 클라이언트 번들 크기 감소입니다. Server Components에서 사용하는 라이브러리는 클라이언트에 전송되지 않습니다.

번들 크기 절감 전략
typescript
// Server Component: 이 라이브러리들은 클라이언트에 포함되지 않음
import { marked } from 'marked';           // ~35KB gzipped
import sanitizeHtml from 'sanitize-html';  // ~40KB gzipped
import { format } from 'date-fns';          // ~7KB gzipped
import Prism from 'prismjs';               // ~15KB gzipped
 
async function ArticlePage({ params }: { params: { id: string } }) {
  const article = await getArticle(params.id);
 
  const html = sanitizeHtml(marked(article.content));
  const formattedDate = format(article.createdAt, 'yyyy년 M월 d일');
  const highlightedCode = Prism.highlight(
    article.code,
    Prism.languages.typescript,
    'typescript'
  );
 
  return (
    <article>
      <time>{formattedDate}</time>
      <div dangerouslySetInnerHTML={{ __html: html }} />
      <pre dangerouslySetInnerHTML={{ __html: highlightedCode }} />
      {/* Client Component는 인터랙션이 필요한 부분만 */}
      <CommentSection articleId={article.id} />
    </article>
  );
}

이 구성에서 약 100KB(gzipped) 이상의 JavaScript가 클라이언트 번들에서 제거됩니다.

'use client' 경계 최적화

'use client' 경계의 위치가 번들 크기를 결정합니다.

경계 최적화 원칙
typescript
// 나쁜 예: 페이지 전체를 Client Component로
// → 모든 하위 컴포넌트와 의존성이 번들에 포함
'use client';
function ProductPage() { /* ... */ }
 
// 좋은 예: 인터랙션이 필요한 최소 단위만 Client Component로
// app/products/[id]/page.tsx (Server Component)
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  return (
    <div>
      {/* 정적 콘텐츠: Server Component */}
      <ProductInfo product={product} />
      <ProductSpecs specs={product.specs} />
 
      {/* 인터랙션 필요: Client Component */}
      <AddToCartButton productId={product.id} price={product.price} />
      <ImageCarousel images={product.images} />
    </div>
  );
}

코드 스플리팅

React.lazy와 Suspense를 활용한 동적 임포트로 초기 번들을 줄입니다.

코드 스플리팅
typescript
import { lazy, Suspense } from 'react';
 
// 무거운 컴포넌트는 동적 임포트
const RichTextEditor = lazy(() => import('@/components/RichTextEditor'));
const ChartLibrary = lazy(() => import('@/components/Chart'));
 
function Dashboard() {
  const [showEditor, setShowEditor] = useState(false);
 
  return (
    <div>
      <button onClick={() => setShowEditor(true)}>에디터 열기</button>
 
      {showEditor && (
        <Suspense fallback={<EditorSkeleton />}>
          <RichTextEditor />
        </Suspense>
      )}
 
      <Suspense fallback={<ChartSkeleton />}>
        <ChartLibrary data={chartData} />
      </Suspense>
    </div>
  );
}

렌더링 성능

데이터 페칭 워터폴 제거

Server Components에서 순차적 데이터 페칭(워터폴)을 방지하는 것이 렌더링 성능의 핵심입니다.

워터폴 안티패턴
typescript
// 나쁜 예: 순차 실행 (워터폴)
async function Dashboard() {
  const user = await getUser();           // 200ms
  const posts = await getPosts(user.id);  // 300ms (user 완료 후 시작)
  const stats = await getStats(user.id);  // 150ms (posts 완료 후 시작)
  // 총 650ms
 
  return <DashboardUI user={user} posts={posts} stats={stats} />;
}
병렬 페칭
typescript
// 좋은 예: 병렬 실행
async function Dashboard() {
  const user = await getUser();  // 200ms (이건 먼저 필요)
 
  // user에 의존하는 두 요청을 병렬로
  const [posts, stats] = await Promise.all([
    getPosts(user.id),   // 300ms
    getStats(user.id),   // 150ms (동시 시작)
  ]);
  // 총 500ms
 
  return <DashboardUI user={user} posts={posts} stats={stats} />;
}
스트리밍 패��
typescript
// 최적: 핵심 데이터만 블로킹, 나머지는 스트리밍
async function Dashboard() {
  const user = await getUser();  // 200ms (블로킹)
 
  // Promise를 생성하되 await하지 않음
  const postsPromise = getPosts(user.id);
  const statsPromise = getStats(user.id);
 
  return (
    <div>
      <UserHeader user={user} />
 
      <Suspense fallback={<PostsSkeleton />}>
        <PostList postsPromise={postsPromise} />
      </Suspense>
 
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel statsPromise={statsPromise} />
      </Suspense>
    </div>
  );
  // 사용자는 200ms 후 헤더를 보고,
  // 나머지는 준비되는 대로 스트리밍
}

React.cache로 중복 요청 제거

요청 중복 제거
typescript
import { cache } from 'react';
 
// cache()로 감싸면 같은 인자로 호출된 결과가 캐싱됨
const getUser = cache(async (userId: string) => {
  return db.users.findUnique({ where: { id: userId } });
});
 
// Header와 Sidebar가 같은 user를 필요로 해도 쿼리는 1회만 실행
async function Header() {
  const user = await getUser('user-1');
  return <nav>{user.name}</nav>;
}
 
async function Sidebar() {
  const user = await getUser('user-1');
  return <aside>{user.bio}</aside>;
}
 
async function Page() {
  return (
    <>
      <Header />
      <Sidebar />
    </>
  );
}

cache의 스코프는 단일 요청(request)입니다. 서로 다른 사용자의 요청 간에는 공유되지 않으므로 보안 문제가 없습니다.

Transition으로 UI 반응성 유지

무거운 상태 업데이트는 useTransition으로 감싸서 UI 반응성을 유지합니다.

Transition 활용
typescript
'use client';
 
import { useState, useTransition } from 'react';
 
function SearchableList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();
 
  function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
    const value = e.target.value;
    setQuery(value);  // 즉시 업데이트 (입력 반응)
 
    startTransition(() => {
      // 비용이 큰 필터링은 낮은 우선순위로
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase()) ||
        item.description.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  }
 
  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="검색..." />
      <div className={isPending ? 'opacity-60' : ''}>
        {filteredItems.map(item => (
          <ItemCard key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
}

startTransition으로 감싼 상태 업데이트는 중단 가능(interruptible)합니다. 사용자가 추가 입력을 하면 이전 필터링이 중단되고 새로운 필터링이 시작됩니다.

Core Web Vitals 최적화

LCP (Largest Contentful Paint)

LCP를 개선하려면 주요 콘텐츠를 최대한 빠르게 표시해야 합니다.

LCP 최적화
typescript
import { preload } from 'react-dom';
 
async function ArticlePage({ params }: { params: { id: string } }) {
  const article = await getArticle(params.id);
 
  // 히어로 이미지 프리로드
  preload(article.heroImage, { as: 'image' });
 
  return (
    <article>
      {/* LCP 요소: 히어로 이미지 */}
      <img
        src={article.heroImage}
        alt={article.title}
        width={1200}
        height={630}
        fetchPriority="high"  // 높은 우선순위로 로드
      />
 
      <h1>{article.title}</h1>
      <p>{article.content}</p>
 
      {/* 부가 콘텐츠는 스트리밍 */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedArticles />
      </Suspense>
    </article>
  );
}

FID/INP (First Input Delay / Interaction to Next Paint)

인터랙션 성능을 개선하려면 하이드레이션 비용을 줄여야 합니다.

INP 최적화
typescript
// 1. Server Components로 하이드레이션 대상 줄이기
async function Page() {
  return (
    <div>
      {/* 하이드레이션 불필요: Server Component */}
      <StaticContent />
 
      {/* 하이드레이션 필요: 최소한의 Client Component */}
      <Suspense>
        <InteractiveWidget />
      </Suspense>
    </div>
  );
}
 
// 2. 이벤트 핸들러에서 무거운 작업은 transition으로
'use client';
function SearchButton() {
  const [isPending, startTransition] = useTransition();
 
  return (
    <button onClick={() => {
      startTransition(() => {
        // 무거운 상태 업데이트
        performSearch();
      });
    }}>
      검색
    </button>
  );
}

CLS (Cumulative Layout Shift)

레이아웃 이동을 방지하려면 Suspense fallback의 크기를 실제 콘텐츠와 맞춰야 합니다.

CLS 방지
typescript
// 나쁜 예: fallback과 실제 콘텐츠의 크기 불일치
<Suspense fallback={<p>로딩 중...</p>}>  {/* 높이: ~20px */}
  <ProductGrid />                         {/* 높이: ~600px */}
</Suspense>
 
// 좋은 예: fallback이 실제 콘텐츠의 공간을 확보
<Suspense fallback={<ProductGridSkeleton />}>  {/* 높이: ~600px */}
  <ProductGrid />                               {/* 높이: ~600px */}
</Suspense>
Skeleton 크기 매칭
typescript
function ProductGridSkeleton() {
  return (
    <div className="grid grid-cols-3 gap-4" style={{ minHeight: '600px' }}>
      {Array.from({ length: 6 }).map((_, i) => (
        <div key={i} className="animate-pulse">
          <div className="aspect-square bg-gray-200 rounded" />
          <div className="mt-2 h-4 bg-gray-200 rounded w-3/4" />
          <div className="mt-1 h-4 bg-gray-200 rounded w-1/2" />
        </div>
      ))}
    </div>
  );
}

캐싱 전략

서버 컴포넌트 캐싱

Next.js 캐싱 패턴
typescript
// 정적 데이터: 빌드 타임에 캐싱
async function StaticPage() {
  const data = await fetch('https://api.example.com/static', {
    cache: 'force-cache',  // 빌드 타임에 캐싱
  });
  return <div>{data.title}</div>;
}
 
// 주기적 갱신: ISR(Incremental Static Regeneration)
async function ISRPage() {
  const data = await fetch('https://api.example.com/articles', {
    next: { revalidate: 3600 },  // 1시간마다 갱신
  });
  return <ArticleList articles={data} />;
}
 
// 동적 데이터: 매 요청마다 새로 가져옴
async function DynamicPage() {
  const data = await fetch('https://api.example.com/live', {
    cache: 'no-store',
  });
  return <LiveDashboard data={data} />;
}

클라이언트 캐싱

Router Cache 활용
typescript
// Next.js는 Client-side Navigation 시
// 방문한 페이지의 RSC Payload를 캐싱합니다.
// 뒤로 가기/앞으로 가기 시 네트워크 요청 없이 즉시 표시.
 
// prefetch로 다음 페이지를 미리 캐싱
import Link from 'next/link';
 
function ArticleList({ articles }: { articles: Article[] }) {
  return (
    <ul>
      {articles.map(article => (
        <li key={article.id}>
          {/* hover 시 자동 prefetch */}
          <Link href={`/articles/${article.slug}`} prefetch>
            {article.title}
          </Link>
        </li>
      ))}
    </ul>
  );
}

성능 측정

React Profiler

Profiler 활용
typescript
import { Profiler } from 'react';
 
function onRender(
  id: string,
  phase: 'mount' | 'update' | 'nested-update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number,
) {
  // 성능 모니터링 서비스에 전송
  if (actualDuration > 16) {  // 60fps 기준 초과
    reportSlowRender({ id, phase, actualDuration, baseDuration });
  }
}
 
function App() {
  return (
    <Profiler id="app" onRender={onRender}>
      <Dashboard />
    </Profiler>
  );
}

Chrome DevTools Performance

React 19.2에서 추가된 Performance Tracks를 활용하면 Chrome DevTools의 Performance 탭에서 React 전용 정보를 확인할 수 있습니다.

  • Scheduler Priority: 각 업데이트의 우선순위
  • Component Render Time: 개별 컴포넌트의 렌더링 시간
  • Suspense Events: Suspense 경계의 suspend/reveal 타이밍

성능 체크리스트

영역체크 항목
번들Server Components에서만 사용하는 라이브러리가 클라이언트 번들에 포함되지 않는가?
번들'use client' 경계가 트리의 최하위에 위치하는가?
번들무거운 컴포넌트에 코드 스플리팅이 적용되었는가?
렌더링데이터 페칭이 병렬로 실행되는가? (워터폴 없음)
렌더링불필요한 데이터 재요청이 cache()로 방지되었는가?
렌더링무거운 상태 업데이트가 startTransition으로 감싸져 있는가?
Suspense경계가 적절히 세분화되어 있는가?
Suspenseskeleton 크기가 실제 콘텐츠와 일치하는가? (CLS 방지)
리소스핵심 리소스에 preload가 적용되었는가?
리소스이미지에 적절한 width/height와 fetchPriority가 설정되었는가?
캐싱정적/동적 데이터의 캐싱 전략이 적절한가?
CompilerReact Compiler가 활성화되어 있는가?

핵심 요약

  • Server Components로 클라이언트 번들에서 서버 전용 라이브러리를 제거하고, 'use client' 경계를 트리 하위에 배치합니다.
  • 데이터 페칭 워터폴을 Promise.all과 스트리밍 패턴으로 제거합니다.
  • React.cache로 동일 요청 내 중복 데이터 호출을 방지합니다.
  • useTransition으로 무거운 상태 업데이트를 낮은 우선순위로 처리하여 UI 반응성을 유지합니다.
  • Suspense fallback의 크기를 실제 콘텐츠와 맞춰 CLS를 방지합니다.
  • preload, preinit API로 핵심 리소스의 로딩을 앞당깁니다.
  • React Compiler를 활성화하여 자동 메모이제이션으로 렌더링 성능을 개선합니다.

다음 장에서는 React 18에서 19로의 마이그레이션 전략과 주의사항을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#react#nextjs#performance#frontend#typescript

관련 글

웹 개발

10장: React 18에서 19로 마이그레이션

React 18에서 19로 안전하게 업그레이드하는 단계별 가이드입니다. 제거된 API, 타입 변경, 동작 변화, 자동 마이그레이션 도구를 다룹니다.

2026년 2월 10일·14분
웹 개발

8장: ref 개선, 메타데이터, 리소스 로딩 API

React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.

2026년 2월 6일·16분
웹 개발

11장: 실전 프로젝트 - React 19 풀스택 앱 구축

React 19의 핵심 기능을 모두 활용한 풀스택 북마크 앱을 구축합니다. Server Components, Server Actions, 새로운 훅, Suspense 패턴을 실전에 적용합니다.

2026년 2월 12일·16분
이전 글8장: ref 개선, 메타데이터, 리소스 로딩 API
다음 글10장: React 18에서 19로 마이그레이션

댓글

목차

약 15분 남음
  • 번들 크기 최적화
    • Server Components로 번들 줄이기
    • 'use client' 경계 최적화
    • 코드 스플리팅
  • 렌더링 성능
    • 데이터 페칭 워터폴 제거
    • React.cache로 중복 요청 제거
    • Transition으로 UI 반응성 유지
  • Core Web Vitals 최적화
    • LCP (Largest Contentful Paint)
    • FID/INP (First Input Delay / Interaction to Next Paint)
    • CLS (Cumulative Layout Shift)
  • 캐싱 전략
    • 서버 컴포넌트 캐싱
    • 클라이언트 캐싱
  • 성능 측정
    • React Profiler
    • Chrome DevTools Performance
  • 성능 체크리스트
  • 핵심 요약