밝을희 클태

Yarn Berry로 Monorepo 구성하기 본문

KEYNUT 프로젝트

Yarn Berry로 Monorepo 구성하기

huipark 2024. 12. 21. 21:41

기존의 어드민 페이지가 한 도메인으로 통합되어 uri로 구분을 하고 있었는데 아예 도메인을 분리하려고 한다. 그러면서 Monorepo를 함께 적용해보려고 한다.

Monorepo란?

한 레포지토리에서 독립된 여러 프로젝트를 관리할 수 있음

Multirepo를 사용한다면 의존성이 낮아지는 장점은 있지만, 코드를 재사용할 수 없고, 의존하는 코드의 변경이 일어나면 일일이 해당 코드를 사용하는 프로젝트로 가서 적용을 시켜줘야 한다. Monorepo를 사용하면 패키지를 여러 프로젝트에서 공유해 한 번의 설치로 공유해서 사용할 수 있다.

패키지 매니저 yarn vs pnpm

yarn을 사용하는 기업

  1. 토스
  2. 우아한형제들
  3. 화해
  4. 원티드랩
  5. 리멤버

Yarn

  • 다양한 기업에서 yarn workspaces을 사용해 모노레포를 구성하고 있다. yarn berry는 PnP(Plug'n'Play)로 node_module 없이 의존성을 관리할 수 있고, Zero-Install을 통해 Yarn Berry에서 의존성을 node_modules에 설치하는 대신, .yarn/cache에 압축된 형태로 저장하고 Git에 커밋해 의존성을 다시 설치할 필요 없이 즉시 사용할 수 있어 설치 시간을 없앨 수 있다.

pnpm

  • pnpm은 node_module을 사용하되 한 번만 설치를 하고 하드 링크로 연결하는 방식이다.

Yarn 결정!

나는 최종적으로 yarn을 사용하기로 했다. 유령 의존성 등 자잘한 버그가 있다는 걸 봤지만, 이미 많은 기업에서 사용을 하고 있고, v4로 올라가면서 유령 의존성 문제도 해결 됐다고 한다.

모노레포 툴

모노레포 툴은 turborepo, nx, lerna 등이 있는데 turborepo를 사용하려고 했으나(Next.js, TS, vercel 등 프로젝트 스택이 너무 잘 맞음) yarn의 pnp를 지원하지 않는다는 걸 알게 됐고, 직접 처음부터 구성하고 싶어 따로 사용하지 않았다.

 

Lerna는 가장 많이 사용되고 있는 방식이었지만, 2022년 5월에 Nx를 관리하고 있는 Nrwl에 소유권을 넘기면서 더 이상 유지보수하지 않는다.

 

터보레포를 사용해 3가지 패키지 매니저의 배포 속도를 측정해 봤다. vercel의 서버 환경에 따라 차이가 있을 순 있지만 yarn berry가 가장 빨리 배포가 됐다.

turborepo yarn classic
turborepo pnpm
turborepo yarn berry

구성하기

yarn 설치하기 (이미 설치되어 있으면 X)

$ npm install -g yarn
$ mkdir monorepo && cd monorepo

yarn berry로 버전을 바꿔준다. 2.x 이상이면 berry

$ yarn set version berry && yarn -v
$ yarn init -p

 

package.json
현재 디렉토리 구성

나는 yarn pnp를 사용할 거기 때문에. yarnrc.yml 파일에 nodeLinker: pnp를 추가

echo nodeLinker: pnp >> .yarnrc.yml

.yarnrc.yml

package.json에 workspaces를 추가한다. workspaces는 여러 패키지를 모노레포 형태로 관리할 수 있게 도와주는 설정으로, 하위 패키지들의 의존성을 통합적으로 관리하고 로컬 참조를 쉽게 설정할 수 있다.

// package.json
{
  "name": "keynut-monorepo",
  "packageManager": "yarn@4.5.3",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

실제 작업 공간을 만들어준다.

$ mkdir -p apps/web apps/admin packages/ui packages/utils

방금 만든 web, admin 디렉터리로 가서 프로젝트를 만들어준다.

$ yarn create next-app .

Error

그러고 프로젝트의 소스를 보면 이렇게 에러가 난다.

yarn pnp에서는 node-modules 디렉터리가 생성이 되지 않는다. vscode는 기본적으로 의존성 탐색을 node-modules 디렉터리에서 하기 때문에 pnp 환경에서는 의존성을 찾을 수 없어 에러가 난다.

아래는 typescript를 찾지 못해 발생하는 에러다.

터미널에서 루트로 이동해 아래 명령어 실행

$ yarn dlx @yarnpkg/sdks vscode

vscode

mac: cmd + shift + p

win: ctrl + shift + p 

-> TypeScript: Select TypecSript Version -> Use Workspace Version 선택

zero-install

zero-install은 yarn berry에서 지원하는 기능으로 압축된 의존성을 git에 포함하여 clone을 받고 yarn install 없이 바로 실행할 수 있게 해주는 기능이다.

zero install 사용 시

// .gitignore

.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

zero install 미사용 시

// .gitignore

.pnp.*
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

실행하기

scripts에 dev 명령어를 추가해 주면 병렬적으로 모든 app을 실행시킬 수 있다.

  • all: 모든 workspace
  • parallel: 병렬 작업
  • interlaced: 로그 확인 
{
  "name": "keynut-monorepo",
  "packageManager": "yarn@4.5.3",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "devDependencies": {
    "typescript": "^5.7.2"
  },
  "scripts": {
    "dev": "yarn workspaces foreach --all --parallel --interlaced run dev"
  }
}

typescript 통합 관리

루트 디렉터리로 돌아가서 타입스크립트 설치

$ yarn add -D typescript

각 프로젝트에도 설치

$ yarn workspace admin add -D typescript
$ yarn workspace web add -D typescript

tsconfig.base.json 파일을 만들어준다. 아래의 파일로 모든 workspace의 ts 설정을 해줄 거다.

{
  "compilerOptions": {
    "target": "ES2022",
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "incremental": false,
    "isolatedModules": true,
    "lib": ["es2022", "DOM", "DOM.Iterable"],
    "module": "NodeNext",
    "moduleDetection": "force",
    "noEmit": true,
    "allowJs": false,
    "moduleResolution": "NodeNext",
    "noUncheckedIndexedAccess": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "jsx": "preserve"
  }
}
// root 경로의 tsconfig.json

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    "next-env.d.ts",
    "next.config.js",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}
// apps 프로젝트들의 tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": [
    "next-env.d.ts",
    "next.config.js",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

Prettier 설정

$ yarn add -D prettier
// root 경로에 .prettierrc

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "auto"
}

packages/ui 구성

packages 디렉터리는 서로 다른 프로젝트에서 재사용되는 코드를 정의하는 작업 공간이다.

ui 디렉터리에서 package.json을 만들고 필요한 의존성 연결

$ cd packages/ui && touch package.json
$ yarn add react react-dom
$ yarn add -D @types/react @types/react-dom typescript

package.json 설정

{
  "name": "@keynut/ui",
  "private": true,
  "exports": {
    ".": "./index.ts"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "typescript": "^5.7.2"
  }
}

import 할 때 나는 전자보다 후자를 더 선호해서 index에서 한 번에 처리를 해줬다.

import Button from '@keynut/ui/Button';
import Button from '@keynut/ui/AlertButton';

import { Button, AlertButton } from '@keynut/ui';
// .index.ts

export * from './src/Button';
export * from './src/AlertButton';

만약 위처럼 각각 import 하고 싶으면 아래처럼 exports를 바꿔주면 된다.

{
...

  "exports": {
    "Button": "./src/Button.tsx",
    "AlertButton": "./src/AlertButton.tsx"
  },
  
...
}
// ./src/Button.tsx

'use client';

export const Button = () => {
  return <button onClick={() => alert(`Hello World!`)}>BUTTON/</button>;
};

이렇게 ui 패키지를 구성하고 만들었으면 사용할 워크스페이스에 연결하면 된다. web 워크스페이스에서 사용하고 싶다면

$ yarn workspace web add @keynut/ui

web package.json

그리고 일반 패키지 사용하듯이 사용하면 된다.

vscode extensions

ZipFS는 .zip 형태로 압축된 의존성을 풀지 않고도 사용할 수 있게 해 준다.

next/image에 정의된 Image 컴포넌트 내부를 확인할 때 ZipFS가 없으면 압축된 의존성의 내부 코드를 보지 못해 아래와 같이 나온다.

 

느낀 점

모노레포를 구성하면서 가장 편리했던 점은 공통 의존성을 만들어 여러 워크스페이스에서 사용할 수 있었다는 것이다. 특히 admin과 client를 분리하면서 공통으로 사용하는 의존성을 한 번만 정의해 사용할 수 있어 효율적이었다.

 

Yarn PnP를 사용해 디스크 공간을 절약하고 의존성 관리가 간단해진 점도 좋았다. 하지만 의존성을 찾지 못하는 문제가 자주 발생했고, VSCode에서 의존성 탐색이 제대로 이루어지지 않아 불편했다. 그리고 처음에는 zero-install을 무조건적으로 좋은 방식이라고 생각을 했는데 github 과부하와 .yarn/cache 파일을 포함한 대규모 파일을 푸시하는 데 상당한 시간이 소요되는 문제가 있어 사용하지 않았다.

 

그럼에도 워크스페이스 간 코드 공유가 쉬워지고, 공통 라이브러리를 효율적으로 관리할 수 있어 만족스러웠다. 초기 설정과 호환성 문제는 있었지만, 전체적인 생산성과 관리 효율성을 크게 높일 수 있었다.

 

계속 사용을 해보다가 끝으로는 뭔가 pnpm으로 다시 바꿀 거 같다.

 

모노레포를 구성하면서 처음으로 Yarn과 pnpm을 학습해 봤다. npm만 사용했을 때는 몰랐던 패키지 매니저의 차이와 효율적인 의존성 관리 방법을 알게 되었다