13. 빌드 도구와 개발 환경 (Build Tools & Development Environment)

13. 빌드 도구와 개발 환경 (Build Tools & Development Environment)

학습 목표

  • 패키지 관리자(npm, yarn, pnpm) 이해와 활용
  • 모던 빌드 도구(Vite, webpack) 설정
  • 개발 환경 구성 및 최적화
  • 환경 변수 관리
  • 프로덕션 빌드 최적화

목차

  1. 패키지 관리자
  2. Vite
  3. webpack 기초
  4. 환경 변수
  5. 코드 품질 도구
  6. 연습 문제

1. 패키지 관리자

1.1 npm (Node Package Manager)

# 프로젝트 초기화
npm init
npm init -y  # 기본값으로 초기화

# 패키지 설치
npm install lodash           # dependencies에 추가
npm install -D typescript    # devDependencies에 추가
npm install -g create-vite   # 전역 설치

# 단축 명령어
npm i lodash
npm i -D typescript

# 패키지 제거
npm uninstall lodash
npm rm lodash

# 패키지 업데이트
npm update                   # 모든 패키지
npm update lodash           # 특정 패키지
npm outdated                # 업데이트 가능한 패키지 확인

# 스크립트 실행
npm run dev
npm run build
npm test                    # npm run test 축약

# 패키지 정보
npm info lodash
npm list                    # 설치된 패키지 트리
npm list --depth=0          # 최상위만

1.2 package.json

{
  "name": "my-project",
  "version": "1.0.0",
  "description": "프로젝트 설명",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint src/**/*.{js,ts}",
    "format": "prettier --write src/**/*.{js,ts}",
    "test": "vitest",
    "prepare": "husky install"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "typescript": "^5.0.0",
    "vite": "^5.0.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/user/repo.git"
  },
  "keywords": ["react", "vite", "typescript"],
  "author": "Your Name <email@example.com>",
  "license": "MIT"
}

1.3 버전 관리

버전 형식: MAJOR.MINOR.PATCH (1.2.3)

package.json 버전 범위:
^1.2.3    1.x.x (MINOR, PATCH 업데이트 허용)
~1.2.3    1.2.x (PATCH 업데이트만 허용)
1.2.3     정확히 1.2.3
>=1.2.3   1.2.3 이상
1.2.x     1.2.0 ~ 1.2.999
*         모든 버전

권장:
- 프로덕션: package-lock.json 커밋
- 라이브러리: 범위 지정 (^)

1.4 yarn

# Yarn 설치
npm install -g yarn

# 기본 명령어
yarn init
yarn add lodash
yarn add -D typescript
yarn remove lodash
yarn upgrade
yarn                  # = yarn install

# Yarn 워크스페이스 (모노레포)
# package.json
{
  "workspaces": [
    "packages/*"
  ]
}

# 워크스페이스 패키지 실행
yarn workspace @myorg/web add react
yarn workspaces foreach run build

1.5 pnpm

# pnpm 설치
npm install -g pnpm

# 기본 명령어
pnpm init
pnpm add lodash
pnpm add -D typescript
pnpm remove lodash
pnpm update
pnpm install

# pnpm 장점
# - 디스크 공간 절약 (하드 링크)
# - 빠른 설치 속도
# - 엄격한 의존성 관리

# pnpm 워크스페이스
# pnpm-workspace.yaml
packages:
  - 'packages/*'

2. Vite

2.1 Vite 소개

┌─────────────────────────────────────────────────────────────────┐
│                    Vite 특징                                     │
│                                                                 │
│   개발 서버:                                                     │
│   - Native ES Modules 사용 (번들링 없음)                        │
│   - 즉각적인 HMR (Hot Module Replacement)                       │
│   - 빠른 콜드 스타트                                            │
│                                                                 │
│   프로덕션 빌드:                                                 │
│   - Rollup 기반 최적화                                          │
│   - 코드 스플리팅                                                │
│   - 트리 쉐이킹                                                  │
│                                                                 │
│   지원:                                                         │
│   - TypeScript, JSX, CSS Modules                               │
│   - React, Vue, Svelte 등                                      │
│   - 플러그인 시스템                                              │
└─────────────────────────────────────────────────────────────────┘

2.2 프로젝트 생성

# Vite 프로젝트 생성
npm create vite@latest my-app

# 템플릿 직접 지정
npm create vite@latest my-app -- --template react-ts
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template svelte-ts

# 프로젝트 구조
my-app/
├── node_modules/
├── public/
│   └── vite.svg
├── src/
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

2.3 vite.config.ts

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],

  // 개발 서버 설정
  server: {
    port: 3000,
    open: true,
    cors: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  // 빌드 설정
  build: {
    outDir: 'dist',
    sourcemap: true,
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'dayjs'],
        },
      },
    },
  },

  // 경로 별칭
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@utils': path.resolve(__dirname, './src/utils'),
    },
  },

  // CSS 설정
  css: {
    modules: {
      localsConvention: 'camelCase',
    },
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`,
      },
    },
  },

  // 최적화 설정
  optimizeDeps: {
    include: ['lodash', 'axios'],
    exclude: ['@vite/client'],
  },
});

2.4 TypeScript 설정

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* 번들러 모드 */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* 린팅 */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* 경로 별칭 */
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

2.5 정적 자산 처리

// 이미지 import
import logo from './assets/logo.png';  // URL 반환
import icon from './assets/icon.svg?raw';  // SVG 문자열

// public 폴더 (처리 없이 복사)
// public/favicon.ico → /favicon.ico

// CSS에서 자산 참조
.bg {
  background-image: url('@/assets/bg.png');
}

// 동적 URL
const imgUrl = new URL('./img.png', import.meta.url).href;

3. webpack 기초

3.1 webpack 소개

┌─────────────────────────────────────────────────────────────────┐
│                    webpack 개념                                  │
│                                                                 │
│   Entry: 진입점 (시작 파일)                                      │
│   Output: 번들링 결과물 위치                                     │
│   Loaders: 비-JS 파일 변환 (CSS, 이미지 등)                     │
│   Plugins: 번들 최적화, 환경 변수 주입 등                        │
│   Mode: development / production                                │
│                                                                 │
│   동작 방식:                                                     │
│   Entry → 의존성 그래프 분석 → Loaders 적용 →                   │
│   Plugins 실행 → Output 생성                                    │
└─────────────────────────────────────────────────────────────────┘

3.2 기본 설정

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // 모드
  mode: 'development', // 또는 'production'

  // 진입점
  entry: './src/index.js',

  // 출력
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    clean: true, // 이전 빌드 파일 삭제
  },

  // 로더
  module: {
    rules: [
      // JavaScript/TypeScript
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      // CSS
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      // SCSS
      {
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
      },
      // 이미지
      {
        test: /\.(png|jpg|gif|svg)$/,
        type: 'asset/resource',
      },
      // 폰트
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource',
      },
    ],
  },

  // 플러그인
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
  ],

  // 개발 서버
  devServer: {
    static: './dist',
    port: 3000,
    hot: true,
    open: true,
  },

  // 모듈 해석
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },

  // 소스맵
  devtool: 'source-map',
};

3.3 프로덕션 최적화

// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = merge(common, {
  mode: 'production',

  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },

  plugins: [
    new CompressionPlugin({
      algorithm: 'gzip',
    }),
    // 번들 분석 (필요 시)
    // new BundleAnalyzerPlugin(),
  ],
});

4. 환경 변수

4.1 Vite 환경 변수

# .env (모든 환경)
VITE_APP_NAME=My App

# .env.development (개발)
VITE_API_URL=http://localhost:8080

# .env.production (프로덕션)
VITE_API_URL=https://api.example.com

# .env.local (로컬, gitignore)
VITE_SECRET_KEY=my-secret
// 환경 변수 사용
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE;  // 'development' | 'production'
const isDev = import.meta.env.DEV;  // boolean
const isProd = import.meta.env.PROD;  // boolean

// 타입 정의
// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

4.2 webpack 환경 변수

// webpack.config.js
const webpack = require('webpack');
const dotenv = require('dotenv');

// .env 파일 로드
const env = dotenv.config().parsed;

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env': JSON.stringify(env),
    }),
  ],
};

// 또는 개별 변수
new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  'process.env.API_URL': JSON.stringify(process.env.API_URL),
});

4.3 환경별 설정

// config/index.ts
interface Config {
  apiUrl: string;
  debug: boolean;
  features: {
    newDashboard: boolean;
  };
}

const configs: Record<string, Config> = {
  development: {
    apiUrl: 'http://localhost:8080',
    debug: true,
    features: {
      newDashboard: true,
    },
  },
  production: {
    apiUrl: 'https://api.example.com',
    debug: false,
    features: {
      newDashboard: false,
    },
  },
};

export const config = configs[import.meta.env.MODE] || configs.development;

5. 코드 품질 도구

5.1 ESLint

# 설치
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# 초기화
npx eslint --init
// eslint.config.js (Flat Config - ESLint 9+)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    plugins: {
      react,
    },
    rules: {
      'no-unused-vars': 'warn',
      'no-console': 'warn',
      '@typescript-eslint/explicit-function-return-type': 'off',
      'react/prop-types': 'off',
    },
  },
  {
    ignores: ['dist/**', 'node_modules/**'],
  },
];

5.2 Prettier

# 설치
npm install -D prettier eslint-config-prettier
// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}
// .prettierignore
node_modules
dist
build
coverage
*.min.js

5.3 Husky + lint-staged

# 설치
npm install -D husky lint-staged

# Husky 초기화
npx husky install

# pre-commit 훅 추가
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,md,json}": [
      "prettier --write"
    ]
  }
}

5.4 EditorConfig

# .editorconfig
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

6. 연습 문제

연습 1: Vite 프로젝트 설정

React + TypeScript 프로젝트를 Vite로 설정하세요.

# 예시 답안
npm create vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install

# 필요한 추가 패키지
npm install -D @types/node
npm install axios react-router-dom
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
  },
});

연습 2: 환경 변수 설정

개발/프로덕션 환경별 API URL을 설정하세요.

# .env.development
VITE_API_URL=http://localhost:8080/api

# .env.production
VITE_API_URL=https://api.myapp.com/api
// src/config.ts
export const config = {
  apiUrl: import.meta.env.VITE_API_URL,
  isDev: import.meta.env.DEV,
};

// src/api/client.ts
import axios from 'axios';
import { config } from '../config';

export const apiClient = axios.create({
  baseURL: config.apiUrl,
});

연습 3: 코드 품질 도구 설정

ESLint + Prettier + Husky를 설정하세요.

# 설치
npm install -D eslint prettier eslint-config-prettier
npm install -D husky lint-staged
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

# Husky 설정
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
  "scripts": {
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix",
    "format": "prettier --write src/**/*.{ts,tsx,css}",
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,json,md}": ["prettier --write"]
  }
}

다음 단계

참고 자료

to navigate between lessons