배경
이번에 풀스택 개발을 진행하면서, Next.js 프론트엔드와 Nest.js 백엔드 애플리케이션을 공동으로 작업하기 위해 모노레포를 구성하였다. 모노레포를 구성하는 것도 처음이라서 혼란스러운 부분이 많았는데, 프론트엔드 애플리케이션 외부에서 tailwind css를 사용할 수 있도록 세팅하는 과정이 많이 힘들었다. 이번 글에서는 Next.js와 tailwindcss로 모노레포를 구성하는 법을 작성하고, 다음 글에서는 그 과정에서 겪은 트러블 슈팅 내용을 공유하고자 한다.
기술스택
이번 풀스택 프로젝트를 위해 사용한 기술스택은 아래와 같다.
프론트엔드
- 언어: TypeScript
- 프레임워크: Next.js v16
- 스타일링: Tailwind css v4
- 패키지 매니저: pnpm
백엔드
- 언어: TypeScript
- 프레임워크: Nestjs
- ORM: TypeORM
- 데이터베이스: PostgreSQL
- 패키지 매니저: pnpm
폴더구조
폴더구조는 다음과 같다. 프로젝트의 모든 파일이 아니라, 모노레포를 구성하기 위해 사용된 파일만 명시했다.
project
├── apps
│ ├── backend
│ │ └── (Nest.js 애플리케이션)
│ └── frontend
│ ├── app
│ │ ├── global.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ └── tsconfig.json
├── packages
│ └── ui
│ ├── components
│ │ ├── index.ts
│ │ └── Modal.tsx
│ ├── index.ts
│ └── package.json
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yamlPNPM 모노레포 구성하기
처음에는 모노레포로 관리할 폴더를 생성해주고 내부에서 pnpm을 초기화 해준다.
mkdir project
cd project
pnpm init이 과정을 거치면 다음과 같은 package.json파일이 생성된다.
{
"name": "project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.18.3"
}다음으로 서비스 애플리케이션과 공유 파일을 분리하기 위해 아래와 같은 프로젝트 구조를 생성했다.
project
├── apps (서비스)
└── packages (공유 파일)프로젝트 구조에 따른 패키지들이 모노레포로서 인식될 수 있도록 설정해야 한다. 이를 위해 루트에서 pnpm-workspace.yaml파일에 다음과 같이 작성했다.
packages:
- 'apps/*'
- 'packages/*'프로젝트 생성
프론트엔드와 백엔드를 모노레포로 구성하거나, 클라이언트와 어드민으로 나누어진 2가지 프론트엔드 애플리케이션으로 모노레포를 구성할 수도 있다. 이 중 프론트엔드와 백엔드를 모노레포로 구성하는 것으로 예시를 들어보도록 하겠다.
먼저, apps에서 프론트엔드(Next.js)와 백엔드(Nest.js) 프로젝트를 생성해준다.
pnpm create next-app@latest frontend --yes
pnpm add -g @nestjs/cli
nest new backend의존성(dependency) 세팅
루트의 package.json에 dependency를 세팅했으면 pnpm-workspace.yaml에서 명시한 프로젝트도 dependency를 사용할 수 있다. 그래서 공통으로 사용하는 dependency인 경우, 루트에 설치하고 개별적으로 사용하는 경우에만 특정 프로젝트에 설치하는 것이 좋다.
이때, 평범하게 루트에 dependency를 설치할 경우 에러가 발생한다.

이 에러는 대부분의 dependency는 하위 프로젝트에서만 필요하기 때문에 루트에 설치하려는 행동에 대한 경고 메시지이다. 나 같은 경우에는 거의 없었지만, 혹시나 루트에 설치하고 싶은 경우에는 -w 옵션을 붙여서 설치할 수 있다.
pnpm add [dependency name] -w모노레포의 의존성 원리는 아주 단순하다. pnpm-workspace.yaml 파일에 작성된 패키지들의 package.json 파일을 읽고 모든 패키지가 공유할 수 있도록 한다.
// apps와 packages 내부의 프로젝트는 서로 의존성을 공유할 수 있다.
packages:
- 'apps/*'
- 'packages/*'그러면 apps/frontend 에서 Next.js 프로젝트를 진행할 때, packages/ui 에서 컴포넌트 파일을 관리한다고 가정하자. frontend 패키지는 Next.js 프로젝트로 생성하여 react, next 등 의존성이 기본적으로 작성되어 있다. 그래서 다음과 같이 react를 활용한 코드를 작성해도 에러가 발생하지 않는다.
// apps/frontend/app/page.tsx
export default function Home() {
return <div>루트 페이지</div>;
}이후, 페이지에서 사용할 컴포넌트를 packages/ui 에 작성하면 의존성 문제가 발생하게 된다.

pnpm-workspace 모노레포는 의존성을 공유하는데, 왜 packages/ui는 next/image를 사용할 수 없을까?
pnpm은 전역 캐시와 심볼릭 링크를 사용해 의존성을 공유하되, 각 패키지는 명시된 의존성만을 import할 수 있어 코드 수준에선 의존성을 자동적으로 공유하지 않음을 의미한다. 그래서 각 패키지는 필요한 의존성을 명시적으로 package.json에 적어야 한다.
"dependencies": {
// ...
"next": "16.0.1"
},이는 pnpm의 개념인 의존성 격리 및 심볼릭 링크를 통해 유추할 수 있다. 그래서 apps/fronted, packages/ui 2개 이상의 패키지에서 같은 라이브러리를 명시해도, 중복 설치가 이루어지지 않는다.
명령어 세팅
pnpm-workspace.yaml을 구성했으면 각 workspace는 루트에서 --filter 옵션을 통해 접근할 수 있다. 다음 명령어는 루트 경로에서 frontend 프로젝트를 실행하기 위한 명령어이다.
pnpm --filter client dev매번 --filter 옵션을 붙이면 불편하기 때문에, 루트의 package.json에서 script에 명령어를 정의할 수 있다.
"scripts": {
"client": "pnpm --filter frontend", // frontend 프로젝트에 접근
"server": "pnpm --filter backend" // backend 프로젝트에 접근
},이후 각 프로젝트를 실행하기 위해 다음과 같이 명령어를 입력한다.
// frontend 프로젝트 실행, frontend 경로에서 pnpm run dev를 입력한 것과 같음
pnpm run client dev
// backend 프로젝트 실행, backend 경로에서 pnpm run start:dev를 입력한 것과 같음
pnpm run server start:dev이 외에도 package.json의 sciprt를 활용하여 각 프로젝트에 맞는 다양한 명령어를 사용할 수 있다.
![[트러블 슈팅] Next.js와 tailwindcss로 모노레포 구성하는 법 (1)](/_next/image?url=https%3A%2F%2Fuzsgwbfhiofnnodlhafc.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fblog-images%2Fposts%2F1774936361620-2875c387-2826-4444-bb01-ff66626329f3-zm0wi.png&w=3840&q=75)