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
-
새로운 프로젝트 생성
- 새로운 폴더에서 새롭게 시작한다고 가정
$ 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/*"]
}
-
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"
}
-
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가 좋습니다.
-
라이브러리 패키지 추가
- 방금 만든 앱에 별도의 패키지를 설치하여 그 패키지의 컴포넌트를 사용 해봅시다. 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';
-
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
- Learning curve & init setting
- Large repositories & Performance issue
How to use
✍️ practice code : https://github.com/Nanjangpan/MonoRepo_practice
-
새로운 프로젝트 생성
- 새로운 폴더에서 새롭게 시작한다고 가정
$ 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/*"]
}
-
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"
}
-
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가 좋습니다.
-
라이브러리 패키지 추가
- 방금 만든 앱에 별도의 패키지를 설치하여 그 패키지의 컴포넌트를 사용 해봅시다. 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';
-
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
새로운 프로젝트 생성
- 새로운 폴더에서 새롭게 시작한다고 가정
$ 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/*"]
}
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" }
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 workspace가 좋고, 필터를 사용해서 여러 패키지에 대한 작업을 수행 할 때는 lerna가 좋습니다.$ yarn lerna run start --scope=@foo/app
라이브러리 패키지 추가
- 방금 만든 앱에 별도의 패키지를 설치하여 그 패키지의 컴포넌트를 사용 해봅시다. 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';
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
계속 위와 같은 에러가 뜰경우, tsconfig.json 파일에
"skipLibCheck": true
위 내용을 추가해주면 에러가 사라집니다.
References
- 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/
- Monorepo Tutorial
cy0c.github.io/2019/06/14/monorepo-tutorial/
- Monorepos in the Wild
https://medium.com/@maoberlehner/monorepos-in-the-wild-33c6eb246cb9
- TypeScript monorepo for React project
https://dev.to/stereobooster/typescript-monorepo-for-react-project-3cpa
- 모노레포로 리액트 프로젝트 만들기
https://imch.dev/posts/make-react-app-with-monorepo
- Monorepo for React Native
https://medium.com/@ratebseirawan/react-native-0-63-monorepo-walkthrough-36ea27d95e26
Author And Source
이 문제에 관하여(Lerna with Yarn Workspaces (Monorepo)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@nanjangpan/Lerna-with-Yarn-Workspaces-Monorepo저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)