Yarn Berry Workspace

Yarn Berry WorkSpace란?

모노레포 방식으로 여러 프로젝트(패키지)를 단일 레포지토리에서 관리하는 방식

의존성 관리를 한 곳에서 관리하기 때문에 공통된 의존성 모듈을 재사용하고, 중복되는 의존성 설치 문제를 피할 수 있다.

  • workspace: 모노레포의 패키지를 의미

Yarn Berry

Yarn v2 에서 사용하는 패키지 매니저

  • 기본적으로 명시적인 의존관계를 나타내야 사용 가능

  • node_modules 에 패키지를 저장하는 방식이 아닌 패키지를 압축하여 한개의 파일을 .yarn/cache/ 폴더에 수평적으로 저장한다. 이러한 방식을 Plug'n'Play(PnP) 라 한다.

    • 수평적으로 존재하여 빠르게 탐색 가능

    • 압축파일을 설치하기 때문에 파일 개수가 감소하여 설치가 빠름

    • Zero Install을 이용하여 저장소에서 함께 관리 가능

      • 저장소에 올라가기 때문에 저장소 자체가 매우 커지게 될 수 있음 (push, pull등 느리게 됨)

      • Zero Install이 아예 설치하지 않는것은 아님, 여러 환경에서 새로운 파일을 만들어내는 경우 때문에 설치 필요

    • Phantom Dependency가 발생하지 않음

  • PnP 모드를 기본적으로 지원

    • PnP 방식은 IDE에서 직접 사용하는 많은 도구들을 SDK 를 통해 우회 호출할 수 있도록 추가적인 설정 필요

  • workspaces plugin 지원

workspaces plugin이란 yarn의 workspace를 좀 더 쉽게 관리하기 위해 제공되는 플러그인

yarn workspace 명령어를 사용해서 워크스페이스에 속한 패키지들에 대해 일괄적으로 작업을 수행할 수 있다.

node_modules 문제점

  • 의존성 탐색 알고리즘 비효율

    • 모듈을 검색할 때 의존성 경로의 깊이 만큼 상위 node_modules 를 검색하게 된다.

  • node_modules 폴더 아래에 수 많은 패키지가 저장되어 공간을 많이 차지하게 되고 그만큼 설치하는데 오래 걸린다.

  • 유령 의존성이란 프로젝트에 실제로 사용하지 않지만 lock 파일에는 존재하는 의존성 패키지

    • 의존성 중복 방지를 위한 호이스팅 기법의 Side Effect

    • 불필요한 공간을 차지하고 느리게 만든다.

패키지 매니저(npm, yarn)에서 중복되는 의존성 설치를 방지하기 위해 호이스팅기법을 사용한다.

Plug & Play

패키지 의존성을 설치할 때, node_modules 폴더를 생성하지 않고, 의존성 패키지들을 .yarn/cache에 수평적으로 압축파일로 설치하여 필요한 패키지를 빠르게 가져와서 사용하는 방식

  • 여러 패키지에서 중복되는 의존성 패키지들을 전역으로 설치해서 재사용하는 방식

  • 해당 모듈을 사용하기 전 메모리에서 ZipFS라는 도구를 사용해서 압축을 해제하고 필요한 부분만 추출하여 가져오는 방식으로 효율적으로 접근한다.

  • 의존성이 .yarn/cache에 수평적으로 설치되므로 모든 패키지에 대한 접근 시간이 O(1)

  • 호이스팅 방식을 사용하지 않기 떄문에 유령 의존성이 발생하지 않는다.

  • 압축파일로 된 의존성을 git으로 관리하게 되면 설치 과정을 생략(zero-install)할 수 있다.

PnP 모드를 지원하지 않는 라이브러리를 사용하게 된다면 의존성 관리 방식을 node_modules 방식으로 변경해서 사용해야 한다.

// .yarnrc.yml
nodeLinker: "node_modeuls" // default "pnp"

Monorepo 환경 세팅하기

NVM

Node Version Manager, 여러 노드 버전을 스위칭하여 관리할 수 있게 도와주는 도구

  • 여러 프로젝트에서 서로 다른 Node 버전을 사용할 수 있게 도와준다.

  • 각 프로젝트마다 Node를 관리할 필요가 없게 된다.

brew install nvm

환경변수 설정하기

mkdir ~/.nvm

vi ~/.zshrc 
export NVM_DIR="$HOME/.nvm"
[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"  # This loads nvm
[ -s "/usr/local/opt/nvm/etc/bash_completion" ] && . "/usr/local/opt/nvm/etc/bash_completion"  # This loads nvm bash_completion

source ~/.zshrc 

설치된 Node version list 확인

nvm ls

'abumalick.vscode-nvm' vscode extension 추가

vscode에서 nvm을 사용하기 쉽도록 도와주는 확장 프로그램

프로젝트 루트경로에 .nvmrc 설정 파일을 추가하여 사용할 노드 버전을 지정하면 nvm use 명령어를 실행하지 않고도 특정 노드 버전을 자동으로 사용할 수 있도록 관리해준다.

.nvmrc

// galium 이란 별칭으로 사용되는 버전 16.20.0을 사용하겠다 선언
lts/gallium

해당 프로젝트의 vscode가 열리면 자동으로 nvm use 명령어가 실행되어 지정한 노드버전을 사용하도록 변경해준다.

nvm use는 로컬에 설치된 특정 노드 버전을 사용하도록 설정하는 명령어이다.

.vscode/extensions.json에 추가

.vscode/extension.json 에 추가하여 자동으로 해당 익스텐션을 설치할 수 있도록 설정하자.

{
  "recommendations": ["abumalick.vscode-nvm"]
}

Yarn Berry Workspace

yarn이 기본적으로 설치되어 있다고 가정

yarn version 변경하기

// yarn 버전 확인
yarn -v 

// yarn berry 버전 활성화
yarn set version berry

// yarn 버전 확인
yarn -v  // 현 시점 4.0.2

Yarn workspace 생성하기

루트 워크스페이스는 모든 프로젝트에 공통적으로 사용되는 패키지 의존성을 한번에 관리할 수 있다.

즉, 하위 워크스페이스에서 yarn install 명령어를 통해 루트에 설치된 패키지들을 사용할 수 있다.

yarn init -w 명령어는 yarn을 이용해서 프로젝트를 생성하는 명령어이다. -w 옵션은 현재 디렉토리를 루트 워크스페이스 디렉토리로 설정 (package.json 생성됨)

yarn berry cli 명령어 참고

yarn init -2 -w

yarn cache 확인하기

yarn config enableGlobalCache
yarn config set enableGlobalCache false

  • enableGlobalCache 설정은 .yarnrc.yml 파일 내부에 설정 되있는것을 확인해 볼 수 있다.

apps/ 디렉토리 생성

실제로 서비스할 어플리케이션들을 모아놓는 디렉토리

mkdir apps

packages/apps/에서 사용할 공통 라이브러리 패키지를 모아놓는 디렉토리

package.json

추가한 apps 디렉토리를 워크스페이스 디렉토리의 package.json에 등록

{ 
  "name": "mono-test",
  "packageManager": "yarn@3.5.1",
  "private": true, 
  // 워크스페이스에서 관리할 패키지 디렉토리 경로를 설정
  "workspaces": [
    "apps/*", // 생성한 apps/ 디렉토리를 추가
    "packages/*"
  ]
}

프로젝트 패키지 추가해보기

apps/ 디렉토리 내에서 프로젝트를 생성해보자.

yarn init

프로젝트 생성이 완료되었다면 해당 프로젝트의 package.json 을 수정

package.json

패키지의 범위를 설정하기 위해 프로젝트의 네임을 @ 접두어를 붙힌 @org/package 형식으로 변경

{
  "name": "@jtwjs/web",
  "version": "0.1.0",
  "private": true,
  //...//
}

워크스페이스로 돌아가서 상태를 갱신

프로젝트가 추가되면 워크스페이스로 돌아가서 상태를 갱신 해주어야 한다.

해당 정보는 .pnp.cjs 에서 관리 된다.

yarn workspace [package name] [scripts] 로 워크스페이스 특정 패키지의 명령어를 사용하여 간단하게 실행할 수 있다.

cd ../../

yarn // 상태 갱신

yarn workspace @jtwjs/web run dev // 실행 확인

.pnp.cjs란? yarn berry pnp 관련 정보를 모아놓은 캐시 파일로 모든 의존성의 메타 정보(zip 경로, 의존성)와 ZipFS에 대한 처리 코드가 들어있다

Typescript 설정하기

타입스크립트 코드에 빨간줄이 생기면서 인식을 못하는 에러가 발생한다.

  • yarn berry는 node_modules 대신 패키지 의존성을 zip 파일 형태로 .yarn/cache에 저장하여 관리하는 plugin & play 모드를 사용한다.

  • 이런 방식의 차이 때문에 추가적인 설정이 필요하다.

yarn add -D typescript
yarn dlx @yarnpkg/sdks vscode

yarn dlx @yarnpkg/sdks vscode

  • yarn dlx 명령어를 통해 @yarnpkg/sdks 패키지를 vscode 환경에 설치

  • yarn에서 제공하는 플러그인 중 하나인 yarn sdk는 yarn을 이용해서 설치한 프로그램을 실행할 수 있도록 도와준다.

  • 즉, vscode가 zip 파일로 된 패키지들을 읽어올 수 있게끔 설정하는 것

  • typescript, eslint, prettier 이 패키지들은 .yarn/sdks 내에 설치된다.

'arcanis.vscode-zipfs' vscode extension 추가

Yarn SDK와 함께 사용하면 캐시에서 파일을 원활하게 열고 편집할 수 있다.

.vscode/extensions.json에 추가

{
  "recommendations": [
    "abumalick.vscode-nvm",
    "arcanis.vscode-zipfs"
  ]
}
Next.js 모노레포 Typescript 이슈

다른 패키지(React.js)에서 내보내는 컴포넌트를(ts) 사용할 때 ts 해석을 하지 못한다.

  1. javascript로 변환시켜 주는 툴 설치

// next-transpile-modules 설치
yarn workspace @jtwjs/web add next-transpile-modules

  1. next.config.json 수정

// @wanted/ui 패키지를 tranpile 시킨다.
const withTM = require('next-transpile-modules')(['@wanted/ui']);

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
};

module.exports = withTM(nextConfig);

공통 패키지를 만들어보기

여러 패키지에서 공유하여 사용할 유틸 함수나 라이브러리를 관리하기 위해 packages/lib 폴더를 생성

// lib 폴더 이동
cd packages/lib

// pacakge.json 파일 생성
yarn init

// typescript 설치
yarn add typescript

package.json name 수정

{
  "name": "@jtwjs/lib",
  "main": "./src/index.ts",
}

tsconfig.json 설정

touch tsconfig.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "useUnknownInCatchVariables": true,
    "allowJs": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "newLine": "lf",
    "module": "ESNext",
    "moduleResolution": "node",
    "target": "ESNext",
    "lib": ["ESNext", "dom"],
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./src",
    "noEmit": false,
    "incremental": true,
    "resolveJsonModule": true,
    "paths": {}
  },
  "exclude": ["**/node_modules", "**/.*/", "./dist", "./coverage"],
  "include": ["**/*.ts", "**/*.js", "**/.cjs", "**/*.mjs", "**/*.json"]
}

패키지를 추가했다면 워크스페이스로 돌아가서 yarn 명령어를 입력해 갱신해주자.

다른 패키지에 의존성 추가하기

루트 워크스페이스 경로로 돌아가서 A 패키지에 B 패키지 의존성을 추가해보자.

// @jtwjs/web 패키지에 @jtwjs/lib 패키지를 추가
yarn workspace @jtwjs/web add @jtwjs/lib

@jtwjs/web 패키지를 확인해보면 아래와 같이 의존성이 추가된걸 확인할 수 있다.

// 워크스페이스 간의 의존관계를 확인하는 방법
yarn workspaces list

개발환경 설정 공유하기

tsconfig 설정 공유하기

프로젝트에서 공통적으로 사용되는 타입스크립트 설정을 한 곳에서 관리하여 중복되는 설정을 최소화한다.

// 루트 프로젝트에 의존성 추가
yarn add <PACKAGE_NAME> 
  1. 워크스페이스 경로에 tsconfig.base.json 파일을 생성하여 공통적으로 사용할 설정

  2. 각 프로젝트 패키지의 tsconfig.json 에서 extends 속성을 사용하여 재사용한다.

    "extends": "../../tsconfig.base.json",
// tscofnig.base.json
{
  "compilerOptions": {
    "strict": true,
    "useUnknownInCatchVariables": true,
    "allowJs": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "incremental": true,
    "newLine": "lf"
  },
  "exclude": ["**/node_modules", "**/.*/"]
}

eslint & prettier 설정 공유하기

여러 패키지에서 공통적으로 사용되는 eslint & prettier 규칙을 공유하여 재사용하자.

yarn add prettier eslint eslint-config-prettier eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-import-resolver-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser -D

yarn dlx @yarnpkg/sdks

기타 vscode 설정 추가

.vscode/settings.json
{
  "search.exclude": {
    "**/.yarn": true,
    "**/.pnp.*": true
  },
  "typescript.tsdk": ".yarn/sdks/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "eslint.nodePath": ".yarn/sdks",
  "prettier.prettierPath": ".yarn/sdks/prettier/index.js",

  // 기본 포맷터 prettier로 사용
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // 파일 저장시 formatter 실행
  "editor.formatOnSave": true,
  "editor.rulers": [120],

  // 추가되는 내용
  "eslint.packageManager": "yarn",
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
.vscode/extensions.json
{
  "recommendations": [
    "abumalick.vscode-nvm",
    "arcanis.vscode-zipfs",
    "esbenp.prettier-vscode",
    "dbaeumer.vscode-eslint"
  ]
}

Eslint & Prettier설정

eslint 설정 공유하기

eslint 잘 활용하기

Eslint 설정

가장 최상위 eslint 설정에 root: true 설정하고 커스텀을 위한 하위 eslint는 root: false 설정을 해주자.

module.exports = {
  root: true,

  env: {
    es6: true,
    node: true,
    browser: true,
  },

  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: { jsx: true },
  },

  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint', 'import', 'react', 'react-hooks'],
  settings: { 'import/resolver': { typescript: {} }, react: { version: 'detect' } },
  rules: {
    'no-implicit-coercion': 'error',
    'no-warning-comments': [
      'warn',
      {
        terms: ['TODO', 'FIXME', 'XXX', 'BUG'],
        location: 'anywhere',
      },
    ],
    curly: ['error', 'all'],
    eqeqeq: ['error', 'always', { null: 'ignore' }],

    // Hoisting을 전략적으로 사용한 경우가 많아서
    '@typescript-eslint/no-use-before-define': 'off',
    // 모델 정의 부분에서 class와 interface를 합치기 위해 사용하는 용법도 잡고 있어서
    '@typescript-eslint/no-empty-interface': 'off',
    // 모델 정의 부분에서 파라미터 프로퍼티를 잘 쓰고 있어서
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-parameter-properties': 'off',
    '@typescript-eslint/no-var-requires': 'warn',
    '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
    '@typescript-eslint/no-inferrable-types': 'warn',
    '@typescript-eslint/no-empty-function': 'off',
    '@typescript-eslint/naming-convention': [
      'error',
      { format: ['camelCase', 'UPPER_CASE', 'PascalCase'], selector: 'variable', leadingUnderscore: 'allow' },
      { format: ['camelCase', 'PascalCase'], selector: 'function' },
      { format: ['PascalCase'], selector: 'interface' },
      { format: ['PascalCase'], selector: 'typeAlias' },
    ],
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
    '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
    '@typescript-eslint/member-ordering': [
      'error',
      {
        default: [
          'public-static-field',
          'private-static-field',
          'public-instance-field',
          'private-instance-field',
          'public-constructor',
          'private-constructor',
          'public-instance-method',
          'private-instance-method',
        ],
      },
    ],

    'import/order': [
      'error',
      {
        groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
        alphabetize: { order: 'asc', caseInsensitive: true },
      },
    ],

    'react/prop-types': 'off',
    // React.memo, React.forwardRef에서 사용하는 경우도 막고 있어서
    'react/display-name': 'off',
    'react-hooks/exhaustive-deps': 'error',
    'react/react-in-jsx-scope': 'off',
    'react/no-unknown-property': ['error', { ignore: ['css'] }],
  },
};
Prettier 설정
{
  "arrowParens": "avoid",
  "bracketSameLine": false,
  "bracketSpacing": true,
  "endOfLine": "lf",
  "jsxSingleQuote": false,
  "printWidth": 120,
  "proseWrap": "preserve",
  "quoteProps": "as-needed",
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

모든 프로젝트에 공통적으로 실행할 명령어

workspace-tools 플러그인 설치

각 패키지 마다 공통적으로 동일한 네이밍의 스크립트를 일괄적으로 실행 시킬 수 있다.

yarn plugin import workspace-tools
"scripts": {
//g:는 global 관례이다.
  "g:typecheck": "yarn workspaces foreach -pv run typecheck"
},

yarn workspaces foreach 여러 패키지 대해 일괄적인 작업을 수행할 수 있는 명령어

  • -p: 병렬 실행

  • -v: workspace name 출력

typecheck

"scripts": {
  "typecheck": "tsc --project ./tsconfig.json --noEmit"
},

개별적으로 특정 스크립트 실행하려면

yarn workspace @jtwjs/web typecheck

Deploy

Github actions

  • .github/workflows 디렉토리 내에서 워크플로우(.yml)를 생성하여 배포 작업을 정의한다.

  • 모노레포의 경우 각 하위 프로젝트에 대해 배포 작업을 개별적으로 정의해야 한다.

  • 배포하기 전 관련 의존성을 모두 설치하는 작업이 추가 되어야 한다.

  • 관련 의존성을 설치하는 작업은 각 배포 워크플로우에서 공통으로 사용되기 때문에 composite actions를 사용하여 액션을 재사용해보자.

Composite actions

....

배포 하기 전 관련 의존성 설치하는 액션

// .github/actions/yarn-install/action.yml
name: 'Monorepo install (yarn)'
description: 'Run yarn install'

runs:
  using: 'composite'

  steps:
    - name: Expose yarn config as "$GITHUB_OUTPUT"
      id: yarn-config
      shell: bash
      run: |
        echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT

    - name: Restore yarn cache
      uses: actions/cache@v3
      id: yarn-download-cache
      with:
        path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }}
        key: yarn-download-cache-${{ hashFiles('yarn.lock') }}
        restore-keys: |
          yarn-download-cache-

    - name: Restore yarn install state
      id: yarn-install-state-cache
      uses: actions/cache@v3
      with:
        path: .yarn/ci-cache/
        key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}

    - name: Install dependencies
      shell: bash
      run: |
        yarn install --immutable --inline-builds
      env:
        YARN_ENABLE_GLOBAL_CACHE: 'false'
        YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
deploy.yml
name: CI-admin-app

on:
  push:
    branches:
      - main
      
    paths:
      - 'apps/admin/**'

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x]
    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 📥 Monorepo install
        uses: ./.github/actions/yarn-install

      - name: Build web-app
        working-directory: apps/admin
        run: |
          yarn build

workflow dispatch를 사용하여 깃헙 저장소에서 수동으로 workflow를 실행할 수 있다.

여러 워크스페이스를 관리하는 상황에서는 수동으로 배포할 워크스페이스를 선택하는 방식도 꽤 유용해 보인다.

workflow dispatch
name: CI-deploy-manual

on:
  workflow_dispatch:
    inputs:
      service_name:
        description: '배포할 서비스명을 선택해주세요.'
        required: true
        default: 'wanted'
        type: choice
        options:
          - wanted
          - admin

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x]
    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 📥 Monorepo install
        uses: ./.github/actions/yarn-install

      - name: Build web-app
        working-directory: apps/${{ inputs.service_name }}
        run: |
          yarn build

Last updated