오늘은 최근 회사 프로젝트에서 Next.js 프로젝트의 빌드 타임을 획기적으로 줄이기 위해 Yarn Classic(v1)에서 Yarn Berry(v4, PnP)로 마이그레이션 한 경험을 공유하려고 합니다.
결론부터 말씀드리면, 78초 걸리던 빌드가 25초로 줄어들었습니다. (약 3배 빨라졌네요 🚀)
물론 그 과정이 순탄치만은 않았습니다. 특히 Docker 환경과 k8s 배포 과정에서 마주친 Cannot find module 에러들과의 사투... 그 해결 과정을 정리해 봅니다.
저희 서비스 환경은 대략 이렇습니다.
기존 Yarn v1은 빌드할 때마다 그 무거운 node_modules를 생성하고, I/O 작업을 수행하느라 시간을 많이 잡아먹었습니다. 로컬이랑 CI 서버랑 설치 속도 차이도 컸고요.
그래서 결심했습니다. "Zero-Install까지는 아니더라도, PnP(Plug'n'Play)를 도입해서 node_modules를 없애버리자!"
호기롭게 Yarn 버전을 올렸습니다.
yarn set version berry
yarn install
터미널에서 설치가 순식간에 끝났습니다. "와, 진짜 빠르네?" 하고 VSCode를 켠 순간...

코드 전체가 빨간 줄로 도배되었습니다.
"Cannot find module 'react'..." "Cannot find module 'next'..."
Yarn PnP는 실제 node_modules 폴더가 없기 때문에, VSCode가 타입을 찾지 못해서 생기는 문제였습니다. 다행히 이건 공식 문서에 해결책이 바로 있더군요.
✅ 해결: Editor SDK 설치
yarn dlx @yarnpkg/sdks vscode
이 명령어 한 방이면 .vscode/settings.json 이 자동으로 수정되면서, VSCode가 PnP 압축 파일(Zip) 내부의 타입을 투시(?)해서 읽을 수 있게 됩니다. 평화가 찾아왔습니다.

로컬 개발 환경 세팅을 마치고, 신나게 Docker 이미지를 말아서 배포 테스트를 했습니다. 빌드(Build Stage)는 잘 넘어갔습니다. 그런데 컨테이너를 실행(Run)하자마자 죽어버립니다.
Error: Cannot find module 'next'
Require stack:
- /app/server.js
...
분명히 .pnp.cjs 파일도 복사했고, 하라는 대로 다 했는데 왜 모듈을 못 찾을까요?

알고 보니 캐시 경로가 문제였습니다. Yarn Berry는 기본적으로 전역 캐시(Global Cache)를 사용하려 하는데, Docker 멀티 스테이지 빌드 과정에서 이 캐시가 제대로 전달되지 않거나 엉뚱한 곳에 저장되고 있었던 겁니다.
컨테이너 내부에서 확실하게 의존성을 잡으려면, 캐시 폴더를 프로젝트 내부로 고정해주는 것이 정신건강에 좋다는 걸 깨달았습니다.
.yarnrc.yml 파일에 캐시 관련 설정을 명시적으로 추가했습니다.
# .yarnrc.yml
nodeLinker: pnp
enableGlobalCache: false # 글로벌 캐시 끄기 (프로젝트 내부에 저장)
cacheFolder: ./.yarn/cache # 캐시 경로를 프로젝트 루트 아래로 고정
yarnPath: .yarn/releases/yarn-4.12.0.cjs
이렇게 하면 yarn install 시 생성되는 의존성 파일들이 .yarn/cache 폴더에 예쁘게 모입니다. 덕분에 Dockerfile에서 .yarn 폴더만 복사하면 누락되는 파일 없이 완벽하게 실행 환경이 구성됩니다.

Next.js의 output: 'standalone' 은 이미지 용량을 줄이기 위해 필수지만, 기본적으로 node_modules 구조를 가정하고 만들어집니다. PnP와는 물과 기름 같은 사이죠.
그래서 Standalone 빌드 결과물에 PnP 환경을 강제로 주입(Injection) 하는 전략으로 최종 Dockerfile을 완성했습니다.
기본적인 Dockerfile은 작성되었다고 가정합니다.
여기서는 필수적인 부분만 작성되었습니다
🛠️ 완성된 Dockerfile
# ---- Stage 1: Buildtime ----
# Corepack 활성화 (Yarn berry를 사용하기 위해 필수)
RUN corepack enable
# PnP 환경 복사 (중요: .yarnrc.yml 포함)
COPY package.json yarn.lock ./
COPY .yarnrc.yml ./
COPY .pnp.cjs ./
COPY .pnp.loader.mjs ./
COPY .yarn ./.yarn
# 의존성 설치
RUN yarn install --immutable
# ---- Stage 2: Runtime ----
# Standalone 결과물 복사
# (주의: Standalone은 node_modules 기반이라 PnP에선 껍데기일 수 있음)
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
# ★ 핵심: PnP 런타임 덮어쓰기 ★
# Standalone이 만든 불완전한 .yarn을 지우고, 빌드 스테이지의 완벽한 .yarn을 복사
RUN rm -rf ./.yarn
COPY --from=builder /app/.pnp.cjs ./
COPY --from=builder /app/.pnp.loader.mjs ./
COPY --from=builder /app/.yarn ./.yarn/
# ★ 핵심: Node 실행 시 PnP 로더 강제 주입
ENV NODE_OPTIONS="--require=/app/.pnp.cjs --loader=/app/.pnp.loader.mjs"
# 잘 로드되는지 확인 사살
RUN node -e "require('next/package.json'); console.log('next ok')"
# 실행
CMD ["node", "server.js"]
이 설정의 핵심은 NODE_OPTIONS입니다. node server.js 가 실행될 때 --require 와 --loader 옵션을 통해 PnP 시스템을 먼저 로드하게 만들어서, node_modules 없이도 압축된 패키지들을 찰떡같이 찾아내게 만들었습니다.
자, 이제 고생한 보람을 느낄 차례입니다.
Before (Yarn Classic v1) node_modules 압축 풀고... 유효성 검사하고... I/O 병목 생기고...
총 소요 시간: 78.0초

After (Yarn Berry v4 PnP) 의존성 설치가 순식간에 끝나고 빌드도 쾌적합니다.
총 소요 시간: 25.2초


78초 ➡️ 25초
단순 계산으로도 3배 이상 빨라졌습니다
Next.js의 Standalone 모드와 Yarn PnP는 구조적으로 서로 지향하는 바가 달라서(물리 파일 vs 가상 파일 시스템), 초기 세팅에 애를 좀 먹었습니다.
하지만 .yarnrc.yml 에서 cacheFolder 를 확실히 잡아주고, Dockerfile에서 NODE_OPTIONS를 통해 PnP 로더를 주입해 주니 아주 안정적으로 돌아가네요. 배포 속도가 빨라지니 개발 생산성도 오르고, CI 돌아가는 거 멍하니 바라보는 시간도 줄어서 아주 만족스럽습니다.
혹시 저처럼 Next.js + Docker + Yarn Berry 조합에서 Cannot find module 에러로 고통받고 계신 분들에게 이 글이 도움이 되었으면 좋겠습니다!
멋지십니다.