๐ Next.js + Docker + CI/CD ์ค์ โ pnpm ์ผ๋ก ํ๋ก๋์ ์์ฑ
๐ ๊ฐ์
Next.js 14 App Router ํ๋ก์ ํธ์์ pnpm ์ ํ์ฉํ ์์ฑํ package.json, BuildKit ์บ์๋ฅผ ์ด Docker ๋ฉํฐ์คํ ์ด์ง, GitHub Actions ํ์ดํ๋ผ์ธ๊น์ง
06. Next.js + CI/CD + Docker
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐ฆ Next.js 14 ์์ฑํ package.json
- โ๏ธ pnpm ์ ์ฉ .npmrc ์ต์ ํ
- ๐ณ Docker ๋ฉํฐ์คํ ์ด์ง
- ๐ GitHub Actions ํ์ดํ๋ผ์ธ
- ๐พ pnpm store ์บ์ ์ ๋ต
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: ์ฝ 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 ์ผ๋ก ์ฎ๊ธฐ๋ ๊ณผ์ ์ธ๋ฐ, ํ์์ ์ค์ ๋ก ํ์ํ ๋ด์ฉ์ด๋ผ ๋ ์ง์คํด์ ๋ด์ผ๊ฒ ๋ค. ์ค๋ ์ผ๊ทผ ์์ด ํด๊ทผํ๋๊น ๊ธฐ๋ถ์ด ์ข๋ค. ์ง ๊ฐ์ ์ข์ํ๋ ๋๋ผ๋ง๋ ๋ด์ผ์ง.