frontend/javascript

🟨 2-41. 프론트엔드 성능 아키텍처 완성 — ‘빠름’을 구조로 설계하기

mirabo01 2025. 11. 7. 09:02

좋아. 이제 이번 편은 시리즈의 마지막을 장식할 **‘프론트엔드 성능 아키텍처 총정리’**야.
그동안 다뤄온 코드, 렌더링, 시각적 튜닝, 네트워크, 모니터링을
하나의 완성된 시스템 관점에서 엮어보자.

이건 단순히 “어떻게 빠르게 만들까?”가 아니라,
“빠른 상태를 지속적으로 유지할 수 있는 구조는 어떻게 설계할까?” 에 초점을 둔다.


1. 성능은 ‘기술’이 아니라 ‘구조’다

대부분의 프로젝트가 초기에 빠르다가
운영 몇 달 지나면 점점 무거워지는 이유는 명확하다.
성능을 “한 번 튜닝하는 일회성 작업” 으로 보기 때문이다.

하지만 진짜 중요한 건,

성능이 유지되도록 구조를 설계하는 것.

이건 “잘 만든 코드”가 아니라 “잘 만들어진 시스템”에서 온다.


2. 전체 구조를 4단계로 나누어 설계하라

렌더링 계층

→ React / Next.js가 실제로 그리는 UI

데이터 계층

→ API 호출, 캐싱, 상태관리 구조

자원 계층

→ 이미지, 폰트, CSS, JS 번들, CDN

관찰 계층

→ 모니터링, 로깅, 사용자 체감 성능 데이터

이 4단계를 각자 독립적으로 설계하면
성능 문제가 생겨도 “어느 계층에서 터졌는지” 즉시 구분할 수 있다.


3. 렌더링 계층 — 최소한만 다시 그리는 구조

핵심 원칙

  1. React.memo 로 자식 리렌더 차단
  2. useCallback / useMemo 로 참조 고정
  3. Suspense / Lazy Loading 으로 페이지 단위 코드 분리
  4. Virtualized Rendering 으로 긴 리스트 최소화

실제 구조 예시

<App>
 ┣ <Header />           // memoized
 ┣ <Sidebar />          // memoized
 ┣ <Suspense fallback={<Loading />}> 
 ┃   <MainRouter />     // dynamic import된 페이지 단위 컴포넌트
 ┗ </Suspense>
</App>

→ 구조적으로 “필요한 부분만 리렌더” 하도록 설계되어 있으면
코드가 늘어나도 체감 속도는 변하지 않는다.


4. 데이터 계층 — 서버 상태와 클라이언트 상태의 분리

성능이 무너지는 가장 흔한 이유 중 하나가
“모든 데이터를 전역 상태로 관리”하기 때문이다.

원칙

구분 특징 적합한 관리 방식

서버 상태 외부 API에서 오는 데이터 React Query / SWR
클라이언트 상태 UI 조작, 필터링, 입력값 등 useState / useReducer
전역 상태 여러 페이지에서 공용으로 쓰는 데이터 Zustand / Redux (selector 기반 구독)

즉,

“모든 상태를 한곳에 몰지 말고, 데이터의 ‘출처’에 따라 관리 구조를 나눠라.”

이렇게만 해도 불필요한 리렌더링과 API 중복 호출이 확 줄어든다.


5. 자원 계층 — 브라우저가 덜 일하게 만드는 설계

성능의 절반은 브라우저가 얼마나 효율적으로 리소스를 받느냐에 달려 있다.

1️⃣ 번들 관리

  • 페이지별 코드 스플리팅
  • Dynamic Import로 무거운 라이브러리 지연 로드
  • Tree Shaking 활성화
  • moment → dayjs, lodash → lodash-es

2️⃣ 이미지 최적화

  • WebP/AVIF 사용
  • loading="lazy"
  • next/image 활용

3️⃣ 폰트 관리

  • next/font 사용 or font-display: swap
  • 필요 굵기만 subset 추출

4️⃣ 캐시 정책

  • 정적 리소스: max-age=31536000, immutable
  • HTML/API: stale-while-revalidate

이 계층을 잘 세팅하면
브라우저는 “이미 받은 건 다시 안 받는” 구조로 돌아가며
체감 속도는 2배 이상 빨라진다.


6. 관찰 계층 — ‘지속적인 성능’의 핵심

아무리 잘 만들어도, 측정하지 않으면 무너진다.

6-1. Lighthouse CI

  • GitHub Actions에 붙여서 main 브랜치마다 성능 측정
  • LCP, CLS, TBT 점수가 기준 이하일 경우 빌드 실패

6-2. Web Vitals 수집

import { getLCP, getFID, getCLS } from "web-vitals";

function report(metric) {
  fetch("/api/vitals", {
    method: "POST",
    body: JSON.stringify(metric),
    keepalive: true,
  });
}

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

→ 실제 사용자 기준으로 Core Web Vitals 데이터를 백엔드로 전송
→ 서버에서 쌓인 데이터로 “실제 사용자가 느끼는 성능”을 확인

6-3. 에러 & UX 모니터링

  • Sentry: JS 에러 및 느린 렌더링 구간 추적
  • LogRocket: 사용자 세션 리플레이로 UX 확인
  • Datadog: API 응답 지연 및 브라우저 메트릭 분석

이 계층이 없으면 성능은 결국 “운에 맡긴 품질” 이 된다.


7. 실제 아키텍처 예시

[User] 
   │
   ▼
[CDN Edge] — Static Assets (이미지, 폰트, 번들)
   │
   ▼
[Next.js Server]
   ┣ SSG: 캐시된 HTML 제공
   ┣ ISR: 주기적 재생성
   ┗ API Proxy → 백엔드
   │
   ▼
[React 클라이언트]
   ┣ Suspense + Lazy 로 분할 로드
   ┣ React Query 캐시로 서버 상태 관리
   ┣ Virtualized List + memoized component
   ┗ Web Vitals → Monitoring 서버

→ 구조 전체가 “덜 렌더링, 덜 다운로드, 덜 재계산”을 중심으로 돌아간다.


8. 성능 유지 루틴 (운영단계 체크리스트)

주기 점검 항목 도구

매 배포 Lighthouse / Bundle Analyzer GitHub Actions
매주 Web Vitals 평균 점검 (LCP/FID/CLS) 자체 대시보드
매월 번들 크기 및 이미지 용량 추세 Source-map Explorer
분기 코드 리팩토링 / 사용 안 하는 패키지 제거 npm-check, depcheck

유지되는 성능은 “정기 점검 루틴”에서 나온다.


9. 완성된 프론트엔드 성능 아키텍처의 핵심 문장

  • 렌더링 계층: 필요한 것만 그린다.
  • 데이터 계층: 필요한 순간에만 가져온다.
  • 자원 계층: 한 번 받은 건 다시 받지 않는다.
  • 관찰 계층: 느려지면 바로 알 수 있다.

이 네 문장만 머릿속에 넣고 프로젝트를 설계하면
어떤 기술 스택을 쓰더라도 성능 문제는 구조적으로 막을 수 있다.