Skip to content

성능 개선 실전 팁

코드 스플릿팅과 Lazy Loading, 불필요한 리렌더링 방지, 번들 크기 분석, 이미지 최적화, 그리고 마이크로프론트엔드 특화 최적화(shared 라이브러리, 프리로드)를 포함한 실전 성능 개선 기법을 학습한다.

학습 목표

  • React.lazy와 Suspense를 활용한 코드 스플릿팅을 구현할 수 있다
  • webpack-bundle-analyzer로 번들 크기를 분석하고 최적화 대상을 식별할 수 있다
  • 이미지 최적화 기법(WebP, lazy loading, srcset)을 적용할 수 있다
  • 마이크로프론트엔드 환경에서 shared 라이브러리와 프리로드 전략을 구현할 수 있다
  • 성능 개선의 핵심 원칙인 "지금 필요 없는 일은 하지 않는다"를 실무에 적용할 수 있다

1. 성능 개선의 핵심 원칙

성능을 개선할 때 가장 중요한 원칙은 단순하다.

"지금 하지 않아도 되는 일을 하지 않는 것"

이 원칙은 두 가지 관점에서 적용된다.

1.1 로딩 시점 최적화

  • 초기 로딩 시 당장 사용하지 않는 코드를 제외한다 (Lazy Loading)
  • 마이크로앱 A에서 B로 전환할 때 불필요한 코드가 딸려 들어가지 않게 한다 (코드 스플릿팅)
  • API 호출을 병렬화하여 대기 시간을 줄인다

1.2 렌더링 시점 최적화

  • 변경되지 않은 컴포넌트를 다시 렌더링하지 않는다 (React.memo)
  • 동일한 계산을 반복하지 않는다 (useMemo)
  • 동일한 함수를 매번 새로 생성하지 않는다 (useCallback)

2. 코드 스플릿팅과 Lazy Loading

2.1 React.lazy + Suspense

React.lazy를 사용하면 컴포넌트를 동적으로 import하여, 해당 컴포넌트가 실제로 렌더링될 때만 코드를 로드한다.

jsx
import React, { Suspense, lazy } from "react";

// 적용 전: 초기 번들에 모든 페이지가 포함됨
// import Dashboard from "./pages/Dashboard";
// import Settings from "./pages/Settings";
// import Analytics from "./pages/Analytics";

// 적용 후: 각 페이지가 별도 청크로 분리됨
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const Analytics = lazy(() => import("./pages/Analytics"));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

2.2 라우트 기반 코드 스플릿팅

가장 효과적인 코드 스플릿팅 전략은 라우트(페이지) 단위로 분할하는 것이다. 사용자가 현재 보고 있는 페이지의 코드만 로드하고, 다른 페이지의 코드는 해당 페이지로 이동할 때 로드한다.

2.3 컴포넌트 수준 Lazy Loading

페이지 내에서도 무거운 컴포넌트를 분리할 수 있다. 예를 들어 차트 라이브러리처럼 용량이 큰 컴포넌트는 사용자가 "차트 보기" 버튼을 클릭한 시점에 lazy(() => import("./HeavyChart"))로 로드하면 초기 번들 크기를 줄일 수 있다.

2.4 Intersection Observer 활용

뷰포트에 진입할 때만 리소스를 로드하는 패턴이다. IntersectionObserver를 사용하여 컴포넌트가 화면에 보이기 직전(rootMargin 활용)에 로드를 트리거하면, 스크롤 아래에 위치한 무거운 섹션의 초기 로딩 비용을 제거할 수 있다.


3. 불필요한 리렌더링 방지

3.1 진단: DevTools 렌더링 하이라이트

React DevTools에서 "Highlight updates when components render"를 활성화하고, 특정 상호작용 시 변경되지 않아야 할 영역에 테두리가 나타나는지 확인한다.

3.2 뷰와 비즈니스 로직 분리 (State Colocation)

렌더링이 자주 변하는 영역과 변하지 않는 영역을 컴포넌트로 분리한다. 예를 들어 타이머 state를 Page 컴포넌트에 두면 매초 전체가 리렌더링되지만, ClockContainer라는 별도 컴포넌트로 분리하면 타이머와 무관한 ExpensiveList는 리렌더링되지 않는다.

3.3 리스트 가상화 (Windowing)

수천 개의 항목이 있는 리스트에서는 화면에 보이는 항목만 렌더링하는 가상화 기법이 필수적이다. react-windowreact-virtuoso 같은 라이브러리를 사용하면 DOM에 실제로 존재하는 요소 수를 뷰포트 크기만큼으로 제한할 수 있다.


4. 번들 크기 분석

4.1 webpack-bundle-analyzer

번들에 포함된 모듈의 크기를 트리맵으로 시각화하여, 어떤 라이브러리가 번들 크기를 차지하는지 한눈에 파악할 수 있다.

javascript
// webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "static",
      reportFilename: "bundle-report.html"
    })
  ]
};

4.2 분석 시 확인 항목

확인 항목조치 방법
사용하지 않는 라이브러리제거 또는 더 가벼운 대안으로 교체
트리 셰이킹이 되지 않는 모듈ES Module 버전 사용 또는 부분 import
중복 포함된 라이브러리Module Federation shared 또는 externals 설정
과도하게 큰 단일 라이브러리동적 import로 코드 스플릿팅
locale/moment 등 불필요한 데이터IgnorePlugin 또는 ContextReplacementPlugin

4.3 번들 크기 예산 설정

webpack의 performance.maxEntrypointSize(예: 250KB)와 performance.hints: "error" 설정으로 번들 크기 예산을 초과하면 빌드를 실패시킬 수 있다. 성능 예산을 설정하면 무심코 큰 라이브러리를 추가하는 것을 방지한다.


5. 이미지 최적화

이미지는 웹페이지 전체 용량의 상당 부분을 차지하므로, 이미지 최적화만으로도 큰 성능 개선 효과를 얻을 수 있다.

5.1 최신 이미지 포맷 사용

포맷특징적합한 용도
WebPJPEG 대비 25-35% 더 작음, 대부분의 브라우저 지원일반 이미지
AVIFWebP보다 20% 더 작음, 지원 브라우저 증가 중고품질 이미지
SVG벡터 형식, 크기 불변, 아이콘에 최적아이콘, 로고

<picture> 요소로 AVIF > WebP > JPEG 순서의 폴백을 제공하면 최신 브라우저에서 최적의 포맷을 자동으로 선택한다.

5.2 반응형 이미지

srcsetsizes 속성으로 디바이스 크기에 맞는 이미지를 로드한다. 모바일에서 데스크톱용 이미지를 다운로드하는 낭비를 방지할 수 있다.

5.3 이미지 Lazy Loading과 크기 명시

  • loading="lazy" 속성으로 뷰포트 밖 이미지의 로딩을 지연한다
  • LCP 대상이 되는 히어로 이미지에는 fetchpriority="high"를 적용하고 lazy를 사용하지 않는다
  • width/height 또는 CSS aspect-ratio를 반드시 명시하여 CLS를 방지한다

6. 마이크로프론트엔드 특화 최적화

6.1 shared 라이브러리 설정

Module Federation의 shared 설정으로 공통 라이브러리의 중복 로딩을 방지한다.

javascript
// webpack.config.js (각 마이크로앱에 동일하게 설정)
new ModuleFederationPlugin({
  name: "posting",
  shared: {
    react: {
      singleton: true,          // 단일 인스턴스 강제
      requiredVersion: "^18.0.0",
      eager: false              // 필요 시점에 로드
    },
    "react-dom": {
      singleton: true,
      requiredVersion: "^18.0.0"
    },
    "react-router-dom": {
      singleton: true,
      requiredVersion: "^6.0.0"
    }
  }
});

6.2 프리로드 / 프리페치 전략

마이크로앱의 리소스를 사전에 로드하여 전환 시 지연을 줄인다.

html
<!-- 현재 페이지에 곧 필요한 리소스: preload -->
<link rel="preload" href="/micro-app-b/remoteEntry.js" as="script" />

<!-- 다음 페이지에 필요할 수 있는 리소스: prefetch -->
<link rel="prefetch" href="/micro-app-c/remoteEntry.js" />

프로그래밍 방식으로도 프리페치를 구현할 수 있다. 예를 들어 네비게이션 링크에 onMouseEnter 이벤트를 걸어, 사용자가 메뉴에 마우스를 올릴 때 해당 마이크로앱의 remoteEntry.js를 프리페치하면 클릭 시 즉시 로드할 수 있다.

6.3 스켈레톤 UI로 CLS 방지

마이크로앱이 비동기로 로드되는 동안 스켈레톤 UI를 표시하여 레이아웃 이동을 방지한다.

마이크로앱 컨테이너에 minHeight를 설정하고, Suspensefallback으로 스켈레톤 UI를 표시하며, ErrorBoundary로 감싸서 장애를 격리한다. 이 세 가지를 결합하면 CLS 방지, 로딩 UX 개선, 장애 격리를 동시에 달성할 수 있다.

6.4 마이크로앱 간 리소스 공유 최적화


7. 성능 개선 체크리스트

카테고리체크 항목
로딩라우트 기반 코드 스플릿팅 적용 여부
로딩불필요한 라이브러리가 번들에 포함되지 않는지
로딩이미지 최적화(WebP/AVIF, lazy, fetchpriority) 적용 여부
렌더링불필요한 리렌더링 발생 여부 (DevTools 하이라이트 확인)
렌더링긴 리스트에 가상화(windowing) 적용 여부
MFEModule Federation shared로 라이브러리 중복 방지 여부
MFE마이크로앱 컨테이너 minHeight 설정으로 CLS 방지 여부
MFEError Boundary로 장애 격리 여부

핵심 정리

  1. "지금 필요 없는 일은 하지 않는다"가 핵심 원칙이다: 사용하지 않는 코드 제거, 불필요한 리렌더링 방지, 적시 로딩이 성능 개선의 기본이다
  2. 코드 스플릿팅은 가장 효과적인 초기 로딩 최적화다: React.lazy와 Suspense로 라우트/컴포넌트 단위 분할을 구현한다
  3. 번들 분석은 최적화의 출발점이다: webpack-bundle-analyzer로 번들을 시각화하면 가장 큰 최적화 기회를 빠르게 발견할 수 있다
  4. 이미지 최적화만으로도 큰 효과를 얻을 수 있다: WebP/AVIF 포맷, lazy loading, 반응형 이미지, 크기 명시가 기본이다
  5. 마이크로프론트엔드는 shared 설정과 프리로드가 핵심이다: 라이브러리 중복 방지, 리소스 선제 로딩, 스켈레톤 UI, 장애 격리를 반드시 구현해야 한다
  6. 성능 최적화는 반복 프로세스다: 측정 -> 분석 -> 최적화 -> 검증 사이클을 CI/CD에 통합하여 지속적으로 관리한다

다음 단계