frontend/javascript

🟨 2-39. 실전 성능 종합 튜닝 — React + Next.js 프로젝트 최적화 로드맵

mirabo01 2025. 11. 7. 09:01

좋아, 이제까지 흩어져 있던 성능 최적화 내용을
“실제 Next.js + React 프로젝트 하나를 처음부터 끝까지 튜닝한다” 는 관점에서 한 번에 정리해보자.

지금 보는 걸 그대로 “체크리스트”로 쓰면 된다.
신규 프로젝트든, 이미 운영 중인 서비스든 이 로드맵 그대로 점검하면 꽤 단단한 성능을 만들 수 있어.


1. 설계 단계(코드 쓰기 전)에서 할 일

성능은 코드를 쓰기 전에 이미 절반 정도 결정된다.

1) 렌더링 전략부터 고르기 (SSR / SSG / ISR / CSR)

Next.js 기준으로, 각 페이지마다 “어떻게 렌더링할지”를 먼저 설계하자.

  • SSG (정적 생성)
    • 자주 안 바뀌는 페이지: 블로그 글, 상품 상세, 설명 페이지
    • generateStaticParams, fetch (캐시 사용) 등
  • ISR (Incremental Static Regeneration)
    • 데이터는 바뀌지만 초 단위로 꼭 최신일 필요는 없을 때
    • 예: 상품 리스트, 인기글, 랭킹
    • revalidate 옵션으로 주기적 재생성
  • SSR
    • 사용자의 상태(로그인, 권한)에 따라 매번 다를 때
    • 예: 마이페이지, 관리자 대시보드 등
  • CSR
    • 클라이언트에서만 의미가 있는 페이지 (에디터, 대시보드 위젯 등)

처음부터 “모든 걸 SSR로 박는” 순간,
오리진 서버는 트래픽 올라갈 때마다 고통받게 된다.


2) 라우트/기능별 코드 분할 계획

  • “페이지 단위로 코드 스플리팅”을 기본 전략으로 깔고
  • 그 위에 “특히 무거운 컴포넌트”는 동적 import로 한 번 더 쪼갠다.

예:

  • /admin, /chart, /map 같은 페이지는 보통 무겁다 → 나중에 로드되게
  • WYSIWYG 에디터, 차트, 지도, 파일 업로더 같은 건 버튼 눌렀을 때만 로드

3) 상태 관리 구조 설계

전역 상태는 정말 필요한 최소한에만 쓰고,
나머지는 가능하면 페이지/컴포넌트 단위 로컬 상태로 둔다.

  • 전역 상태 라이브러리(예: Redux, Zustand)는
    • “여러 페이지에서 동시에 필요”한 데이터 위주로
  • React Query / SWR은
    • 서버 상태(백엔드 데이터) 캐싱 전담

이렇게 역할을 나누면 쓸데없는 리렌더링과 오버페칭을 크게 줄일 수 있다.


4) 이미지 · 폰트 · 아이콘 전략 미리 정하기

  • 이미지:
    • Next.js next/image 사용
    • WebP/AVIF 사용, Lazy Load 기본
  • 폰트:
    • next/font 또는 @font-face + font-display: swap
    • 필요한 폰트만, 굵기도 최소한으로
  • 아이콘:
    • 가능한 SVG 기반 (아이콘 폰트 X)
    • 자주 쓰는 아이콘 세트는 컴포넌트화

설계 단계에서 이 방향만 박아두면
나중에 “이미지, 폰트 때문에 LCP 터지는” 상황을 훨씬 덜 본다.


2. 개발 단계 — 컴포넌트/로직 성능 튜닝

1) 렌더링 비용 줄이기

  • React.memo
    • 부모 state 변경과 상관없는 컴포넌트에 적용
  • useCallback / useMemo
    • 정말 자주 렌더되는 컴포넌트 + 연산 무거운 부분에만 적용
  • 리스트 최적화
    • 긴 리스트 → react-window / react-virtualized
  • Context 남발 금지
    • 전역 Context 하나에 다 때려넣지 말고, 도메인별로 분리하거나 Zustand/Redux selector 사용

패턴:

“자주 바뀌는 것”과 “잘 안 바뀌는 것”을 최대한 분리한다.


2) 비동기 + UX: Suspense, Transition

  • 데이터 로딩 부분은 Suspense + Skeleton UI
  • 무거운 필터/검색/정렬은 useTransition 으로 감싸기

예:

const [isPending, startTransition] = useTransition();

const handleFilter = (value: string) => {
  startTransition(() => {
    setFilter(value);
  });
};

사용자는 “입력이 끊기지 않는다”는 느낌을 받게 된다.


3) API 통신 & 캐싱

  • React Query or SWR로
    • 동일한 API 호출은 한 번만 서버로 가게 하고
    • 캐시된 데이터는 즉시 사용 + 백그라운드에서 최신화(SWR 패턴)
const { data } = useQuery({
  queryKey: ["products", category],
  queryFn: fetchProducts,
  staleTime: 60 * 1000,
});
  • 자주 보는 데이터 → staleTime 길게
  • 민감한 데이터 → cacheTime/staleTime 짧게

이렇게 하면 트래픽이 몰려도 서버 부하가 덜하다.


3. 빌드/번들 단계 — Bundle 크기 줄이기

1) Bundle Analyzer로 구성 파악

Next.js라면:

npm install @next/bundle-analyzer

next.config.js

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
  • ANALYZE=true npm run build
  • 어느 페이지에서 어떤 라이브러리가 번들을 잡아먹는지 시각적으로 확인

보통 범인:

  • chart 라이브러리
  • date 라이브러리(moment)
  • 전체 lodash
  • 아이콘 패키지 전부 import

2) 가벼운 대체재로 교체

  • moment → dayjs, date-fns
  • lodash → lodash-es + 필요한 함수만 import
  • chart.js → 필요하면 dynamic import + SSR off
import debounce from "lodash-es/debounce";

3) Dynamic Import 적극 활용

Next.js 예:

const Editor = dynamic(() => import('@/components/Editor'), {
  ssr: false,
  loading: () => <p>에디터 로딩 중...</p>,
});
  • 관리자, 대시보드, 에디터, 차트처럼 “초기에 안 필요한 것들”은 전부 나중에 로드
  • 초기 진입 번들 사이즈를 1/2~1/3까지 줄일 수 있다.

4. 인프라/배포 단계 — CDN · 캐시 · 압축

1) CDN 사용은 사실상 필수

  • Next.js + Vercel, Cloudflare Pages, Netlify
    • 기본적으로 전 세계 엣지 서버에서 정적 리소스 제공
  • 정적 파일(JS, CSS, 이미지, 폰트)은 최대한 오리진에서 떼어낸다.

2) 압축

  • Brotli 활성화 (Vercel, Cloudflare는 기본 지원)
  • Nginx라면:
brotli on;
brotli_comp_level 6;
brotli_types text/html text/css application/javascript;

3) 캐시 헤더

  • 해시가 붙은 번들 파일:
    • Cache-Control: public, max-age=31536000, immutable
  • HTML/JSON/APIs:
    • 데이터 성격에 맞게 max-age, s-maxage, stale-while-revalidate 조합

5. 런타임 모니터링 — 배포 후 계속 지켜보기

1) Lighthouse CI

  • GitHub Actions / GitLab CI에 붙여서
    • main 브랜치에 푸시될 때마다 성능 점수 체크
    • 기준 점수 미만이면 빌드 실패 처리 가능

2) Web Vitals 수집

  • LCP, FID, CLS 값을 실제 사용자 기준으로 수집
  • Google Analytics, Sentry, 자체 API 등으로 전송
import { getLCP, getCLS, getFID } from "web-vitals";

getLCP(report);
getFID(report);
getCLS(report);

function report(metric) {
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}

3) Sentry / LogRocket / Datadog 등

  • JS 에러, 느린 페이지, 느린 API, rage click 같은 UX 문제까지 추적

이렇게 해 두면,
“개발할 땐 빨랐는데 어느 순간 느려짐” 같은 현상을 바로 잡을 수 있다.


6. 실제 프로젝트용 종합 체크리스트

아래는 Next.js + React 서비스 하나 기준의 성능 체크 가이드다.
티스토리 글 끝에 붙여도 딱 좋을 정도의 느낌으로 정리해보면 👇

🔹 설계 단계

  • 페이지별 SSR/SSG/ISR/CSR 전략 결정
  • 공통 레이아웃, 헤더/푸터 구조 설계
  • 상태 관리(전역 vs 로컬 vs 서버 상태) 구분
  • 이미지/폰트/CDN 전략 미리 정함

🔹 개발 단계

  • React.memo / useCallback / useMemo 필요한 곳에만 적용
  • 긴 리스트는 Virtualized List 사용
  • Suspense + Skeleton으로 로딩 UX 구성
  • React Query/SWR로 서버 상태 캐싱
  • Context는 도메인별로 쪼개고 구독 최소화

🔹 빌드 단계

  • Bundle Analyzer로 무거운 모듈 확인
  • dynamic import로 무거운 컴포넌트/페이지 분리
  • moment, lodash 등은 경량 대체재 사용
  • 불필요한 polyfill, legacy 코드 제거

🔹 배포/인프라 단계

  • CDN 활성화 (정적 파일 엣지에서 제공)
  • Brotli 압축 또는 Gzip 설정 확인
  • Cache-Control 헤더 적절히 설정
  • 이미지/폰트 Preload, Lazy Load 적용

🔹 운영/모니터링 단계

  • Lighthouse CI로 성능 점수 자동 점검
  • Web Vitals를 실제 사용자 기준으로 수집
  • Sentry/에러 추적 도구로 JS 에러, 느린 구간 모니터링
  • 정기적으로 번들/성능 리포트 확인

7. 마무리

이제까지 나눠서 이야기했던 모든 성능 최적화 주제를
“Next.js + React 서비스 하나” 기준으로 한 번에 꿰어본 거라 보면 된다.

정리하자면,

  1. 렌더링 전략을 먼저 고르고
  2. 컴포넌트/리렌더링을 줄이고
  3. 번들 크기를 줄이고
  4. CDN/캐시/압축으로 네트워크를 줄이고
  5. 모니터링으로 계속 지켜본다.

이 흐름만 몸에 익혀두면,
어떤 규모의 서비스든 “느려지면 어디부터 손대야 할지”가 자연스럽게 떠오르게 될 거야.