what-happens-when-build
Next.js 빌드 과정: 개발 코드가 프로덕션으로 변환되는 여정
Next.js 애플리케이션을 빌드할 때 무슨 일이 일어나는지 궁금해하신 적이 있나요? next build
명령어 하나로 개발 중인 코드가 어떻게 최적화된 프로덕션 애플리케이션으로 변환되는지 자세히 알아보겠습니다.
핵심 개념 이해하기
파일 크기 최소화 (Minification)
빌드 과정에서 가장 중요한 작업 중 하나는 파일 크기 최소화입니다. 이 과정에서는:
- 공백 제거: 불필요한 공백, 탭, 줄바꿈 문자를 제거합니다
- 변수명 단축: 긴 변수명을 짧은 문자로 변경합니다 (
userName
→a
) - 주석 제거: 개발용 주석을 모두 제거합니다
- 데드 코드 제거: 사용되지 않는 코드를 식별하여 제거합니다
// 빌드 전
function calculateUserAge(birthYear, currentYear) {
// 나이를 계산하는 함수
const age = currentYear - birthYear;
return age;
}
// 빌드 후 (최소화됨)
function a(b,c){return c-b}
트랜스파일 (Transpilation)
트랜스파일은 최신 JavaScript 문법을 구형 브라우저에서도 실행 가능한 코드로 변환하는 과정입니다:
- ES6+ → ES5: 화살표 함수, 구조 분해 할당 등을 구형 문법으로 변환
- TypeScript → JavaScript: 타입 정보를 제거하고 순수 JavaScript로 변환
- JSX → JavaScript: React JSX 문법을
React.createElement()
호출로 변환
// 빌드 전 (JSX)
const Welcome = ({ name }) => <h1>Hello, {name}!</h1>;
// 빌드 후 (트랜스파일됨)
const Welcome = ({ name }) => React.createElement("h1", null, "Hello, ", name, "!");
파일 통합 (Bundling)
번들링은 여러 개의 파일을 하나 또는 몇 개의 파일로 합치는 과정입니다:
- JavaScript 번들링: 여러 JS 파일을 하나의 번들로 통합
- CSS 번들링: 분산된 CSS 파일들을 통합하여 HTTP 요청 수 최소화
- 의존성 해결: import/require 구문을 분석하여 필요한 모듈만 포함
Next.js 빌드 과정 상세 분석
1. 컴파일 단계 (Compilation Phase)
TypeScript/JavaScript 컴파일
Next.js는 먼저 모든 TypeScript 파일을 JavaScript로 컴파일합니다. 이 과정에서:
// pages/api/users.ts
interface User {
id: number;
name: string;
}
export default function handler(req: NextApiRequest, res: NextApiResponse<User[]>) {
// API 로직
}
타입 정보가 제거되고 순수 JavaScript로 변환됩니다.
JSX 변환
React 컴포넌트의 JSX 문법이 JavaScript 함수 호출로 변환됩니다:
// 변환 전
function HomePage() {
return (
<div className="container">
<h1>Welcome to Next.js!</h1>
</div>
);
}
// 변환 후 (React 17+ 자동 변환)
import { jsx as _jsx } from "react/jsx-runtime";
function HomePage() {
return _jsx("div", {
className: "container",
children: _jsx("h1", { children: "Welcome to Next.js!" })
});
}
2. 번들링 단계 (Bundling Phase)
Webpack 기반 번들링
Next.js는 내부적으로 Webpack을 사용하여 번들링을 수행합니다:
- Entry Point 분석:
pages/
디렉토리의 각 파일을 진입점으로 식별 - 의존성 그래프 생성: 각 파일이 import하는 모듈들의 관계를 파악
- 청크 분할: 코드를 효율적인 크기의 청크로 분할
// 의존성 그래프 예시
pages/index.js
├── components/Header.js
│ ├── styles/header.module.css
│ └── utils/navigation.js
├── components/Footer.js
└── lib/api.js
└── config/constants.js
코드 분할 (Code Splitting)
Next.js는 자동으로 페이지별 코드 분할을 수행합니다:
- 페이지별 분할: 각 페이지는 별도의 JavaScript 번들로 생성
- 공통 코드 추출: 여러 페이지에서 사용되는 코드는 공통 청크로 분리
- 동적 import:
dynamic()
함수로 지연 로딩되는 컴포넌트는 별도 청크로 생성
3. 최적화 단계 (Optimization Phase)
Tree Shaking
사용되지 않는 코드를 제거하는 과정입니다:
// utils/helpers.js
export function usedFunction() { /* ... */ }
export function unusedFunction() { /* ... */ } // 이 함수는 제거됨
// pages/index.js
import { usedFunction } from '../utils/helpers';
// unusedFunction은 import되지 않았으므로 최종 번들에서 제거
이미지 최적화
Next.js의 Image 컴포넌트를 사용한 이미지들은:
- WebP 변환: 지원하는 브라우저에서는 WebP 형식으로 제공
- 크기 최적화: 여러 크기의 이미지를 생성하여 반응형 제공
- 지연 로딩: 뷰포트에 들어올 때까지 로딩을 지연
CSS 최적화
CSS 파일들도 최적화 과정을 거칩니다:
/* 빌드 전 */
.container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* 빌드 후 (최소화됨) */
.container{display:flex;justify-content:center;align-items:center;padding:20px}
4. 정적 생성 및 서버 사이드 렌더링
정적 페이지 생성 (Static Generation)
getStaticProps
나 getStaticPaths
를 사용하는 페이지들은 빌드 시점에 HTML로 미리 생성됩니다:
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post } };
}
export async function getStaticPaths() {
const posts = await fetchAllPosts();
const paths = posts.map(post => ({ params: { slug: post.slug } }));
return { paths, fallback: false };
}
빌드 시점에 모든 블로그 포스트에 대한 HTML 파일이 생성됩니다.
서버 사이드 렌더링 준비
getServerSideProps
를 사용하는 페이지들은 서버에서 실행될 수 있도록 준비됩니다.
5. 출력 파일 생성
빌드가 완료되면 .next
디렉토리에 다음과 같은 파일들이 생성됩니다:
.next/
├── static/
│ ├── chunks/ # JavaScript 청크들
│ ├── css/ # CSS 파일들
│ └── media/ # 이미지, 폰트 등
├── server/
│ ├── pages/ # 서버 사이드 코드
│ └── chunks/ # 서버용 청크들
└── BUILD_ID # 빌드 식별자
성능 최적화 기법
번들 분석
빌드된 번들의 크기를 분석하려면:
# 번들 분석기 설치
bun add @next/bundle-analyzer
# next.config.js에 설정 추가
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Next.js 설정
});
# 분석 실행
ANALYZE=true bun run build
캐싱 전략
Next.js는 효율적인 캐싱을 위해:
- 빌드 ID: 각 빌드마다 고유한 ID를 생성하여 캐시 무효화
- 해시 기반 파일명: 파일 내용이 변경될 때만 파일명이 변경되어 브라우저 캐시 활용
- 청크 분할: 변경되지 않은 코드는 캐시에서 재사용
환경별 최적화
개발 환경 vs 프로덕션 환경
// next.config.js
module.exports = {
// 프로덕션에서만 최적화 적용
swcMinify: true,
compress: true,
// 개발 환경에서는 빠른 빌드를 위해 최적화 생략
webpack: (config, { dev }) => {
if (!dev) {
// 프로덕션 전용 최적화
config.optimization.minimize = true;
}
return config;
}
};
빌드 성능 향상 팁
1. 의존성 최적화
불필요한 의존성을 제거하고, 필요한 부분만 import하세요:
// 좋지 않은 예
import * as _ from 'lodash';
// 좋은 예
import { debounce } from 'lodash';
// 또는
import debounce from 'lodash/debounce';
2. 동적 import 활용
큰 라이브러리나 컴포넌트는 동적으로 로드하세요:
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
});
3. 이미지 최적화
Next.js Image 컴포넌트를 적극 활용하세요:
import Image from 'next/image';
function MyComponent() {
return (
<Image
src="/hero-image.jpg"
alt="Hero"
width={800}
height={600}
priority // 중요한 이미지는 우선 로딩
/>
);
}
다른 React 빌드 도구들과의 비교
Create React App (CRA) 빌드 과정
Create React App은 React 애플리케이션을 위한 전통적인 빌드 도구입니다. 내부적으로 Webpack을 사용하지만, 설정이 추상화되어 있습니다.
CRA의 빌드 단계
# CRA 빌드 실행
npm run build
1. 환경 변수 처리
// .env 파일의 REACT_APP_ 접두사 변수들이 빌드 시점에 주입됩니다
const API_URL = process.env.REACT_APP_API_URL;
2. Webpack 설정 적용 CRA는 미리 정의된 Webpack 설정을 사용합니다:
- Entry Point:
src/index.js
- Output:
build/
디렉토리 - Code Splitting: React.lazy()와 동적 import 지원
// 동적 import를 통한 코드 분할
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
3. 정적 파일 처리
build/
├── static/
│ ├── css/
│ │ └── main.[hash].css
│ ├── js/
│ │ ├── main.[hash].js
│ │ └── [chunk].[hash].chunk.js
│ └── media/
│ └── [name].[hash].[ext]
├── index.html
└── manifest.json
CRA의 한계점
- 설정 커스터마이징 어려움: eject 없이는 Webpack 설정 변경이 제한적
- 빌드 속도: 대규모 프로젝트에서 상대적으로 느린 빌드 시간
- 번들 크기: 최적화가 제한적일 수 있음
# eject를 통한 설정 노출 (되돌릴 수 없음)
npm run eject
Vite 빌드 과정
Vite는 현대적인 빌드 도구로, 개발 시에는 ES 모듈을 활용하고 프로덕션에서는 Rollup을 사용합니다.
Vite의 개발 vs 프로덕션
개발 환경 (Dev Server)
// 개발 시에는 ES 모듈을 직접 사용
import { useState } from 'react';
import './App.css';
// 브라우저가 직접 ES 모듈을 로드 (번들링 없음)
프로덕션 빌드
# Vite 빌드 실행
npm run build
Vite는 프로덕션에서 Rollup을 사용하여 최적화된 번들을 생성합니다:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios']
}
}
}
}
});
Vite의 장점
1. 빠른 개발 서버
- Cold Start: 의존성을 사전 번들링하여 빠른 시작
- HMR (Hot Module Replacement): 변경사항을 즉시 반영
2. 효율적인 번들링
// Tree Shaking이 더 효과적
import { debounce } from 'lodash-es'; // ES 모듈 버전 사용
3. 플러그인 생태계
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
});
전통적인 Webpack 설정
순수 Webpack을 사용하는 경우의 빌드 과정을 이해하는 것도 중요합니다.
기본 Webpack 설정
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
Webpack의 핵심 개념
1. Loaders 파일을 변환하는 역할을 합니다:
// babel-loader: JSX를 JavaScript로 변환
{
test: /\.(js|jsx)$/,
use: 'babel-loader'
}
// css-loader: CSS를 JavaScript 모듈로 변환
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
2. Plugins 번들링 과정에서 추가 작업을 수행합니다:
// HTML 파일 생성
new HtmlWebpackPlugin({
template: './src/index.html'
})
// CSS 파일 분리
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
3. Code Splitting
// 동적 import를 통한 코드 분할
const LazyComponent = React.lazy(() =>
import(/* webpackChunkName: "lazy-component" */ './LazyComponent')
);
빌드 도구별 성능 비교
빌드 속도 비교
도구 | 초기 빌드 | 증분 빌드 | 개발 서버 시작 |
---|---|---|---|
Next.js | 보통 | 빠름 | 빠름 |
CRA | 느림 | 보통 | 보통 |
Vite | 빠름 | 매우 빠름 | 매우 빠름 |
Webpack | 설정에 따라 | 설정에 따라 | 설정에 따라 |
번들 크기 최적화
Next.js
// 자동 코드 분할과 Tree Shaking
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/Heavy'), {
loading: () => <p>Loading...</p>
});
Vite
// 효율적인 Tree Shaking
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom'], // 외부 의존성으로 분리
}
}
});
CRA/Webpack
// 수동 최적화 필요
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
},
vendor: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
}
}
}
}
도구 선택 가이드
프로젝트 특성에 따른 선택
Next.js를 선택해야 하는 경우:
- SSR/SSG가 필요한 프로젝트
- SEO가 중요한 웹사이트
- 풀스택 React 애플리케이션
Vite를 선택해야 하는 경우:
- 빠른 개발 경험이 중요한 프로젝트
- 모던 브라우저를 주로 타겟하는 경우
- 라이브러리 개발
CRA를 선택해야 하는 경우:
- 빠른 프로토타이핑
- React 학습 목적
- 복잡한 설정이 필요 없는 간단한 SPA
순수 Webpack을 선택해야 하는 경우:
- 완전한 커스터마이징이 필요한 경우
- 특수한 빌드 요구사항이 있는 경우
- 기존 Webpack 설정을 유지해야 하는 경우
마이그레이션 고려사항
CRA에서 Vite로 마이그레이션
# 의존성 변경
npm uninstall react-scripts
npm install vite @vitejs/plugin-react
# package.json 스크립트 수정
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
// vite.config.js 생성
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 3000
}
});
환경 변수 처리 차이
// CRA
const apiUrl = process.env.REACT_APP_API_URL;
// Vite
const apiUrl = import.meta.env.VITE_API_URL;
// Next.js
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
마무리
React 생태계의 빌드 도구들은 각각 고유한 장단점을 가지고 있습니다. Next.js는 풀스택 React 애플리케이션에 최적화되어 있고, Vite는 빠른 개발 경험을 제공하며, CRA는 간단한 시작을 도와주고, 순수 Webpack은 완전한 제어권을 제공합니다.
현대적인 웹 개발에서는 Vite와 Next.js가 주류가 되고 있지만, 기존 도구들의 동작 원리를 이해하는 것은 여전히 중요합니다. 특히 레거시 프로젝트를 유지보수하거나, 빌드 과정에서 문제가 발생했을 때 근본적인 해결책을 찾는 데 도움이 됩니다.
빌드 도구를 선택할 때는 프로젝트의 요구사항, 팀의 경험, 성능 목표 등을 종합적으로 고려하여 결정하는 것이 중요합니다. 그리고 무엇보다 각 도구의 빌드 과정을 이해하고 있다면, 더 효율적인 개발과 최적화된 애플리케이션을 만들 수 있을 것입니다.