본문 바로가기
시작/TIL(Today I Learned)

231109 - Monorepo를 이용한 Next.js 프로젝트 구성하기 (feat. pnpm)

by 백씨네 2023. 11. 9.

Monorepo를 이용한 Next.js 프로젝트 구성하기 (feat. pnpm)

 

모노레포를 위한 pnpm 설치

해당 모노레포의 패키지는 pnpm을 이용한다.

pnpm : https://pnpm.io/ko/installation

  • pnpm은 npm과 비슷한 패키지 매니저이다.
    • 하드 링크와 심볼릭 링크를 사용하여 중복된 패키지를 여러 프로젝트에서 재사용한다. ( 디스크 공간 절약 )
    • 즉, npm은 패키지를 중복해서 설치하지만 pnpm은 패키지를 공유한다.
  • pnpm은 패키지를 병렬로 설치하기 때문에 npm보다 빠르다.
# homebrew로 설치
$ brew install pnpm

# Powershell
$ iwr https://get.pnpm.io/install.ps1 -useb | iex


# npm으로 설치
$ npm install -g pnpm


# 다른 방법도 많이 있다..

 

루트 지정

작업을 진행할 루트 디렉토리를 지정하고 해당 디렉토리에서 pnpm init을 실행한다.

$ mkdir next-monorepo
$ cd next-monorepo
$ pnpm init
$ pnpm -v # 버전 확인

package.json이 생성이 된다.

{
    "name": "monorepotest2",
    "version": "1.0.0",
    "description": "",
    "main": "index.js", // 필요 없으므로 삭제
    "packageManager": "pnpm@8.10.2"
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
}

package.json 세팅 이후 프로젝트에 맞게 디렉토리 구조를 설정한다.

 

디렉토리 구조 설정

$ mkdir packages
$ mkdir packages/components
$ mkdir packages/api
$ mkdir packages/utils

$ mkdir apps
$ mkdir apps/client
$ mkdir apps/admin

프로젝트마다 원하는 디렉토리 구조는 다르겠지만 예시로 위와 같이 만든다.

components 디렉토리는 공통으로 사용할 컴포넌트를 모아놓는다.
api 디렉토리는 api 요청을 모아놓는다.
utils 디렉토리는 공통으로 사용할 유틸리티 함수를 모아놓는다.

그리고 만들어진 디렉토리들을 monorepo로 사용하기 위해서 워크스페이스 설정을 해주어야 한다.

 

워크스페이스 설정

루트 디렉토리에 pnpm-workspace.yaml 파일을 생성한다.

packages:
    - "packages/*"
    - "apps/*"

pnpm-workspace.yaml 파일은 pnpm이 모노레포를 인식할 수 있도록 해주는 파일이다.
위와 같이 설정하면 packages와 apps 디렉토리를 모두 인식한다.

해당 디렉토리들 내에 위치한 각각의 package.json 파일들을 인식하여 의존성을 관리한다.

현재까지 디렉토리 구조

📦next-monorepo
 ┣ 📂apps
 ┃ ┣ 📂admin
 ┃ ┗ 📂client
 ┣ 📂packages
 ┃ ┣ 📂api
 ┃ ┣ 📂components
 ┃ ┗ 📂utils
 ┣ 📜package.json
 ┗ 📜pnpm-workspace.yaml

 

각 프로젝트에 Next 설치

# next-monorepo 디렉토리에서

$ cd apps/client
$ pnpx create-next-app@latest .

$ cd apps/admin
$ pnpx create-next-app@latest .

각 서비스를 위한 apps 디렉토리 내부에서 next를 설치한다.

Next도 동일하게 쓰는 건데 왜 따로 설치하는지 의문이 들 수 있다.
하지만 각각의 서비스마다 필요한 패키지가 다를 수 있기 때문에 따로 설치한다.
또한 독립적인 배포를 위해서도 따로 설치한다.

 

프로젝트 실행하기

각 page.tsx 파일에 Hello, Client!! 와 Hello, Admin!! 을 출력하는 코드를 작성한다.

 

 

 

루트 디렉토리에서 --filter을 이용해서 해당 서비스를 실행한다.

  • pnpm --filter [서비스명] [명령어] 명령어를 작성해야 한다.
# 클라이언트를 실행하기 위해서 루트 디렉토리에서 client 실행
$ pnpm --filter client dev

# 어드민을 실행하기 위해서 루트 디렉토리에서 admin 실행
$ pnpm --filter admin dev

매번 --filter 서비스명을 지정하는 것이 번거롭기 때문에 루트에 있는 package.json의 script를 이용해서 지정하여 쓸 수 있다.

{
    "name": "monorepotest2",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "client": "pnpm --filter client",
        "admin": "pnpm --filter admin"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
}

이후 아래와 같이 실행할 수 있다.

 

# 루트 디렉토리에서
$ pnpm client dev
$ pnpm admin dev

 

 

현재까지 디렉토리 구조

📦next-monorepo
 ┣ 📂apps
 ┃ ┣ 📂admin
 ┃ ┃ ┗ 📜(next 설치됨)
 ┃ ┗ 📂client
 ┃ ┃ ┗ 📜(next 설치됨)
 ┣ 📂node_modules
 ┣ 📂packages
 ┃ ┣ 📂api
 ┃ ┣ 📂components
 ┃ ┗ 📂utils
 ┣ 📜package.json
 ┗ 📜pnpm-workspace.yaml

각 프로젝트에 next를 설치했지만 루트 디렉토리에 node_modules가 생성되었다.
이는 pnpm이 모노레포를 인식할 수 있도록 해주는 pnpm-workspace.yaml 파일이 있기 때문이다.

즉, 각 프로젝트에 공통된 패키지가 있다면 루트에서 설치하여 각 프로젝트는 하나의 패키지로 관리할 수 있다.






공통 패키지

공통 패키지 설치

만약 각 프로젝트에 recoil을 설치하고 싶다면 루트 디렉토리에서 설치하면 된다.

# 루트 디렉토리에서
$ pnpm install recoil

 

 

 

ERR_PNPM_ADDING_TO_ROOT  Running this command will add the dependency to the workspace root, which might not be what you want - if you really meant it, make it explicit by running this command again with the -w flag (or --workspace-root). If you don't want to see this warning anymore, you may set the ignore-workspace-root-check setting to true.

이는 해당 패키지가 루트 디렉토리에 설치되었기 때문에 발생하는 에러이다. 프로젝트 내부에서 설치해야 할 것을 잘못 설치하는 상황을 방지하기 위해서 발생하는 에러이다.

 

 

하지만 우리는 루트 디렉토리에 설치하고 싶다. 이를 위해서는 -w 를 추가해 주면 된다.

# 루트 디렉토리에서
$ pnpm install recoil -w

 

 

 

그러면 이렇게 문제없이 설치가 된다

공통 패키지 사용하기

공통으로 사용하는 components, api, utils를 packages 디렉토리를 만들었고
하위에 각 디렉토리를 만들었을 때 각각의 디렉토리마다 package.json을 생성해야 한다.

# /packages 각 디렉토리에서
$ npm init

각 디렉토리마다 package.json을 생성하고 해당 필요한 패키지를 설치한다.

📦packages
 ┣ 📂api
 ┃ ┗ 📜package.json
 ┣ 📂components
 ┃ ┗ 📜package.json
 ┗ 📂utils
 ┃ ┗ 📜package.json

공통 패키지 작성하기

components 디렉토리에 Button 컴포넌트를 만들고 각각의 프로젝트에서 사용하기 위해서 package.json을 지정해야 한다.

공통으로 사용할 컴포넌트

// /packages/components/Button.tsx
import React from "react"

interface ButtonProps {
    children: React.ReactNode
}

export const Button = ({ children }: ButtonProps) => {
    return <button>{children}</button>
}

공통으로 사용할 컴포넌트를 만든 후 사용할 프로젝트에서 해당 패키지를 설치한다.

👉 Client 측에서 components 패키지를 이용하겠다.

# 루트 디렉토리에서
$ pnpm --filter client add components --workspace
$ pnpm client add components --workspace
$ pnpm client add utils --workspace
$ pnpm client add api --workspace


# 즉
$ pnpm --filter [서비스명] add [패키지명] --workspace

pnpm client으로 쓸 수 있는 이유는 위에서 루트 package.json에 script로 작성해 뒀기 때문이다.

패키지를 이용하겠다는 명령어를 쓰고 나면 client 디렉토리의 package.json이

{
    "dependencies": {
        "components": "workspace:^"
    }
}

이렇게 추가됐을 것이다. 이는 해당 프로젝트에서 components 패키지를 사용하겠다는 의미이다.

또한 node_modules 디렉토리에 components 디렉토리가 생성되어 있을 것이다.


📦node_modules
 ┗ 📂components
 ┃ ┣ 📜Button.tsx
 ┃ ┗ 📜index.ts

👉 반드시 공유하고 싶은 디렉토리(패키지)는 package.json이 있어야 한다.

컴포넌트 내부 디렉토리 구조

그리고 컴포넌트가 많을 때 아래와 같은 경우로 디렉토리를 설정할 경우도 있다.


📦components
┣ 📂Box
┃ ┗ 📜index.tsx
┣ 📂Icon
┃ ┣ 📜ArrowDown.tsx
┃ ┗ 📜index.ts
┣ 📂boxIcon
┃ ┗ 📜index.tsx
┣ 📂button
┃ ┗ 📜index.tsx
┣ 📂input
┃ ┗ 📜index.tsx
┣ 📜index.ts
┗ 📜package.json

패키지로 내보내는 components 디렉토리 내부에 각각의 컴포넌트를 디렉토리로 만들 경우 index를 이용해서 내보내고 components/index.ts를 만들어 각각의 디렉토리도 내보내야 한다.

Icon을 예시로 살펴보면

ArrowDown.tsx로 만든 컴포넌트를 내보내기 위해서 Icon/index.ts를 만들어야 한다.

// /packages/components/Icon/index.ts
export * from "./ArrowDown"

그리고 components/index.ts를 만들어서 각각의 디렉토리를 내보내야 한다.

// /packages/components/index.ts

//...
export * from "./Box"
export * from "./Icon"
// ...

이렇게 하면 components 패키지를 설치한 프로젝트에서 아래와 같이 사용할 수 있다.

// (client 프로젝트에서 사용할 곳)/page.tsx

import { Button, Box, Icon } from "components"

//...
return (
    <>
        <Button />
        <Box />
        <Icon />
    </>
)
//...

모노레포를 이용한 components 패키지 완성

 

 

모노레포를 이용한 components 패키지 완성!!!

댓글