Turborepo 에서 next.js 도커라이징 하기
0. 프롤로그
이 글은 뮤즈라이브에서 웹 서비스 3개를 모노레포 구조에서 Turborepo의 공식 문서를 참조하여 Next.js의 standalone 모드와 Docker를 활용한 모노레포 환경 설정 및 최적화 방법을 설명합니다.
목표
- Next.js standalone 모드 설명
- Turborepo를 활용한 모노레포 구성 이해
- Docker를 통해 효율적인 최적화 및 배포 방법
1. Next.js standalone 란?
Next.js standalone 모드는 애플리케이션을 컴파일하여 런타임에 불필요한 파일을 제거하고, 최적화된 상태로 제공하는 기능입니다. 이 모드는 애플리케이션을 더 작고 독립적인 단위로 패키징하여, 서버 실행 환경(Docker 등)에서 경량화와 성능 향상을 도모할 수 있습니다.
주요 특징
- 최소화된 파일 크기: 번들링 과정에서
node_modules
및 불필요한 파일을 배제하고, 필수 파일들만 포함합니다. - 독립적인 실행 환경: 애플리케이션 실행을 위해 Next.js 소스 코드와 관련된 종속 파일만 필요합니다.
- 서버 배포 최적화: Docker와 같은 컨테이너 기반 시스템과 결합하여 보다 효과적인 배포가 가능해집니다.
- 빠른 로딩 및 적은 리소스 사용: 실행 시 필요한 최소한의 파일과 리소스만 포함되기 때문에 초기화 속도와 성능이 향상됩니다.
- CDN과 조합: 캐싱 기능 및 네트워크 요청 최적화를 통해 전 세계적으로 빠르고 안정적인 콘텐츠 제공이 가능합니다.
이 모드를 활용하면 Next.js 애플리케이션 배포 및 운영 관리를 더욱 효율적으로 할 수 있습니다.
2. 프로젝트 구조
📂 apps
├── 📂 web1
│ ├── 📄 package.json
├── 📂 web2
│ ├── 📄 package.json
📂 pakages
├── 📂 ui
│ ├── 📄 package.json
├── 📄 package.json
├── 📄 package-lock.json
프로젝트 구조 설명
apps
폴더는 실제 애플리케이션이 위치하는 디렉토리입니다. 여기에는 여러 개의 애플리케이션이 포함될 수 있으며, 이 예제에서는 web1, web2
애플리케이션이 있습니다.
packages
폴더는 재사용 가능한 패키지들이 위치하는 디렉토리입니다. 이곳에는 ui
같은 공통으로 사용 가능한 모듈이나 라이브러리가 포함됩니다.
-
apps/web
: 애플리케이션이 위치한 폴더로, Next.js 또는 다른 프레임워크 기반의 프로젝트가 포함될 수 있습니다.package.json
: 해당 애플리케이션의 종속성을 정의하는 파일입니다.
-
packages/ui
: 공유 가능한 UI 컴포넌트 또는 유틸리티가 포함된 폴더입니다.package.json
: 공통 패키지의 종속성을 정의하는 파일입니다.
-
최상위
package.json
및package-lock.json
: 프로젝트 전체를 관리하기 위한 종속성 정의 및 고정 파일들입니다.
이런 구조를 통해 각 애플리케이션과 공통 패키지를 독립적이면서도 효율적으로 관리할 수 있습니다.
3. Turborepo를 활용한 효율적인 도커라이징 방법 소개 (feat:공식문서)
DockerFile 작성하기
FROM node:20 WORKDIR /usr/src/app # Copy root package.json and lockfile COPY package.json ./ COPY package-lock.json ./ # Copy the docs package.json COPY apps/web1/package.json ./apps/web1/package.json RUN npm install # Copy app source COPY . . EXPOSE 8080 CMD [ "node", "apps/docs/server.js" ]
의존성 문제 발생
-
패키지 설치 문제: apps/web1에 필요하지 않은 apps/web2의 의존성이 바뀌는 경우 불필요한 설치가 생깁니다. 또한 lock파일 또한 자주 바뀌어 이 또한 불필요한 빌드가 생길 수 있습니다.
-
의존성 충돌: 여러 패키지가 같은 의존성을 다르게 요구하는 경우, 루트에서 설치된 의존성과 앱별 의존성이 충돌할 가능성이 있습니다.
-
해결책
-
Dockerfile에 대한 입력을 엄격히 필요한 것으로만 정리하는 것입니다. Turborepo는 간단한 솔루션을 제공합니다
pnpm turbo prune --scope=web1 --docker
이 명령을 실행하면 모노레포의 정리된 버전이 생성되어, ./out 디렉토리에 docs 이후에 달라지는 작업 공간만 포함됩니다. 핵심은 잠금 파일을 정리하여 필요한 파일만 node_modules에 다운로드되도록 하는 것입니다. 기본적으로 turbo prune은 모든 관련 파일을 ./out에 넣지만, Docker 캐시 최적화를 위해 이상적으로는 파일을 두 단계로 복사합니다.
out
// json: 의존성 설치를 위해 사용하는 폴더
├── json
│ └── apps
│ └── web1
│ └── package.json
│ └── packages
│ └── ui
│ └── package.json
// full: 실제 빌드에 필요한 모든 파일을 포함
├── full
│ └── apps
│ └── wseb1
│ └── package.json
│ └── packages
│ └── ui
│ └── package.json
└── package-lock.json
솔류션을 적용한 DockerFile
FROM node:alpine AS builder RUN apk add --no-cache libc6-compat RUN apk update # Set working directory WORKDIR /app RUN pnpm global add turbo COPY . . RUN turbo prune --scope=web1 --docker # Add lockfile and package.json's of isolated subworkspace FROM node:alpine AS installer RUN apk add --no-cache libc6-compat RUN apk update WORKDIR /app # First install the dependencies (as they change less often) COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . COPY --from=builder /app/out/pnpm.lock ./pnpm.lock RUN pnpm install # Build the project COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json RUN pnpm turbo run build --filter=web1... FROM node:alpine AS runner WORKDIR /app # Don't run production as root RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs USER nextjs COPY --from=installer /app/apps/web1/next.config.js . COPY --from=installer /app/apps/web1/package.json . # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=installer --chown=nextjs:nodejs /app/apps/web1/.next/standalone ./ COPY --from=installer --chown=nextjs:nodejs /app/apps/web1/.next/static ./apps/web1/.next/static CMD node apps/web1/server.js
static s3 upload
빌드 과정에서 나오는 css, js, html을 cdn으로 올려 static 파일을 Amazon S3에 업로드하면, 빠르고 안정적으로 해당 리소스를 제공할 수 있습니다. 이를 통해 서버 부하를 줄이고, 클라이언트가 가까운 CDN 엣지에서 파일을 다운로드하도록 최적화할 수 있습니다.
S3 업로드 예시
RUN --mount=type=secret,id=aws-access-key-id \ --mount=type=secret,id=aws-secret-access-key \ --mount=type=secret,id=aws-default-region \ export AWS_ACCESS_KEY_ID=$(cat /run/secrets/aws-access-key-id) && \ export AWS_SECRET_ACCESS_KEY=$(cat /run/secrets/aws-secret-access-key) && \ export AWS_DEFAULT_REGION=$(cat /run/secrets/aws-default-region) && \ aws s3 cp /app/apps/web1/.next/static s3://your-s3-bucket-name/_next/static --recursive RUN rm -rf /app/apps/web1/.next/static
./out/apps/web1/.next/static
: Next.js 빌드 시 생성된 정적 파일들이 위치하는 디렉토리입니다../out/apps/web1/public
: 퍼블릭 폴더에 포함된 정적 리소스 파일들입니다.s3://your-cdn-bucket-name
: 대상 S3 버킷 이름으로 적절히 변경합니다. 해당 파일들은 클라이언트에서 CDN URL을 통해 접근할 수 있으므로, Next.js 설정에서assetPrefix
를 활용해 빌드된 출력물을 S3를 통해 제공받도록 수정해야 **your-cdn-bucket-name******합니다.
Next.js assetPrefix
설정 추가
// next.config.js module.exports = { assetPrefix: 'https://your-cdn-bucket-name.s3.amazonaws.com', };
최종 구성
위 설정을 적용하면 정적 파일은 S3에서 제공되고, 애플리케이션 서버는 클라이언트 요청에 대한 동적 처리를 수행합니다. 이로써 효율적인 리소스 관리 및 빠른 정적 파일 제공이 가능합니다.
4. 마무리
위 과정을 통해 Turborepo 기반의 모노레포 환경에서 각 애플리케이션의 의존성을 효율적으로 관리하고, Docker 이미지를 최적화하는 방법을 학습했습니다. 이를 통해 다음과 같은 이점들을 얻을 수 있습니다
- 효율적인 의존성 관리:
turbo prune
을 통해 필요한 의존성과 파일만 포함하여 빌드 효율성을 극대화하였습니다. - 최적화된 Docker 이미지: 단계별 빌드와 최소한의 파일 복사를 통해 빌드 시간을 단축하고 이미지 크기를 줄였습니다.
- CDN을 활용한 정적 파일 최적화: 정적 파일을 S3로 업로드하고,
assetPrefix
를 사용해 Next.js 설정을 최적화하여 빠르고 안정적인 파일 제공이 가능해졌습니다.
다음은 ecs와 code deploy를 활용한 배포 방식에대해 설명드리겠습니다.