๐Ÿš€ Next.js + Docker + CI/CD ์‹ค์ „ โ€” pnpm ์œผ๋กœ ํ”„๋กœ๋•์…˜ ์™„์„ฑ

2026๋…„ 3์›” 20์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

Next.js 14 App Router ํ”„๋กœ์ ํŠธ์—์„œ pnpm ์„ ํ™œ์šฉํ•œ ์™„์„ฑํ˜• package.json, BuildKit ์บ์‹œ๋ฅผ ์“ด Docker ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€, GitHub Actions ํŒŒ์ดํ”„๋ผ์ธ๊นŒ์ง€

06. Next.js + CI/CD + Docker

๐Ÿ“‹ ๋ชฉ์ฐจ

  1. ์ด์ •๋ฆฌ
  2. ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ
  3. ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ
  4. ๋” ์•Œ์•„๋ณด๊ธฐ

๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: ์•ฝ 28๋ถ„(์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 14๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„

[Next.js package.json ์„ค๊ณ„] โ†’ [.npmrc ์ตœ์ ํ™”] โ†’ [Docker ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€] โ†’ [GitHub Actions] โ†’ [์บ์‹œ ์ „๋žต]

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • Next.js 14 ํ”„๋กœ์ ํŠธ์—์„œ pnpm ์— ์ตœ์ ํ™”๋œ package.json ์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค
  • pnpm ์˜ BuildKit ์บ์‹œ ๋งˆ์šดํŠธ๋ฅผ ํ™œ์šฉํ•œ Docker ์ด๋ฏธ์ง€๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค
  • GitHub Actions ์—์„œ pnpm/action-setup ๊ณผ store ์บ์‹œ๋ฅผ ์กฐํ•ฉํ•œ ๋น ๋ฅธ CI ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค
  • ๋ชจ๋…ธ๋ ˆํฌ์—์„œ ํŠน์ • ์•ฑ๋งŒ Docker ์ด๋ฏธ์ง€๋กœ ๋ถ„๋ฆฌํ•˜๋Š” pnpm deploy ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

๋А๋ฆฐ ๋นŒ๋“œ์— ์ง€์นœ ์˜์ฒ ์ด์™€ ์บ์‹œ ์ „๋žต์„ ์•Œ๋ ค์ฃผ๋Š” ์˜ํ˜ธ
  • ๐Ÿฃ ์˜์ฒ  ( ์‹ ์ž… ): "์˜์ˆ˜ ๋‹˜, CI ๋นŒ๋“œ๊ฐ€ ๋งค๋ฒˆ 5๋ถ„ ์ด์ƒ ๊ฑธ๋ ค์š”. pnpm install ํ•˜๋Š” ๋ฐ๋งŒ 2๋ถ„์ธ๋ฐ ์ด ๋ถ€๋ถ„์„ ์–ด๋–ป๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์„๊นŒ์š”? ๊ทธ๋ฆฌ๊ณ  Docker ์ด๋ฏธ์ง€ ํฌ๊ธฐ๋„ 1.2GB ๋‚˜ ๋˜๋Š”๋ฐ ์ค„์ด๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์„๊นŒ์š”? ์ง€๊ธˆ์€ FROM node:20 ์— pnpm install ํ•œ ๋ฒˆ์— ๋‹ค ๋•Œ๋ ค๋„ฃ๊ณ  ์žˆ์–ด์š”."
  • ๐Ÿฆ ์˜ํ˜ธ ( ๋ฆฌ๋“œ ): "๋‘ ๊ฐ€์ง€ ํ•ต์‹ฌ๋งŒ ์žก์œผ๋ฉด ๋ผ์š”. GitHub Actions ์—์„œ๋Š” setup-node ์˜ ์บ์‹œ ์˜ต์…˜์œผ๋กœ pnpm store ๋ฅผ ์บ์‹œํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ๋นŒ๋“œ๋ถ€ํ„ฐ install ์ด 20์ดˆ ์•ˆ์— ๋๋‚˜์š”. Docker ๋Š” BuildKit ์บ์‹œ ๋งˆ์šดํŠธ(--mount=type=cache,target=/pnpm/store)๋กœ ์Šคํ† ์–ด๋ฅผ ๋นŒ๋“œ ๊ฐ„์— ์žฌ์‚ฌ์šฉํ•˜๊ณ , node:20-slim + ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€๋กœ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€๋ณ๊ฒŒ ๋งŒ๋“ค๋ฉด 400MB ๋‚ด๋กœ ์ถฉ๋ถ„ํžˆ ์ค„์ผ ์ˆ˜ ์žˆ์–ด์š”."

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

pnpm ์„ ๋‹จ์ˆœํžˆ "npm ๋Œ€์‹  ์“ฐ๋Š” ๊ฒƒ" ์— ๊ทธ์น˜๋ฉด, ์ง„์งœ ์ด์ ์˜ ์ ˆ๋ฐ˜๋„ ๋ชป ์“ฐ๋Š” ๊ฒƒ์ด๋‹ค.

pnpm ์ด CI/CD ์™€ Docker ์—์„œ ๋น›๋‚˜๋Š” ์ด์œ :

  • ๊ธ€๋กœ๋ฒŒ ์Šคํ† ์–ด ์บ์‹œ: ~/.pnpm-store ๋ฅผ CI ์บ์‹œ๋กœ ์ €์žฅํ•˜๋ฉด ์ดํ›„ ์„ค์น˜๊ฐ€ ํ•˜๋“œ๋งํฌ ์ƒ์„ฑ๋งŒ์œผ๋กœ ๋๋‚จ
  • BuildKit ์บ์‹œ ๋งˆ์šดํŠธ: Docker ๋นŒ๋“œ ์‹œ ์Šคํ† ์–ด๋ฅผ ๋ ˆ์ด์–ด ์บ์‹œ์ฒ˜๋Ÿผ ์žฌ์‚ฌ์šฉ โ†’ ์˜์กด์„ฑ ๋ณ€๊ฒฝ ์—†์œผ๋ฉด ์„ค์น˜ ์ฆ‰์‹œ ์™„๋ฃŒ
  • pnpm deploy: ๋ชจ๋…ธ๋ ˆํฌ์—์„œ ํŠน์ • ์•ฑ๋งŒ node_modules ๋ฅผ ํฌํ•จํ•ด ๋ฐฐํฌ ํด๋”๋กœ ๋ถ„๋ฆฌ โ†’ Docker ์ด๋ฏธ์ง€ ๊ฒฝ๋Ÿ‰ํ™”

๐Ÿ“ฆ Next.js 14 ์™„์„ฑํ˜• package.json

// package.json (Next.js 14 App Router + pnpm ์ตœ์ ํ™”)
// ๐Ÿ’ก JSON ํŒŒ์ผ์€ ์›๋ž˜ ์ฃผ์„์ด ์•ˆ ๋˜์ง€๋งŒ, ์„ค๋ช…์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
// ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ๋ณต์‚ฌํ•  ๋•Œ๋Š” ์ฃผ์„์„ ์ง€์›Œ์ฃผ์„ธ์š”! ๐Ÿ˜
{
  "name": "@youngsu/community",
  "version": "0.1.0",
  "private": true,
  
  // ๐Ÿฆ [ํ•ต์‹ฌ] packageManager ํ•„๋“œ: ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ์‚ฌ์šฉํ•  ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ๋ฒ„์ „์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.
  // ์ด ํ•œ ์ค„ ๋•๋ถ„์— ํŒ€์›๋“ค์˜ pnpm ๋ฒ„์ „์ด ์ž๋™์œผ๋กœ ํ†ต์ผ๋˜๊ณ , CI(GitHub Actions)์—์„œ๋„ ์•Œ์•„์„œ ๋ฒ„์ „์„ ๋งž์ถฐ์ค๋‹ˆ๋‹ค.
  "packageManager": "pnpm@9.15.0",
  
  "engines": {
    "node": ">=20.0.0", // ๐Ÿฆ Node 20 ๋ฏธ๋งŒ์—์„œ๋Š” ์„ค์น˜ ์ž์ฒด๋ฅผ ๋ง‰์•„์„œ "๋‚ด ์ปดํ“จํ„ฐ์—์„  ๋˜๋Š”๋ฐ?" ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
    "pnpm": ">=9.0.0"
  },
  
  "scripts": {
    // ๐Ÿ’ก ํ‰์†Œ ์ž์ฃผ ์“ฐ๋Š” ์‹คํ–‰ ๋ช…๋ น์–ด๋“ค
    "dev": "next dev --turbo", // ์†๋„๊ฐ€ ๋น ๋ฅธ ํ„ฐ๋ณดํŒฉ์œผ๋กœ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰
    "build": "next build", // ์‹ค์ œ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ (์ด๋•Œ standalone ํŒŒ์ผ์ด ์ƒ์„ฑ๋จ)
    "start": "next start",
 
    // ๐Ÿ’ก ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ ๋ช…๋ น์–ด
    "lint": "next lint",
    "lint:fix": "next lint --fix",
    "typecheck": "tsc --noEmit", // ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์—๋Ÿฌ๋งŒ ๊ฒ€์ถœ (๋นŒ๋“œ ์ „ CI์—์„œ ๋Œ๋ฆฌ๊ธฐ ์ข‹์Œ)
    "format": "prettier --write .",
 
    // ๐Ÿ’ก DB ๋ช…๋ น์–ด (Prisma)
    "db:generate": "prisma generate", // DB ์Šคํ‚ค๋งˆ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ํƒ€์ž… ์ž๋™์™„์„ฑ ์ƒ์„ฑ
    "db:migrate": "prisma migrate dev", // ๋กœ์ปฌ DB์— ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ ์šฉ
    "db:migrate:prod": "prisma migrate deploy", // (์ค‘์š”) ์‹ค์ œ ์šด์˜ ์„œ๋ฒ„ ๋ฐฐํฌ ์‹œ ์‹คํ–‰ํ•˜๋Š” DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
 
    // ๐Ÿฆ [ํ•ต์‹ฌ] install ์งํ›„ ์ž๋™ ์‹คํ–‰: ๋นˆ๋ฒˆํ•œ 'ํƒ€์ž… ์—๋Ÿฌ'๋ฅผ ๋ง‰์•„์ค๋‹ˆ๋‹ค.
    "postinstall": "prisma generate"
  },
  
  // ๐Ÿ‘‰ ์‹ค์ œ ๋Ÿฐํƒ€์ž„(์šด์˜ ์„œ๋ฒ„)์—์„œ ํ•„์š”ํ•œ ํ•„์ˆ˜ ํŒจํ‚ค์ง€๋“ค
  "dependencies": {
    "next": "^14.2.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@prisma/client": "^5.9.1",
    "@tanstack/react-query": "^5.17.15",
    "axios": "^1.6.7",
    "zod": "^3.22.4", // ํผ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ์šฉ
    "next-auth": "^4.24.5", // ๋กœ๊ทธ์ธ ๋“ฑ ์ธ์ฆ ๊ด€๋ฆฌ
    "clsx": "^2.1.0",
    "tailwind-merge": "^2.2.1"
  },
  
  // ๐Ÿ‘‰ ๊ฐœ๋ฐœํ•  ๋•Œ๋งŒ ํ•„์š”ํ•˜๊ณ  ์šด์˜ ๋นŒ๋“œ ์‹œ์—๋Š” ๋น ์ง€๋Š” ํŒจํ‚ค์ง€๋“ค (์šฉ๋Ÿ‰ ์ตœ์ ํ™”์˜ ํ•ต์‹ฌ!)
  "devDependencies": {
    // pnpm์€ npm๊ณผ ๋‹ฌ๋ฆฌ devDependencies๋ฅผ ์ฒ ์ €ํ•˜๊ฒŒ ๊ฒฉ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    // ๊ทธ๋ž˜์„œ ์ด๊ณณ์— ์žˆ๋Š” ํŒจํ‚ค์ง€๊ฐ€ dependencies ์ธ ์ฒ™ ์ž‘๋™ํ•˜๋Š” ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. (์œ ๋ น ์˜์กด์„ฑ ๋ฐฉ์ง€)
    "prisma": "^5.9.1",
    "typescript": "^5.3.3",
    "@types/node": "^20.11.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "tailwindcss": "^3.4.1",
    "postcss": "^8.4.35",
    "autoprefixer": "^10.4.17",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.0",
    "@typescript-eslint/eslint-plugin": "^7.0.2",
    "@typescript-eslint/parser": "^7.0.2",
    "prettier": "^3.2.5",
    "prettier-plugin-tailwindcss": "^0.5.11",
    "jest": "^29.7.0",
    "@testing-library/react": "^14.2.1",
    "@testing-library/jest-dom": "^6.4.2",
    "jest-environment-jsdom": "^29.7.0",
    "tsx": "^4.7.1",
    "husky": "^9.0.10",
    "lint-staged": "^15.2.2"
  },
  
  // Git Commit ํ•˜๊ธฐ ์ „์— ๋ณ€๊ฒฝ๋œ ํŒŒ์ผ๋งŒ ์ž๋™์œผ๋กœ Lint & Prettier ๋Œ๋ ค์ฃผ๋Š” ์„ค์ •
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml,yml}": ["prettier --write"]
  }
}

โš™๏ธ pnpm ์ „์šฉ .npmrc ์ตœ์ ํ™”

# .npmrc (ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ โ€” git ์— ์ปค๋ฐ‹)
 
# ๐Ÿฆ TypeScript, ESLint ๊ณ„์—ด์€ ๋ฃจํŠธ๋กœ ํ˜ธ์ด์ŠคํŒ…
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=postcss
public-hoist-pattern[]=tailwindcss
public-hoist-pattern[]=autoprefixer
 
# ์—”์ง„ ๋ฒ„์ „ ๊ฐ•์ œ (์ž˜๋ชป๋œ Node.js ๋ฒ„์ „์œผ๋กœ ์„ค์น˜ ์‹œ ์—๋Ÿฌ)
engine-strict=true
 
# ๋ณด์•ˆ: audit ํ•ญ์ƒ ํ™œ์„ฑํ™”
audit=true
 
# ๐Ÿฆ lock ํŒŒ์ผ ์—†์œผ๋ฉด ์—๋Ÿฌ (์‹ค์ˆ˜๋กœ npm install ๋ฐฉ์ง€)
# (pnpm ci = pnpm install --frozen-lockfile ์œผ๋กœ ์ฒ˜๋ฆฌ)
 
# ์ €์žฅ์†Œ ์บ์‹œ ๊ฒฝ๋กœ ๋ช…์‹œ (์„ ํƒ ์‚ฌํ•ญ)
# store-dir=~/.pnpm-store

๐Ÿณ Docker ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ โ€” BuildKit ์บ์‹œ ํ™œ์šฉ

๋‹จ์ผ ์•ฑ Dockerfile

# -----------------------------------------------------------
# [๊ธฐ์ดˆ ์ƒ์‹] ๐Ÿณ ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋ž€?
# Docker ๋นŒ๋“œ ๊ณผ์ •์„ ์—ฌ๋Ÿฌ ๋‹จ๊ณ„(Stage)๋กœ ๋‚˜๋ˆ„์–ด, ์ตœ์ข… ์ด๋ฏธ์ง€์—๋Š” ์‹คํ–‰์— ํ•„์š”ํ•œ ์—‘๊ธฐ์Šค๋งŒ ๋‚จ๊ธฐ๊ณ  ๋ถˆํ•„์š”ํ•œ ๋นŒ๋“œ ๋„๊ตฌ๋ฅผ ์‹น ๋ฒ„๋ฆฌ๋Š”(๊ฒฝ๋Ÿ‰ํ™”) ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค.
# -----------------------------------------------------------
 
# ๐Ÿฆ base ๋‹จ๊ณ„: ๊ณตํ†ต ํ™˜๊ฒฝ ์„ค์ • (๋‹ค๋ฅธ ๋‹จ๊ณ„๋“ค์ด ์ด base๋ฅผ ๊ฐ€์ ธ๋‹ค ์”๋‹ˆ๋‹ค)
FROM node:20-slim AS base
# ๐Ÿ’ก '-slim' ์ด ๋ถ™์€ ์ด๋ฏธ์ง€๋Š” ์šด์˜์ฒด์ œ์˜ ๋ถˆํ•„์š”ํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ฑฐํ•˜์—ฌ ์šฉ๋Ÿ‰์ด ์•„์ฃผ ๊ฐ€๋ณ์Šต๋‹ˆ๋‹ค (์•ฝ 900MB -> 200MB)
 
# pnpm ์‚ฌ์šฉ์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์„ค์ •
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# Node.js ์— ๋‚ด์žฅ๋œ ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ๊ด€๋ฆฌ ๋„๊ตฌ์ธ corepack ์„ ์ผœ์„œ pnpm ์„ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
RUN corepack enable
 
# -----------------------------------------------------------
# Stage 1: prod-deps (์šด์˜ ๋ฐฐํฌ์šฉ ์˜์กด์„ฑ๋งŒ ์„ค์น˜)
# -----------------------------------------------------------
FROM base AS prod-deps
 
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
 
# ๐Ÿฆ [์ดˆํ•ต์‹ฌ] BuildKit ์บ์‹œ ๋งˆ์šดํŠธ (--mount=type=cache)
# ์ผ๋ฐ˜์ ์ธ Docker ๋นŒ๋“œ๋Š” ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ๋‹ค์šด๋กœ๋“œ๋ถ€ํ„ฐ ๋‹ค์‹œ ํ•ฉ๋‹ˆ๋‹ค.
# ํ•˜์ง€๋งŒ ์ด ์˜ต์…˜์„ ์“ฐ๋ฉด, pnpm์ด ํŒจํ‚ค์ง€๋ฅผ ์ž„์‹œ ์ €์žฅํ•˜๋Š” ๊ธ€๋กœ๋ฒŒ ํด๋”(/pnpm/store)๋ฅผ Docker ์™ธ๋ถ€์— ์•ˆ์ „ํ•˜๊ฒŒ ์บ์‹ฑํ•ด๋‘ก๋‹ˆ๋‹ค.
# ๋‹ค์Œ ๋ฒˆ ๋นŒ๋“œํ•  ๋•Œ๋Š” ๋‹ค์šด๋กœ๋“œ ์—†์ด ์ฆ‰์„์—์„œ 0.1์ดˆ๋งŒ์— ํ•˜๋“œ๋งํฌ๋งŒ ๊ฑธ์–ด์ฃผ์–ด ๋นŒ๋“œ ์†๋„๊ฐ€ ํš๊ธฐ์ ์œผ๋กœ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค!
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm install --prod --frozen-lockfile 
    # `--prod`: devDependencies(ํ…Œ์ŠคํŠธ ๋„๊ตฌ, ๋ฆฐํŠธ ๋“ฑ)๋ฅผ ์ œ์™ธํ•˜๊ณ  ๊ฐ€๋ณ๊ฒŒ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.
    # `--frozen-lockfile`: lock ํŒŒ์ผ์„ ๋ฌด์กฐ๊ฑด ์‹ ๋ขฐํ•˜๊ณ , ๋ฒ„์ „์ด ๋‹ฌ๋ผ์ง€๋ฉด ์—๋Ÿฌ๋ฅผ ๋ƒ…๋‹ˆ๋‹ค. (์šด์˜ ๋ฐฐํฌ ์‹œ ํ•„์ˆ˜)
 
# -----------------------------------------------------------
# Stage 2: build (์ „์ฒด ์†Œ์Šค์ฝ”๋“œ ๋นŒ๋“œ)
# -----------------------------------------------------------
FROM base AS build
 
WORKDIR /app
COPY package.json pnpm-lock.yaml .npmrc ./
 
# ๋นŒ๋“œํ•  ๋•Œ๋Š” ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ๋“ฑ devDependencies ๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ `--prod` ์—†์ด ํ’€(full) ์„ค์น˜๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm install --frozen-lockfile
 
# ์†Œ์Šค์ฝ”๋“œ๋ฅผ ์‹น ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
COPY . .
# Next.js ๋นŒ๋“œ๋ฅผ ๋Œ๋ฆฝ๋‹ˆ๋‹ค. (๊ฒฐ๊ณผ๋ฌผ์€ .next ํด๋”์— ์ƒ๊น€)
RUN pnpm run build
 
# -----------------------------------------------------------
# Stage 3: runner (์ตœ์ข… ์‹คํ–‰ ์ด๋ฏธ์ง€ - ์ •๋ง ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ๋‹ด์Šต๋‹ˆ๋‹ค)
# -----------------------------------------------------------
FROM base AS runner
 
WORKDIR /app
 
ENV NODE_ENV=production
# Next.js ์‚ฌ์šฉ ํ†ต๊ณ„ ์ „์†ก์„ ๋•๋‹ˆ๋‹ค. (์‚ด์ง ๋นจ๋ผ์ง)
ENV NEXT_TELEMETRY_DISABLED=1
 
# ๐Ÿฆ [๋ณด์•ˆ ํ•„์ˆ˜] root ๊ถŒํ•œ์„ ์“ฐ์ง€ ์•Š๊ธฐ ์œ„ํ•ด nextjs ๋ผ๋Š” ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ๊ณ„์ •์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
 
# Stage 1 (prod-deps) ์—์„œ ๊ฐ€๋ณ๊ฒŒ ์„ค์น˜ํ•ด ๋‘” ์™„์ „์ฒด ์šด์˜์šฉ node_modules ๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
COPY --from=prod-deps /app/node_modules ./node_modules
 
# Stage 2 (build) ์—์„œ ๋นŒ๋“œ๋œ ๊ฒฐ๊ณผ๋ฌผ์„ ์ทจํ•ฉํ•ฉ๋‹ˆ๋‹ค.
# ๐Ÿ’ก Next.js ์˜ standalone ๋ชจ๋“œ๋Š” "์‹คํ–‰์— ๊ผญ ํ•„์š”ํ•œ ํŒŒ์ผ๋“ค๋งŒ" ๋ชจ์•„์ฃผ๋Š” ๊ฟ€ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. (next.config.js ์„ค์ • ํ•„์š”)
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
 
# ๋ฐฉ๊ธˆ ๋งŒ๋“  ์•ˆ์ „ํ•œ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ๊ณ„์ •์œผ๋กœ ์ „ํ™˜
USER nextjs
 
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
 
# ์ตœ์ข… ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ๋ช…๋ น์–ด (standalone ์‚ฐ์ถœ๋ฌผ ์‹คํ–‰)
CMD ["node", "server.js"]

.dockerignore:

node_modules
.next
.git
.gitignore
*.md
.env
.env.*
!.env.example

next.config.js ์—์„œ standalone ์ถœ๋ ฅ ํ™œ์„ฑํ™”:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // ๐Ÿฆ standalone ๋ชจ๋“œ: ์‹คํ–‰์— ํ•„์š”ํ•œ ํŒŒ์ผ๋งŒ .next/standalone ์— ๋ชจ์Œ
  // โ†’ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋Œ€ํญ ๊ฐ์†Œ
  output: 'standalone',
};
 
module.exports = nextConfig;

๋ชจ๋…ธ๋ ˆํฌ โ€” pnpm deploy ํ™œ์šฉ

๋ชจ๋…ธ๋ ˆํฌ์—์„œ ํŠน์ • ์•ฑ๋งŒ Docker ์ด๋ฏธ์ง€๋กœ ๋งŒ๋“ค ๋•Œ pnpm deploy ๊ฐ€ ๊ฐ•๋ ฅํ•˜๋‹ค.

# Dockerfile (๋ชจ๋…ธ๋ ˆํฌ ๋ฃจํŠธ)
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
 
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Stage 1: ์ „์ฒด ๋นŒ๋“œ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM base AS build
 
COPY . /usr/src/app
WORKDIR /usr/src/app
 
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
    pnpm install --frozen-lockfile
 
# ๋ชจ๋“  ํŒจํ‚ค์ง€ ๋นŒ๋“œ (์˜์กด ์ˆœ์„œ ์ž๋™ ์ฒ˜๋ฆฌ)
RUN pnpm run -r build
 
# ๐Ÿฆ pnpm deploy โ€” community ์•ฑ์— ํ•„์š”ํ•œ ํŒŒ์ผ๋งŒ /prod/community ๋กœ ๋ถ„๋ฆฌ
# --prod: devDependencies ์ œ์™ธ
# --filter: ๋Œ€์ƒ ์•ฑ ์ง€์ •
RUN pnpm deploy --filter=@youngsu/community --prod /prod/community
RUN pnpm deploy --filter=@youngsu/admin --prod /prod/admin
 
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Stage 2: community ์•ฑ ์‹คํ–‰ ์ด๋ฏธ์ง€
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM base AS community
 
COPY --from=build /prod/community /prod/community
WORKDIR /prod/community
 
EXPOSE 3000
CMD ["pnpm", "start"]
 
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Stage 3: admin ์•ฑ ์‹คํ–‰ ์ด๋ฏธ์ง€
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM base AS admin
 
COPY --from=build /prod/admin /prod/admin
WORKDIR /prod/admin
 
EXPOSE 3001
CMD ["pnpm", "start"]
# ์•ฑ๋ณ„ ์ด๋ฏธ์ง€ ๋นŒ๋“œ
docker build . --target community --tag youngsu-community:latest
docker build . --target admin --tag youngsu-admin:latest

๐Ÿ”„ GitHub Actions โ€” pnpm ์ตœ์ ํ™” ํŒŒ์ดํ”„๋ผ์ธ

๋‹จ์ผ ์•ฑ CI/CD

# .github/workflows/ci.yml
name: CI
 
# ๐Ÿ’ก ์ด ํŒŒ์ดํ”„๋ผ์ธ์ด ์–ธ์ œ ์‹คํ–‰๋ ์ง€ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. (๋ฐฉ์•„์‡  ์—ญํ• )
on:
  push:
    branches: [main, develop] # main์ด๋‚˜ develop ๋ธŒ๋žœ์น˜์— ์ฝ”๋“œ๊ฐ€ ํ‘ธ์‹œ๋  ๋•Œ
  pull_request:
    branches: [main] # ์ฝ”๋“œ๋ฅผ main์œผ๋กœ ํ•ฉ์ณ๋‹ฌ๋ผ(PR)๊ณ  ์š”์ฒญํ–ˆ์„ ๋•Œ
 
jobs:
  ci:
    runs-on: ubuntu-latest # ๐Ÿ’ก ์ž„๋Œ€๋ฐ›์€ ๊ฐ€์žฅ ์ตœ์‹ ๋ฒ„์ „ ๋ฆฌ๋ˆ…์Šค ๊นกํ†ต ์ปดํ“จํ„ฐ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
    strategy:
      matrix:
        node-version: [20] # Node.js 20๋ฒ„์ „์—์„œ ํ…Œ์ŠคํŠธ
 
    steps:
      # 1๏ธโƒฃ GitHub์—์„œ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ๋ฅผ ์šฐ๋ถ„ํˆฌ ์ปดํ“จํ„ฐ๋กœ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์Šต๋‹ˆ๋‹ค.
      - uses: actions/checkout@v4
 
      # 2๏ธโƒฃ ๐Ÿฆ [ํ•ต์‹ฌ] ์ปดํ“จํ„ฐ์— pnpm์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. (pnpm ๊ณต์‹ Action ์‚ฌ์šฉ)
      # ๋ฒ„์ „ ๋ช…์‹œ๋ฅผ ์ƒ๋žตํ•˜๋ฉด package.json ์˜ "packageManager": "pnpm@9.x.x" ํ•„๋“œ๋ฅผ ์ฝ์–ด๋‹ค ์ž๋™์œผ๋กœ ๊น”์•„์ค๋‹ˆ๋‹ค! ์ง„์งœ ํŽธํ•จ!
      - name: Setup pnpm
        uses: pnpm/action-setup@v4
 
      # 3๏ธโƒฃ ์ปดํ“จํ„ฐ์— Node.js ๋ฅผ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค.
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          # ๐Ÿฆ [๊ทน๊ฐ•์˜ ์ตœ์ ํ™” ํ•ต์‹ฌ] cache: 'pnpm' ๋‹จ ํ•œ ์ค„!
          # ์ด ํ•œ ์ค„์„ ์ ์œผ๋ฉด "GitHub ์ €์žฅ์†Œ ์–ด๋”˜๊ฐ€์— ๊ธฐ์กด pnpm-store๋ฅผ ์••์ถ•ํ•ด๋’€๋‹ค๊ฐ€ ๋‹ค์Œ ๋นŒ๋“œ์— ์žฌ์‚ฌ์šฉ" ํ•ฉ๋‹ˆ๋‹ค.
          # pnpm install์ด 2๋ถ„์—์„œ 10์ดˆ๋กœ ์ค„์–ด๋“œ๋Š” ๊ธฐ์ ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
          cache: 'pnpm'
 
      # 4๏ธโƒฃ ์˜์กด์„ฑ ์„ค์น˜ (๋ฌผ๋ก  ์œ„์—์„œ ์บ์‹œํ•ด๋‘” ๊ฒŒ ์žˆ๋‹ค๋ฉด ํ•˜๋“œ๋งํฌ ๊ต์ฒด๋งŒ์œผ๋กœ ๋ฒˆ๊ฐœ๊ฐ™์ด ๋๋‚จ)
      - name: Install dependencies
        run: pnpm install --frozen-lockfile # lock ํŒŒ์ผ์„ ์ ˆ๋Œ€ ์‹ ๋ขฐ. ๋ฒ„์ „ ๋‹ฌ๋ผ์ง€๋ฉด ์—๋Ÿฌ!
 
      # 5๏ธโƒฃ ๋นŒ๋“œ ์ „ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ๋ฌธ๋ฒ• ์—๋Ÿฌ๊ฐ€ ์—†๋Š”์ง€ ์ฒดํฌ
      - name: Type check
        run: pnpm typecheck
 
      # 6๏ธโƒฃ ์ฝ”๋“œ ์Šคํƒ€์ผ์ด๋‚˜ ๋ฃฐ์„ ์ž˜ ์ง€์ผฐ๋Š”์ง€ ๋ฆฐํŠธ(์ž”์†Œ๋ฆฌ๋จธ์‹ ) ์ฒดํฌ
      - name: Lint
        run: pnpm lint
 
      # 7๏ธโƒฃ ์ž‘์„ฑํ•ด๋‘” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธ
      - name: Test
        run: pnpm test:ci
 
      # 8๏ธโƒฃ ์•„๋ฌด ๋ฌธ์ œ ์—†๋‹ค๋ฉด ์ •๋ง๋กœ ๋นŒ๋“œ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค!
      - name: Build
        run: pnpm build
        env: # ๋นŒ๋“œํ•  ๋•Œ ํ•„์š”ํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ, Key ๊ฐ™์€ ๊ฒƒ๋“ค์€ GitHub Secrets ์ฐฝ๊ณ ์—์„œ ๊บผ๋‚ด์˜ต๋‹ˆ๋‹ค.
          NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
          NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

๋ชจ๋…ธ๋ ˆํฌ CI โ€” ๋ณ€๊ฒฝ๋œ ํŒจํ‚ค์ง€๋งŒ ๋นŒ๋“œ

# .github/workflows/ci-monorepo.yml
name: Monorepo CI
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      community: ${{ steps.filter.outputs.community }}
      admin: ${{ steps.filter.outputs.admin }}
      ui: ${{ steps.filter.outputs.ui }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            community:
              - 'apps/community/**'
              - 'packages/ui/**'
              - 'packages/types/**'
            admin:
              - 'apps/admin/**'
              - 'packages/ui/**'
            ui:
              - 'packages/ui/**'
 
  test-community:
    needs: changes
    if: ${{ needs.changes.outputs.community == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter @youngsu/community typecheck
      - run: pnpm --filter @youngsu/community build
 
  test-admin:
    needs: changes
    if: ${{ needs.changes.outputs.admin == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter @youngsu/admin build

๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ

# .github/workflows/deploy.yml
name: Deploy
 
on:
  push:
    branches: [main]
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: pnpm/action-setup@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
 
      - name: Install
        run: pnpm install --frozen-lockfile
 
      # Prisma ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (ํ”„๋กœ๋•์…˜ DB)
      - name: Run migrations
        run: pnpm db:migrate:prod
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
 
      # Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ & push
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/youngsu/community:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

๐Ÿ’พ pnpm store ์บ์‹œ ์ „๋žต

GitHub Actions ์บ์‹œ ๋™์ž‘ ์›๋ฆฌ

- uses: actions/setup-node@v4
  with:
    cache: 'pnpm'
    # ๋‚ด๋ถ€์ ์œผ๋กœ ์•„๋ž˜๋ฅผ ์ž๋™ ์ฒ˜๋ฆฌ:
    # 1. pnpm store path ์‹คํ–‰ โ†’ ๊ฒฝ๋กœ ํ™•์ธ
    # 2. pnpm-lock.yaml ํ•ด์‹œ๋ฅผ ์บ์‹œ ํ‚ค๋กœ ์‚ฌ์šฉ
    # 3. ์บ์‹œ ํžˆํŠธ ์‹œ store ๋ณต์› โ†’ pnpm install ์ด ํ•˜๋“œ๋งํฌ๋งŒ ์ƒ์„ฑ
    # 4. ์บ์‹œ ๋ฏธ์Šค ์‹œ ์„ค์น˜ ํ›„ store ์ €์žฅ

์บ์‹œ ํšจ์œจ ๊ทน๋Œ€ํ™”

# pnpm-lock.yaml ์ด ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ PR ์—์„œ์˜ install ์‹œ๊ฐ„
# ์บ์‹œ ์—†์Œ: ~120์ดˆ
# ์บ์‹œ ์žˆ์Œ: ~15์ดˆ  (์Šคํ† ์–ด ๋ณต์› + ํ•˜๋“œ๋งํฌ ์ƒ์„ฑ)
 
# ๐Ÿฆ ์บ์‹œ ํ‚ค ์ „๋žต
# setup-node cache: 'pnpm' ์ด ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ๋ณ„๋„ ์„ค์ • ๋ถˆํ•„์š”
# ๋‹จ, ์บ์‹œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์ œ์–ดํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด:
- name: Get pnpm store directory
  id: pnpm-cache
  run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
 
- uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.dir }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-store-

CI ํ™˜๊ฒฝ์—์„œ pnpm ๋ณด์•ˆ ์„ค์ •

- name: Security audit
  run: pnpm audit --audit-level=high
 
- name: Check for unused dependencies
  run: pnpm dlx depcheck

๐Ÿ ์ด์ •๋ฆฌ

pnpm + Next.js + CI/CD ํ•ต์‹ฌ ํŒจํ„ด
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
Docker:   node:20-slim + BuildKit --mount=type=cache + output:standalone
๋ชจ๋…ธ๋ ˆํฌ:  pnpm deploy --filter=์•ฑ --prod ๋กœ ์•ฑ๋ณ„ ๋…๋ฆฝ ์ด๋ฏธ์ง€
CI ์„ค์น˜:  pnpm/action-setup + setup-node cache:'pnpm'
CI ์‹คํ–‰:  pnpm install --frozen-lockfile (lock ํŒŒ์ผ ๊ธฐ์ค€)
๋ฐฐํฌ:     pnpm db:migrate:prod ํ›„ Docker push
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
ํ•ญ๋ชฉnpm ๋ฐฉ์‹pnpm ๋ฐฉ์‹
CI install~120์ดˆ~15์ดˆ (์บ์‹œ ์‹œ)
Docker ๋นŒ๋“œ๋งค๋ฒˆ ์ „์ฒด ๋‹ค์šด๋กœ๋“œBuildKit ์บ์‹œ๋กœ ์Šคํ† ์–ด ์žฌ์‚ฌ์šฉ
์ด๋ฏธ์ง€ ํฌ๊ธฐ1GB+400MB ์ดํ•˜ (standalone + slim)
๋ชจ๋…ธ๋ ˆํฌ ๋ถ„๋ฆฌ์ˆ˜๋™ ๋ณต์‚ฌpnpm deploy --filter

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. Next.js Docker ์ด๋ฏธ์ง€์—์„œ output: 'standalone' ๊ณผ ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“œ๋Š” ์›๋ฆฌ๋Š”?

โœ… ์ •๋‹ต: output: 'standalone' ์€ Next.js ๊ฐ€ .next/standalone ์— ์‹คํ–‰์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ํŒŒ์ผ๋งŒ ๋ณต์‚ฌํ•œ๋‹ค. node_modules ์ „์ฒด ๋Œ€์‹  ์‹ค์ œ ์‚ฌ์šฉ๋˜๋Š” ํŒŒ์ผ๋งŒ ํฌํ•จํ•˜๊ณ  server.js ํ•˜๋‚˜๋กœ ์„œ๋ฒ„๋ฅผ ๊ตฌ๋™ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ์—์„œ ์ตœ์ข… ์ด๋ฏธ์ง€๋Š” ์ด standalone ํด๋”๋งŒ ๋ณต์‚ฌ ํ•˜๋ฏ€๋กœ devDependencies ๋Š” ๋ฌผ๋ก , ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” dependencies ํŒŒ์ผ๋„ ์ด๋ฏธ์ง€์— ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์ผ๋ฐ˜ Next.js ๋นŒ๋“œ: ์ „์ฒด node_modules (~400MB) ํ•„์š”
  • standalone ๋นŒ๋“œ: ํ•„์š” ํŒŒ์ผ๋งŒ (~50-100MB)
  • node:20-slim ์‚ฌ์šฉ: ํ’€ node:20 (~900MB) ๋Œ€์‹  slim (~200MB)
  • ์ตœ์ข… ์ด๋ฏธ์ง€ = slim base + standalone ํŒŒ์ผ โ‰ˆ 300-400MB
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "standalone = Next.js ๊ฐ€ ์ง์ ‘ ์†Œ์ง์„ ์‹ธ์คŒ โ€” ๊ผญ ํ•„์š”ํ•œ ๊ฒƒ๋งŒ."

Q2. pnpm/action-setup ์—์„œ version ์„ ์ƒ๋žตํ•˜๋ฉด pnpm ๋ฒ„์ „์ด ์–ด๋–ป๊ฒŒ ๊ฒฐ์ •๋˜๋Š”๊ฐ€?

โœ… ์ •๋‹ต: package.json ์˜ packageManager ํ•„๋“œ ์—์„œ ์ž๋™์œผ๋กœ ๋ฒ„์ „์„ ์ฝ๋Š”๋‹ค. "packageManager": "pnpm@9.15.0" ์ฒ˜๋Ÿผ ์ •ํ™•ํ•œ ๋ฒ„์ „์ด ๋ช…์‹œ๋˜์–ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ฒ„์ „์˜ pnpm ์„ ์„ค์น˜ํ•œ๋‹ค. ์ด ๋ฐฉ์‹์ด ๋ฒ„์ „์„ workflow ํŒŒ์ผ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚ซ๋‹ค โ€” packageManager ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด CI ๋„ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ๊ฐ„๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • packageManager ํ•„๋“œ: Node.js Corepack ํ‘œ์ค€ (v16.13+)
  • workflow ์—์„œ with: version: 9 ์ฒ˜๋Ÿผ ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋ฉด ๋ฒ„์ „ ๋ถˆ์ผ์น˜ ์œ„ํ—˜
  • packageManager + pnpm/action-setup ์กฐํ•ฉ์ด ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "packageManager ๊ฐ€ pnpm ๋ฒ„์ „์˜ ์ง„์‹ค, workflow ๋Š” ๊ทธ๊ฑธ ๋”ฐ๋ผ๊ฐ„๋‹ค."

Q3. ์˜์ฒ ์ด์˜ ํ…Œ์ŠคํŠธ ํƒ€์ž„: ๊ธด๊ธ‰ ๋””๋ฒ„๊น… (์˜์ˆ˜์˜ ํ˜ธํ†ต)

์˜์ˆ˜ ๋‹˜: "์˜์ฒ  ๋‹˜, ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ์ด ๊ฐ‘์ž๊ธฐ 3๋ฐฐ๋‚˜ ๋А๋ ค์กŒ์–ด์š”! ์–ด์ œ๊นŒ์ง€ 2๋ถ„์ด์—ˆ๋Š”๋ฐ ์˜ค๋Š˜์€ 6๋ถ„ ๋„˜๊ฒŒ ๊ฑธ๋ ค์š”. pnpm store ์บ์‹œ๊ฐ€ ๋‚ ์•„๊ฐ„ ๊ฑด๊ฐ€์š”?"

CI ๋กœ๊ทธ๋ฅผ ๋ณด๋‹ˆ pnpm install ์ด Cache miss ๋กœ ์ถœ๋ ฅ๋˜๊ณ  ์žˆ์—ˆ๋‹ค.

์บ์‹œ ๋ฏธ์Šค์˜ ๊ฐ€๋Šฅํ•œ ์›์ธ๊ณผ ๊ฐ ์›์ธ๋ณ„ ํ•ด๊ฒฐ์ฑ…์€?

โœ… ์ •๋‹ต: ๊ฐ€์žฅ ํ”ํ•œ ์›์ธ์€ pnpm-lock.yaml ์ด ๋ณ€๊ฒฝ๋œ ๊ฒƒ ์ด๋‹ค. ์บ์‹œ ํ‚ค๊ฐ€ pnpm-lock.yaml ์˜ ํ•ด์‹œ ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ, ์ƒˆ ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ์บ์‹œ ๋ฏธ์Šค๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋‘ ๋ฒˆ์งธ ์›์ธ์€ restore-keys ์„ค์ • ์—†์ด ์ •ํ™•ํ•œ ํ‚ค ๋งค์นญ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค:

  • ์›์ธ 1 (์ •์ƒ): ๋ˆ„๊ตฐ๊ฐ€ pnpm add ํ•ด์„œ pnpm-lock.yaml ๋ณ€๊ฒฝ โ†’ ์ฒซ ์‹คํ–‰์€ ๋ฏธ์Šค, ์ดํ›„ ํžˆํŠธ
  • ์›์ธ 2: restore-keys ์—†์Œ โ†’ ์ด์ „ lock ํŒŒ์ผ ๋ฒ„์ „ ์บ์‹œ๋ฅผ fallback ์œผ๋กœ ๋ชป ์”€
    • ํ•ด๊ฒฐ: restore-keys: ${{ runner.os }}-pnpm-store- ์ถ”๊ฐ€
  • ์›์ธ 3: ์บ์‹œ ์ €์žฅ ์šฉ๋Ÿ‰ ์ดˆ๊ณผ (GitHub Actions ๋Š” ์ €์žฅ์†Œ๋‹น 10GB ์ œํ•œ)
    • ํ•ด๊ฒฐ: pnpm store prune ์œผ๋กœ ์˜ค๋ž˜๋œ ์บ์‹œ ์ •๋ฆฌ
  • ์›์ธ 4: setup-node ์˜ cache: 'pnpm' ์˜ต์…˜ ๋ˆ„๋ฝ
  • ๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "์บ์‹œ ๋ฏธ์Šค โ†’ lock ํŒŒ์ผ ๋ณ€๊ฒฝ ํ™•์ธ โ†’ restore-keys ์žˆ๋Š”์ง€ ํ™•์ธ โ†’ ์šฉ๋Ÿ‰ ํ™•์ธ ์ˆœ์„œ๋กœ."

๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

๋“œ๋””์–ด Docker ์™€ GitHub Actions ํŒŒ์ดํ”„๋ผ์ธ์„ pnpm ์— ๋งž๊ฒŒ ์ตœ์ ํ™”ํ–ˆ๋‹ค.

์†”์งํžˆ ์ฒ˜์Œ์— --mount=type=cache,id=pnpm,target=/pnpm/store ์ด ๊ธด ์ค„์ด ๋ญ”์ง€ ๋ชฐ๋ผ์„œ ๊ทธ๋ƒฅ ๋ณต๋ถ™ํ–ˆ๋Š”๋ฐ, BuildKit ์บ์‹œ ๋งˆ์šดํŠธ๋ผ๋Š” ๊ฐœ๋…์„ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋‹ˆ๊นŒ "์˜ค, ์ด๊ฒŒ Docker ๋ ˆ์ด์–ด ์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ณ„๋„ ์บ์‹œ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์“ฐ๋Š” ๊ฑฐ๊ตฌ๋‚˜" ํ•˜๊ณ  ์ดํ•ด๋๋‹ค. ๋นŒ๋“œ ๋ ˆ์ด์–ด๊ฐ€ ๋ฐ”๋€Œ์–ด๋„ pnpm ์Šคํ† ์–ด๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€๋˜๋‹ˆ๊นŒ pnpm install ์ด ํ•˜๋“œ๋งํฌ๋งŒ ์ƒ์„ฑํ•˜๋ฉด ๋˜๋Š” ๊ฑฐ์˜€๋‹ค.

CI ์—์„œ cache: 'pnpm' ํ•œ ์ค„์ด ์ž๋™์œผ๋กœ pnpm store ๊ฒฝ๋กœ ์ฐพ๊ณ , lock ํŒŒ์ผ ํ•ด์‹œ๋กœ ์บ์‹œ ํ‚ค ๋งŒ๋“ค๊ณ , ๋ณต์›ํ•˜๊ณ , ์ €์žฅํ•˜๋Š” ๊ฑธ ๋‹ค ํ•ด์ค€๋‹ค๋Š” ๊ฒŒ ์‹ ๊ธฐํ–ˆ๋‹ค. ์˜ˆ์ „์— npm ์“ธ ๋•Œ ์บ์‹œ ์„ค์ •ํ•˜๋А๋ผ workflow ํŒŒ์ผ์— 20์ค„์”ฉ ์ผ๋˜ ๊ธฐ์–ต์ด ๋‚˜๋Š”๋ฐ.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "pnpm ์˜ content-addressable store ๋Š” CI ์™€ Docker ์—์„œ ๋น›๋‚œ๋‹ค. ์บ์‹œ๊ฐ€ ์‚ด์•„์žˆ์œผ๋ฉด ์„ค์น˜๊ฐ€ ํ•˜๋“œ๋งํฌ ์ƒ์„ฑ์œผ๋กœ๋งŒ ๋๋‚œ๋‹ค โ€” ๊ทธ๊ฒŒ '๋น ๋ฅธ CI' ์˜ ๋น„๋ฐ€์ด๋‹ค."

๋‚ด์ผ์ด ๋งˆ์ง€๋ง‰ ์ฑ•ํ„ฐ์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ๋‹ค. ๊ธฐ์กด npm ํ”„๋กœ์ ํŠธ๋ฅผ pnpm ์œผ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ณผ์ •์ธ๋ฐ, ํŒ€์—์„œ ์‹ค์ œ๋กœ ํ•„์š”ํ•œ ๋‚ด์šฉ์ด๋ผ ๋” ์ง‘์ค‘ํ•ด์„œ ๋ด์•ผ๊ฒ ๋‹ค. ์˜ค๋Š˜ ์•ผ๊ทผ ์—†์ด ํ‡ด๊ทผํ•˜๋‹ˆ๊นŒ ๊ธฐ๋ถ„์ด ์ข‹๋‹ค. ์ง‘ ๊ฐ€์„œ ์ข‹์•„ํ•˜๋Š” ๋“œ๋ผ๋งˆ๋‚˜ ๋ด์•ผ์ง€.


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ