[트러블 슈팅] Next.js와 tailwindcss로 모노레포 구성하는 법 (1)
모노레포TailwindCSSNext.js

[트러블 슈팅] Next.js와 tailwindcss로 모노레포 구성하는 법 (1)

pnpm-workspace 기반 모노레포를 구성하는 과정에서 발생한 트러블 슈팅을 다룬다.

15views·0likes

배경

이번에 풀스택 개발을 진행하면서, 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.yaml

PNPM 모노레포 구성하기

처음에는 모노레포로 관리할 폴더를 생성해주고 내부에서 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를 설치할 경우 에러가 발생한다.

image.png

이 에러는 대부분의 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 에 작성하면 의존성 문제가 발생하게 된다.

image.png

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를 활용하여 각 프로젝트에 맞는 다양한 명령어를 사용할 수 있다.

Comments