성능 최적화 기법
Next.js 애플리케이션의 성능을 향상시키는 다양한 최적화 기법을 학습합니다.
Next.js 기초 입문 25회차: Next.js 애플리케이션 성능 최적화 기법
Next.js는 기본적으로 뛰어난 성능을 제공하지만, 대규모 애플리케이션에서는 추가적인 최적화 노력이 필요합니다. 본 회차에서는 Next.js 애플리케이션의 성능을 체계적으로 분석하고 개선하는 다양한 기법들을 학습합니다.
📌 이번 회차 학습 목표
- Next.js 애플리케이션의 성능 병목 지점을 식별할 수 있습니다.
- 코드 스플리팅(Code Splitting)을 활용하여 초기 로딩 시간을 단축할 수 있습니다.
- 번들 분석 도구를 사용하여 애플리케이션의 번들 크기를 최적화할 수 있습니다.
- 이미지 최적화 및 데이터 페칭 최적화 기법을 적용할 수 있습니다.
- 성능 최적화가 사용자 경험(UX) 및 SEO에 미치는 영향을 이해합니다.
📝 개념 설명
1. 성능 최적화의 중요성
웹 애플리케이션의 성능은 사용자 경험, 전환율, 검색 엔진 최적화(SEO)에 직접적인 영향을 미칩니다. 로딩 시간이 길어지면 사용자는 이탈할 가능성이 높아지며, 검색 엔진 순위에도 부정적인 영향을 미칠 수 있습니다. Next.js는 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG)과 같은 기능을 통해 초기 로딩 성능을 향상시키지만, 클라이언트 측 번들 크기, 이미지 로딩, 데이터 페칭 방식 등 다양한 요소가 여전히 성능에 영향을 미칩니다.
2. 코드 스플리팅 (Code Splitting)
코드 스플리팅은 애플리케이션의 코드를 여러 개의 작은 번들로 나누어, 사용자가 현재 필요한 코드만 로드하도록 하는 기법입니다. Next.js는 페이지 기반 라우팅을 통해 기본적으로 페이지 단위의 코드 스플리팅을 제공합니다. 즉, 사용자가 특정 페이지에 접속할 때 해당 페이지에 필요한 JavaScript 코드만 로드됩니다. 하지만 컴포넌트 레벨에서도 동적 임포트(Dynamic Imports)를 사용하여 추가적인 코드 스플리팅을 구현할 수 있습니다.
next/dynamic을 사용하면 특정 컴포넌트를 필요할 때만 로드하도록 설정할 수 있습니다. 이는 특히 크기가 큰 라이브러리나 특정 사용자 인터랙션에만 필요한 컴포넌트에 유용합니다.
3. 번들 분석 (Bundle Analysis)
애플리케이션의 번들 크기를 이해하는 것은 성능 최적화의 첫걸음입니다. 번들 분석 도구는 어떤 모듈이 번들에 가장 큰 비중을 차지하는지 시각적으로 보여주어, 최적화 대상을 쉽게 식별할 수 있도록 돕습니다. @next/bundle-analyzer와 같은 도구를 Next.js 프로젝트에 통합하여 번들 맵을 생성하고 분석할 수 있습니다.
4. 이미지 최적화
이미지는 웹 페이지에서 가장 큰 용량을 차지하는 요소 중 하나입니다. Next.js는 next/image 컴포넌트를 통해 이미지 최적화를 기본적으로 지원합니다. 이 컴포넌트는 다음과 같은 기능을 제공합니다.
- 자동 이미지 최적화: 요청 시 이미지를 자동으로 최적화하고 웹에 최적화된 형식(예: WebP)으로 변환합니다.
- 반응형 이미지: 뷰포트 크기에 따라 적절한 크기의 이미지를 제공합니다.
- 지연 로딩 (Lazy Loading): 뷰포트에 들어올 때까지 이미지 로딩을 지연시켜 초기 로딩 성능을 향상시킵니다.
- 레이아웃 시프트 방지: 이미지 로딩으로 인한 레이아웃 시프트를 방지하여 사용자 경험을 개선합니다.
5. 데이터 페칭 최적화
데이터 페칭 방식도 애플리케이션 성능에 큰 영향을 미칩니다. Next.js에서는 getServerSideProps, getStaticProps, getStaticPaths를 통해 데이터를 효율적으로 가져올 수 있습니다. 상황에 맞는 적절한 데이터 페칭 전략을 선택하는 것이 중요합니다.
getStaticProps: 빌드 시점에 데이터를 가져와 정적 HTML을 생성합니다. 데이터가 자주 변경되지 않는 페이지에 적합하며, 가장 빠른 로딩 속도를 제공합니다.getServerSideProps: 요청 시마다 서버에서 데이터를 가져와 페이지를 렌더링합니다. 실시간 데이터가 필요한 페이지에 적합하지만, 매 요청마다 서버 부하가 발생할 수 있습니다.- 클라이언트 측 페칭:
useEffect훅과 같은 클라이언트 측에서 데이터를 가져오는 방식입니다. 초기 로딩 후 동적으로 데이터를 업데이트해야 할 때 유용합니다. SWR이나 React Query와 같은 라이브러리를 사용하면 캐싱, 재검증 등의 기능을 통해 더욱 효율적인 데이터 페칭을 구현할 수 있습니다.
@next/bundle-analyzer로 번들 구성 파악 및 최적화 대상 식별.next/image를 활용하여 반응형, 지연 로딩, 자동 최적화 구현.getStaticProps, getServerSideProps 등 상황에 맞는 최적의 전략 선택.💡 예제 & 실습
1. 동적 임포트를 이용한 코드 스플리팅
특정 버튼을 클릭했을 때만 로드되는 모달 컴포넌트를 구현하여 동적 임포트의 효과를 확인해봅니다.
단계 1: 모달 컴포넌트 생성 (components/MyModal.js)
// components/MyModal.js
import React from 'react';
const MyModal = ({ onClose }) => {
return (
<div style={{ border: '1px solid black', padding: '20px', backgroundColor: 'white', position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: 1000 }}>
<h2>동적으로 로드된 모달입니다!</h2>
<p>이 내용은 모달이 열릴 때만 로드됩니다.</p>
<button onClick={onClose}>닫기</button>
</div>
);
};
export default MyModal;
단계 2: 페이지에서 동적 임포트 적용 (pages/index.js)
// pages/index.js
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
// MyModal 컴포넌트를 동적으로 임포트합니다.
// ssr: false는 서버 사이드 렌더링을 비활성화하고 클라이언트 측에서만 로드하도록 합니다.
const DynamicMyModal = dynamic(() => import('../components/MyModal'), {
ssr: false,
loading: () => <p>모달 로딩 중...</p>,
});
export default function Home() {
const [showModal, setShowModal] = useState(false);
const handleOpenModal = () => {
setShowModal(true);
};
const handleCloseModal = () => {
setShowModal(false);
};
return (
<div>
<h1>Next.js 성능 최적화 실습</h1>
<p>아래 버튼을 클릭하면 모달이 동적으로 로드됩니다.</p>
<button onClick={handleOpenModal}>모달 열기</button>
{showModal && <DynamicMyModal onClose={handleCloseModal} />}
</div>
);
);
}
해설: 초기 페이지 로드 시에는 MyModal 컴포넌트의 코드가 번들에 포함되지 않습니다. ‘모달 열기’ 버튼을 클릭하여 showModal 상태가 true가 되면, 그때서야 DynamicMyModal이 로드되고 렌더링됩니다. 개발자 도구의 네트워크 탭에서 JS 파일 로딩을 확인해보면, 모달을 열기 전에는 MyModal 관련 청크가 로드되지 않음을 확인할 수 있습니다.
2. @next/bundle-analyzer를 이용한 번들 분석
번들 분석 도구를 설치하고 사용하여 애플리케이션의 번들 크기를 시각적으로 확인해봅니다.
npm install --save-dev @next/bundle-analyzer
# 또는
yarn add --dev @next/bundle-analyzer
단계 2: next.config.js 설정
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Next.js 설정 추가
reactStrictMode: true,
});
단계 3: package.json 스크립트 추가
// package.json
{
"name": "nextjs-perf-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"analyze": "ANALYZE=true next build"
},
"dependencies": {
"next": "14.x.x",
"react": "18.x.x",
"react-dom": "18.x.x"
},
"devDependencies": {
"@next/bundle-analyzer": "latest",
"eslint": "8.x.x",
"eslint-config-next": "14.x.x"
}
}
npm run analyze
# 또는
yarn analyze
해설: 빌드가 완료되면 브라우저에 번들 맵이 자동으로 열립니다. 이 맵을 통해 어떤 파일이나 라이브러리가 번들 크기에 가장 큰 영향을 미치는지 시각적으로 확인할 수 있습니다. 이를 바탕으로 불필요한 라이브러리를 제거하거나, 더 가벼운 대안을 찾거나, 동적 임포트를 적용하는 등의 최적화 전략을 수립할 수 있습니다.
3. next/image를 이용한 이미지 최적화
next/image 컴포넌트를 사용하여 이미지를 최적화하는 방법을 살펴봅니다.
단계 1: 이미지 파일 준비 (public/my-image.jpg)
public 폴더 안에 적당한 크기의 이미지 파일을 하나 넣어둡니다. (예: public/my-image.jpg)
단계 2: 페이지에 next/image 적용 (pages/image-test.js)
// pages/image-test.js
import Image from 'next/image';
export default function ImageTestPage() {
return (
<div>
<h1>Next.js Image 최적화 테스트</h1>
<p>일반 img 태그:</p>
<img src='/my-image.jpg' alt='일반 이미지' width='500' height='300' />
<p>next/image 컴포넌트:</p>
<Image
src='/my-image.jpg'
alt='최적화된 이미지'
width={500}
height={300}
priority={true} // 이 이미지는 페이지 로드 시 즉시 로드되어야 함을 나타냅니다.
/>
<p>지연 로딩되는 next/image 컴포넌트 (스크롤해야 보임):</p>
<div style={{ height: '1000px' }}></div> {/* 스크롤을 위한 공간 */}
<Image
src='/my-image.jpg'
alt='지연 로딩 이미지'
width={500}
height={300}
// priority가 없으면 기본적으로 lazy 로딩됩니다.
/>
</div>
);
}
해설: next/image 컴포넌트는 <img> 태그와 달리 width와 height 속성을 필수로 요구하여 레이아웃 시프트를 방지합니다. priority 속성을 사용하면 LCP(Largest Contentful Paint)에 기여하는 중요한 이미지를 우선적으로 로드할 수 있습니다. priority가 없는 이미지는 기본적으로 지연 로딩(lazy loading)되어 사용자가 스크롤하여 해당 이미지가 뷰포트에 들어올 때 로드됩니다. 개발자 도구의 네트워크 탭에서 이미지 형식이 WebP 등으로 변환되고, 여러 해상도의 이미지가 생성되는 것을 확인할 수 있습니다.
next/image 사용⚠️ 자주 틀리는 것 / 주의사항
- 모든 컴포넌트에 동적 임포트 적용: 동적 임포트는 필요한 경우에만 사용해야 합니다. 너무 많은 컴포넌트에 적용하면 오히려 네트워크 요청이 늘어나 성능이 저하될 수 있습니다.
next/image의width,height누락:next/image사용 시width와height속성을 반드시 지정해야 합니다. 이를 누락하면 개발 서버에서는 경고가 발생하고, 프로덕션 빌드에서는 레이아웃 시프트가 발생할 수 있습니다.- 번들 분석 결과 오해: 번들 분석 결과에서 특정 라이브러리가 크게 보인다고 무조건 제거하거나 교체할 필요는 없습니다. 해당 라이브러리가 애플리케이션의 핵심 기능을 제공한다면, 그 크기는 정당화될 수 있습니다. 중요한 것은 불필요하게 큰 라이브러리나 중복된 코드를 식별하는 것입니다.
- 데이터 페칭 전략의 오용: 모든 데이터를
getServerSideProps로 가져오면 서버 부하가 커지고 TTFB(Time To First Byte)가 길어질 수 있습니다. 정적인 데이터는getStaticProps를, 자주 변경되는 데이터는 클라이언트 측 페칭을 고려하는 것이 좋습니다.
🔗 다음 회차 예고
이번 회차에서는 Next.js 애플리케이션의 성능 최적화 기법들을 학습하였습니다. 다음 26회차에서는 Next.js 배포 및 CI/CD 구축에 대해 다룰 예정입니다. 개발한 애플리케이션을 실제 서비스 환경에 배포하고, 지속적인 통합 및 배포(CI/CD) 파이프라인을 구축하는 방법을 학습하여 효율적인 개발 워크플로우를 완성할 것입니다.
- Next.js 성능 최적화는 사용자 경험, SEO, 전환율에 필수적입니다.
- 코드 스플리팅은
next/dynamic을 사용하여 필요한 코드만 로드함으로써 초기 번들 크기를 줄입니다. - 번들 분석 도구(예:
@next/bundle-analyzer)는 번들 구성 요소를 시각화하여 최적화 대상을 식별하는 데 도움을 줍니다. next/image컴포넌트는 이미지 자동 최적화, 반응형, 지연 로딩 기능을 제공하여 이미지 로딩 성능을 향상시킵니다.- 데이터 페칭 전략(
getStaticProps,getServerSideProps, 클라이언트 측 페칭)을 적절히 선택하여 데이터 로딩 성능을 최적화할 수 있습니다.