안동민 개발노트

안동민 개발노트

일반적인 오류 및 해결 방법성능 병목 현상 식별 및 해결브라우저 개발자 도구 활용서버 사이드 로깅 및 모니터링
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 18장 : 문제 해결 및 디버깅
  5. 성능 병목 현상 식별 및 해결
  1. Next.js
  2. 성능 병목 현상 식별 및 해결

성능 병목 현상 식별 및 해결

Lighthouse·DevTools·Speed Insights·번들 분석기로 병목을 측정하고 이미지·폰트·스크립트·데이터를 개선합니다.

애플리케이션의 기능이 정상적으로 작동하는 것만큼이나 중요한 것은 바로 성능입니다.

사용자는 빠르고 반응성이 좋은 웹사이트를 선호하며, 느린 웹사이트는 사용자 이탈로 이어질 수 있습니다.

Next.js는 기본적으로 뛰어난 성능을 제공하지만, 복잡한 로직, 과도한 데이터 페칭, 최적화되지 않은 이미지 등으로 인해 성능 병목 현상이 발생할 수 있습니다.

이 절에서는 Next.js 애플리케이션에서 성능 병목 현상을 식별하는 방법과 이를 해결하기 위한 구체적인 전략들을 알아보겠습니다.


성능 병목 현상이란?

성능 병목 현상(Performance Bottleneck)이란 애플리케이션의 전반적인 성능을 저해하는 특정 부분 또는 리소스를 의미합니다.

마치 병의 목처럼, 데이터나 처리 흐름이 특정 지점에서 막혀 전체 시스템의 속도를 늦추는 현상입니다.

웹 애플리케이션에서는 다음과 같은 영역에서 병목 현상이 주로 발생할 수 있습니다.

  • 네트워크 요청: 과도한 API 호출, 큰 이미지/비디오 파일, 최적화되지 않은 CSS/JavaScript 번들.
  • 렌더링 성능: 복잡한 UI, 불필요한 리렌더링, 잘못된 CSS 사용.
  • 서버 응답 시간: 비효율적인 데이터베이스 쿼리, 복잡한 서버 측 로직.
  • 클라이언트 측 JavaScript 실행: 긴 스크립트 실행 시간, 블로킹 작업.

성능 병목 현상 식별 도구

문제를 해결하기 위해서는 먼저 문제가 어디에서 발생하는지 정확히 파악해야 합니다.

웹 성능 분석에 사용되는 주요 도구들은 다음과 같습니다.

Lighthouse 성능 기준 측정

  • 용도: 성능, 접근성, 권장사항(Best Practices), SEO 카테고리와 LCP·CLS·TBT 같은 실험실 지표를 측정합니다. TBT는 로딩 중 메인 스레드의 차단량을 나타내며, 실제 상호작용의 INP를 대신하지 않습니다. Lighthouse 12부터 PWA 카테고리는 제거되었으므로 설치 가능성, manifest, Service Worker와 오프라인 동작은 별도 점검표와 DevTools Application 패널에서 확인합니다.
  • 활용: 개발자 도구에서 직접 실행하거나, PageSpeed Insights 웹사이트에서 URL을 입력하여 사용합니다. Lighthouse의 진단과 절감 예상치는 실험실 실행 조건에서 얻은 개선 후보이며, 실제 사용자의 효과를 보장하지 않습니다.

Chrome DevTools

Chrome 개발자 도구는 실시간으로 웹 애플리케이션의 성능을 분석하는 데 쓰는 기본 도구입니다.

  • Network 탭
    • 활용: 페이지 로딩 시 발생하는 모든 네트워크 요청(HTML, CSS, JS, 이미지, API 호출 등)을 시각화합니다. 각 리소스의 크기, 로딩 시간, 캐싱 여부 등을 확인하여 불필요하게 큰 파일이나 느린 요청을 식별할 수 있습니다.
    • 팁: Disable cache로 DevTools가 열린 동안 브라우저 HTTP 캐시 재사용을 끄고, Throttling을 사용하여 느린 네트워크 조건을 재현합니다. 이 설정만으로 Service Worker·CDN·서버 캐시까지 비워지지는 않습니다.
  • Performance 탭
    • 활용: 페이지의 런타임 성능(스크롤, 클릭 등 사용자 인터랙션 시)을 기록하고 분석합니다. JavaScript 실행 시간, 렌더링 시간, 레이아웃 재계산 시간 등을 그래프로 보여주며, 긴 작업을 수행하는 함수나 불필요한 리렌더링을 찾아낼 수 있습니다.
    • 팁: Start profiling and reload page를 사용하여 페이지 로딩부터의 전체 과정을 기록하고 분석합니다.
  • Memory 탭
    • 활용: 애플리케이션의 메모리 사용량을 분석하여 메모리 누수나 과도한 메모리 사용을 식별합니다.

Vercel Speed Insights

  • 용도: Vercel에 배포된 Next.js 애플리케이션에 대해 실제 사용자 데이터를 기반으로 성능 지표(Core Web Vitals 등)를 수집하고 시각화합니다.
  • 활용: 개발 환경 테스트를 넘어 실제 사용자 환경의 성능을 수집해 문제 해결 우선순위를 정합니다. Vercel 대시보드에서 기능을 활성화하고 @vercel/speed-insights를 설치한 뒤 Root Layout에 <SpeedInsights />를 렌더링해야 합니다.

번들 분석기

  • 용도: 빌드에 포함된 패키지와 모듈의 크기·의존 관계를 분석합니다. @next/bundle-analyzer는 Webpack용 플러그인입니다. Turbopack에서는 Next.js 16.1 이상이 제공하는 실험적 번들 분석기처럼 해당 버전이 지원하는 도구를 확인합니다.
  • 활용: 불필요하게 큰 라이브러리를 제거하거나, 동적 임포트(next/dynamic)를 적용할 대상을 식별하는 데 유용합니다.

성능 자료의 관측 범위

실험실 지표, 실사용 지표, 실행 기록, 번들 크기가 답하는 질문을 구분한다.

성능 자료의 관측 범위
자료확인할 수 있는 것단정할 수 없는 것
실험실 실행정해진 기기·네트워크 조건의 LCP·CLS와 로딩 중 TBT그 점수가 모든 사용자의 경험을 대표하지는 않습니다.
실사용 수집실제 방문에서 수집된 LCP·INP·CLS의 분포표본·기간·기기 구성이 다르면 단순 전후 비교가 어렵습니다.
실행 기록특정 재현 구간의 긴 작업·레이아웃·이벤트 처리한 번의 trace만으로 전체 사용자 원인을 확정하지 않습니다.
번들 분석포함된 모듈의 크기와 import 관계파일 크기만으로 실제 실행 시간이나 사용자 지연을 알 수 없습니다.
실험실 실행
확인할 수 있는 것: 정해진 기기·네트워크 조건의 LCP·CLS와 로딩 중 TBT
단정할 수 없는 것: 그 점수가 모든 사용자의 경험을 대표하지는 않습니다.
실사용 수집
확인할 수 있는 것: 실제 방문에서 수집된 LCP·INP·CLS의 분포
단정할 수 없는 것: 표본·기간·기기 구성이 다르면 단순 전후 비교가 어렵습니다.
실행 기록
확인할 수 있는 것: 특정 재현 구간의 긴 작업·레이아웃·이벤트 처리
단정할 수 없는 것: 한 번의 trace만으로 전체 사용자 원인을 확정하지 않습니다.
번들 분석
확인할 수 있는 것: 포함된 모듈의 크기와 import 관계
단정할 수 없는 것: 파일 크기만으로 실제 실행 시간이나 사용자 지연을 알 수 없습니다.

성능 병목 현상 해결 전략

병목 현상이 식별되면, 다음 전략들을 사용하여 성능을 개선할 수 있습니다.

이미지 최적화

  • next/image 컴포넌트 사용
    • 자동 최적화: 기본 이미지 최적화기를 통해 허용된 포맷과 크기 후보를 제공합니다. 실제 선택은 브라우저 지원, sizes와 이미지 설정 등에 따라 달라집니다.
    • 레이지 로딩: 기본 loading="lazy"는 브라우저가 계산한 뷰포트 주변 거리에서 로드를 시작합니다. 첫 화면의 중요한 이미지는 지연시키지 않도록 별도로 판단합니다.
    • Placeholder: placeholder="blur"와 정적 import가 만든 blurDataURL 또는 직접 제공한 blurDataURL을 함께 사용하면 로딩 중 흐릿한 미리보기를 표시합니다.
    • 공간 예약: CLS를 줄이는 핵심은 width와 height, 또는 크기가 정해진 부모의 fill로 이미지 비율과 공간을 먼저 확보하는 것입니다.
  • SVG 사용: 아이콘이나 단순한 그래픽은 SVG 형식을 사용하여 파일 크기를 줄입니다.

폰트 최적화

  • next/font 사용
    • 자동 최적화: 자체 호스팅과 대체 폰트 조정으로 표시 지연·레이아웃 이동을 줄일 수 있습니다. swap 자체가 CLS 0을 보장하지 않으므로 실제 글꼴과 배치를 확인합니다.
    • 네트워크 요청 감소: Google Fonts 등을 사용할 때 폰트를 앱의 정적 자산으로 자체 호스팅하므로 브라우저가 Google 폰트 서버에 직접 요청할 필요가 없습니다. Vercel에 배포해야만 사용할 수 있는 기능은 아닙니다.

스크립트 최적화

  • next/script 사용
    • 서드파티 스크립트(Google Analytics, 광고 등) 로딩 전략을 제어하여 페이지 로딩에 미치는 영향을 최소화합니다. strategy="afterInteractive" 또는 strategy="lazyOnload"를 사용하여 필수적이지 않은 스크립트의 로딩 시점을 지연시킵니다.
  • 불필요한 스크립트 제거: 사용하지 않는 라이브러리나 스크립트는 제거하여 번들 크기를 줄입니다.

데이터 페칭 및 캐싱 전략

  • 서버 컴포넌트 활용: 초기 로딩에 필요한 데이터는 서버 컴포넌트에서 페칭하여 클라이언트 측 JavaScript 번들 크기를 줄이고, SSR/SSG를 통해 빠른 초기 렌더링을 제공합니다.
  • Next.js 데이터 캐싱 활용
    • fetch 응답은 기본적으로 캐시되지 않습니다. 반복해서 재사용할 요청에는 cache: 'force-cache'를 지정합니다. use cache는 Next.js 16에서 cacheComponents: true를 활성화한 프로젝트에 적용할 수 있습니다.
    • 한 번의 렌더링 안에서 같은 fetch 요청은 메모이제이션될 수 있지만, 이는 요청 사이에 응답을 보존하는 Data Cache와 다릅니다.
    • 캐싱한 경로는 revalidatePath, 태그의 SWR 갱신은 revalidateTag(tag, 'max'), Server Action의 즉시 만료는 updateTag로 처리합니다.
    • Cache Components를 사용하지 않는 Node.js 라우트에서는 export const revalidate = 60; 같은 segment 설정을 사용할 수 있습니다. cacheComponents: true인 모델에서는 캐시 대상 안의 cacheLife 등 해당 모델의 API를 사용합니다.
  • 응답 DTO와 필드 선택: 화면에 필요한 필드만 조회하고 반환하여 데이터베이스 작업과 네트워크 전송량을 줄입니다.

동적 분할 적용 지점

  • 자동 코드 스플리팅: 라우트와 컴포넌트 경계로 코드를 분할하지만 공유 모듈이나 링크 prefetch로 다른 라우트의 코드가 미리 로드될 수도 있습니다.
  • 동적 임포트 (next/dynamic): 특정 컴포넌트나 라이브러리가 초기 로딩에 필수가 아닌 경우, 필요할 때만 로드되도록 동적 임포트를 적용합니다. 특히 큰 차트 라이브러리, 에디터 등에서 유용합니다.
    src/components/ClientOnlyMap.tsx (클라이언트 컴포넌트)
    "use client";
    
    import dynamic from 'next/dynamic';
    
    const DynamicMap = dynamic(() => import('../components/MapComponent'), {
      ssr: false, // 이 컴포넌트는 클라이언트에서만 렌더링
      loading: () => <p>지도를 로딩 중...</p>,
    });
    
    function MyPage() {
      return (
        <div>
          <DynamicMap />
        </div>
      );
    }

컴포넌트 최적화 및 렌더링 성능

  • 불필요한 리렌더링 방지
    • memo는 컴포넌트, useMemo는 계산 결과, useCallback은 함수 참조의 재사용을 돕습니다. 측정된 비용에 적용하며 재렌더 생략이나 성능 향상을 보장하지는 않습니다.
    • 자주 업데이트되는 상태를 상위 컴포넌트에서 하위 컴포넌트로 props로 전달하기보다는, 해당 상태를 사용하는 컴포넌트 자체에서 관리하도록 구조를 조정합니다.
  • CSS 최적화
    • Tailwind CSS 4는 소스에서 감지한 클래스에 필요한 유틸리티를 생성합니다. 동적으로 조립한 클래스 이름은 감지되지 않을 수 있으므로 완전한 문자열로 표현합니다.
    • CSS-in-JS 라이브러리 사용 시 서버 사이드 렌더링을 위한 설정이 올바른지 확인합니다.
  • 가상화된 목록 (Virtualization): 수백, 수천 개의 항목이 있는 긴 목록을 렌더링할 때, react-window나 react-virtualized와 같은 라이브러리를 사용하여 뷰포트 주변의 일부 항목만 렌더링해 DOM과 렌더 비용을 줄일 수 있습니다. overscan 범위와 키보드 탐색도 함께 확인합니다.

서버 측 로직 최적화

Route Handler와 Server Actions에서 활용할 수 있는 최적화 전략입니다.

  • 데이터베이스 쿼리 최적화
    • N+1 쿼리 문제 해결: 조인이나 배치 조회로 반복 쿼리를 줄입니다. Mongoose의 populate가 SQL JOIN처럼 항상 단일 쿼리를 실행하는 것은 아니므로 실제 조회 횟수와 지연을 확인합니다.
    • 인덱스 사용: 자주 쿼리되는 필드에 데이터베이스 인덱스를 생성하여 검색 속도를 향상시킵니다.
    • 불필요한 데이터 제외: 필요한 필드만 select하여 가져옵니다.
  • 서버리스 콜드 스타트(Cold Start) 관리
    • 실행 위치 측정: Proxy는 Node.js 런타임을 사용하고, Edge Route Handler는 지역 분산의 이점이 측정될 때만 선택합니다.
    • 지속적인 트래픽 유지: 웜업 요청이 새 인스턴스나 증설된 인스턴스의 준비 상태를 보장하지는 않습니다. 공급자의 실행 모델과 실제 콜드 스타트 비중을 먼저 확인하며, 주기 호출의 비용도 함께 계산합니다.

성능 병목 식별과 해결은 반복적인 과정입니다.

URL·기기·네트워크·캐시 상태를 기록하고 한 가지 수정의 전후를 같은 조건에서 비교합니다. 실사용 지표는 표본 수와 기간을 함께 확인해 개선 우선순위를 정합니다.

일반적인 오류 및 해결 방법

이전 페이지

브라우저 개발자 도구 활용

다음 페이지

이 페이지의 목차

성능 병목 현상이란?성능 병목 현상 식별 도구Lighthouse 성능 기준 측정Chrome DevToolsVercel Speed Insights번들 분석기성능 병목 현상 해결 전략이미지 최적화폰트 최적화스크립트 최적화데이터 페칭 및 캐싱 전략동적 분할 적용 지점컴포넌트 최적화 및 렌더링 성능서버 측 로직 최적화