Lerna with Yarn Workspaces (Monorepo)

What is it

Mono(단일) - Repository(저장소)

MonoRepo란 하나의 저장소에서 여러 컴포넌트 또는 module packages를 가지는 것을 의미합니다. 즉 여러 프로젝트의 코드를 보관하는 단일 저장소입니다. 현재 Google, Facebook 및 Twitter와 같은 대기업이 거대한 코드베이스의 복잡성을 관리하기 위해 선택된 방법들입니다.
아래는 그림은 제가 했던 프로젝트의 모노레포 예시입니다. eagle-web은 웹에 대한 로직 함수가 들어가 있고, web-ui는 ui 컴포넌트, eagle-backend는 nodejs가 들어 있습니다.

Lerna with Yarn Workspaces

Lerna와 Yarn을 같이 사용하는 이유는 각 라이브러리가 잘하는 일이 다르기 때문문입니다.

Lerna는 Monorepo로 구성된 프로젝트 전체를 관리, Yarn은 각 패키지들간의 의존성을 관리합니다.

💡 Lerna로만 관리하고자 할 때 react 프로젝트에서 lerna bootstrap --hoist로 패키지를 설치했을때 리액트 모듈을 중복해서 불러와 에러가 발생하여 많은 블로거들이 yarn과 lerna를 같이 사용하도록 권장합니다

Pros

  • Test, Build, Release process를 한 번에 진행 가능
  • 하나의 저장소에서 issue 처리
  • Module별로 개별적인 버전관리
  • Package manage에 등록하지 않고도 코드를 공유하기 쉬워짐

Cons

  • Learning curve & init setting
👉 새로운 개발자를 온 보딩하는 것은 더 작은 코드베이스를 하나씩 발견하는 대신 거대한 코드베이스에 즉시 직면하기 때문에 더 어려워집니다
  • Large repositories & Performance issue

How to use

✍️ practice code : https://github.com/Nanjangpan/MonoRepo_practice
  1. 새로운 프로젝트 생성

    • 새로운 폴더에서 새롭게 시작한다고 가정
    $ mkdir react-monorepo
    $ cd react-monorepo
    $ yarn init -y
    • 새로운 package.json 파일을 열어서 private, workspaces 항목을 추가합니다. (package들이 들어갈 디렉토리의 이름을 packages로 가정하였습니다)
    {
      "name": "react-monorepo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "private": true,
      "workspaces": ["packages/*"]
    }
  2. Lerna 설정

    • 모노레포 구성에 필요한 Lerna를 설치합니다. ( -W 옵션을 주지 않으면 프로젝트 루트 레벨에 패키지를 설치할 수 없습니다)
      $ yarn add -DW lerna
    • Lerna 설정 파일을 생성합니다.
      $ yarn lerna init
    • 생성된 lerna.json 파일을 열어서 useWorkspaces, npmclient 항목을 추가합니다. (useWorkspace는 Yarn Workspace 를 사용하겠다는 옵션이고, npmClient는 어떤 것을 패키지 관리 클라이언트로 사용할 것인지 결정하는 옵션입니다.)
      {
        "packages": ["packages/*"],
        "version": "1.0.0",
        "useWorkspaces": true,
        "npmClient": "yarn"
      }
  3. React app 추가

    • Create React App을 사용하여 React app package를 추가합니다
    $ yarn create react-app packages/app --typescript
    • 명령어를 실행하면 packages 폴더 내에 app으로 생성됩니다. 생성된 폴더 내의 package.json 파일을 열어 name을 변경합니다. 이 리액트 앱을 lerna 나 yarn workspace 에서 식별하기 위한 이름으로 @foo/app 으로 결정하면 아래와 같이 수정하면 됩니다.
    {
      name: '@foo/app',
      version: '0.1.0',
      private: true,
      dependencies: {
        '@types/jest': '24.0.23',
        '@types/node': '12.12.7',
        '@types/react': '16.9.11',
        '@types/react-dom': '16.9.4',
        // ...
      },
    }
    • 생성된 패키지(@foo/app)를 실행 합니다. yarn workspace
      $ yarn workspace @foo/app start
      lerna
      $ yarn lerna run start --scope=@foo/app
      💡 단일 명령을 사용하고 확인 할 때는 yarn workspace가 좋고, 필터를 사용해서 여러 패키지에 대한 작업을 수행 할 때는 lerna가 좋습니다.
  4. 라이브러리 패키지 추가

    • 방금 만든 앱에 별도의 패키지를 설치하여 그 패키지의 컴포넌트를 사용 해봅시다. packages 폴더 안에 shared 폴더를 생성합니다.
      $ mkdir -p packages/shared
      $ cd packages/shared
    • 프로젝트 생성할 때 처럼 yarn init 을 진행하고 이름을 @foo/shared로 변경합니다. 그 후 타입스크립트를 설치합니다.
      $ yarn init -y
      $ yarn add -D typescript
    • yarn tsc --init 를 통해 타입스크립트 설정 파일을 생성하고, jsx 옵션과 declaration 옵션을 활성화 후, outDir를 설정하여 빌드 파일이 생성될 위치를 지정합니다.
      {
        "compilerOptions": {
          "jsx": "react",
          "target": "es5",
          "module": "commonjs",
          "declaration": true,
          "declarationMap": true,
          "outDir": "./build",
          "strict": true,
          "esModuleInterop": true
        }
      }
    • package.json 파일을 열어서 main, types, files 를 수정하고 scripts에 build 를 추가합니다.
      {
        "name": "@foo/shared",
        "version": "1.0.0",
        "main": "build/index.js",
        "types": "build/index.d.ts",
        "files": ["build"],
        "license": "MIT",
        "devDependencies": {
          "typescript": "^3.7.2"
        },
        "scripts": {
          "build": "tsc"
        }
      }
    • src 폴더를 만들고 Foo.tsx 파일을 생성하여 간단한 컴포넌트를 작성합니다.
      import React from 'react';
      
      const Foo: React.FC = () => <div>Foo!</div>;
      
      export default Foo;
    • index.ts 파일을 추가하여 export 하는 내용을 작성합니다.
      export { default as Foo } from './Foo';
  5. app 패키지에 shared 패키지 설치

    • @foo/app 에 패키지를 추가해보자. 로컬 패키지끼리 의존 관계를 추가할 때는 lerna 를 사용합니다.
      yarn lerna add @foo/shared --scope=@foo/app
    • @foo/app 내에서 추가된 @foo/shared 패키지의 Foo 컴포넌트를 가져오도록 한 다음 실행하면 오류가 발생 합니다. 빌드를 하지 않으면 정상적으로 사용할 수 없습니다. 실제로 실행시켜야 하는건 자바스크립트 파일인데 @foo/shared 에서 제공하는건 타입스크립트 파일만 제공하기 때문입니다. 아래는 app/src/App.js 파일을 수정한 것입니다.
    import './App.css';
    import {Foo} from '@foo/shared' 
    
    function App() {
      return (
        <div className="App">
          <Foo></Foo>
        </div>
      );
    }
    
    export default App;
    $ yarn workspace @foo/app start 
    • @foo/shared 패키지를 빌드하고 실행하면 정상적으로 작동됩니다.
      $ yarn workspace @foo/shared build
      $ yarn workspace @foo/app start
    • 이 상태에서 @foo/shared 패키지 내의 컴포넌트를 변경해도 @foo/app 에서 변경 사항이 반영되지 않습니다. 아까 언급한대로 변경한 타입스크립트 파일이 자바스크립트로 변경 해야합니다. 이를 해결하려면 tsc -w 를 실행합니다. @foo/shared 패키지 scripts에 start를 추가합니다.
      {
        "name": "@foo/shared",
        "version": "1.0.0",
        "main": "build/index.js",
        "types": "build/index.d.ts",
        "files": ["build"],
        "license": "MIT",
        "devDependencies": {
          "typescript": "^3.7.2"
        },
        "scripts": {
          "build": "tsc",
          "start": "tsc -w"
        }
      }
    • @foo/shared 와 @foo/app 패키지의 start 명령을 동시에 실행합니다. 아래와 같이 별도의 --scope 플래그 같은 필터를 추가하지 않으면 모든 패키지를 대상으로 start 스크립트를 실행하면 @foo/shared 패키지의 변경 사항이 바로 반영되는 것을 확인할 수 있습니다.
      $ yarn lerna run start --parallel

Potential errors

계속 위와 같은 에러가 뜰경우, tsconfig.json 파일에

"skipLibCheck": true

위 내용을 추가해주면 에러가 사라집니다.

References

  1. Why Lerna and Yarn Workspaces is a Perfect Match for Building Mono-Repos – A Close Look at Features and Performance

https://doppelmutzi.github.io/monorepo-lerna-yarn-workspaces/

  1. Monorepo Tutorial

cy0c.github.io/2019/06/14/monorepo-tutorial/

  1. Monorepos in the Wild

https://medium.com/@maoberlehner/monorepos-in-the-wild-33c6eb246cb9

  1. TypeScript monorepo for React project

https://dev.to/stereobooster/typescript-monorepo-for-react-project-3cpa

  1. 모노레포로 리액트 프로젝트 만들기

https://imch.dev/posts/make-react-app-with-monorepo

  1. Monorepo for React Native
    https://medium.com/@ratebseirawan/react-native-0-63-monorepo-walkthrough-36ea27d95e26

좋은 웹페이지 즐겨찾기