프로젝트를 Lerna로 초기화한다.
cd path/to/your-project
lerna init
다음과 같이 초기 디렉터리가 구성된다.
lerna.json
- version: 각 workspace에 대한 버전을 관리할 수 있다. 개별로 관리하고자 할 때는 "independent"를 입력한다.
- npmClient: "npm" 또는 "yarn"
- useWorkspaces: npmClient의 workspace로 관리되도록 한다.
workspace 추가
다음 명령을 실행해 새로운 workspace를 스캐폴딩할 수 있다.
lerna create <PACKAGE-NAME>
다음과 같이 client workspace를 생성할 수 있다.
lerna create client
workspace에 의존성 추가
아래와 같이 client workspace에 react 패키지를 설치할 수 있다. --scope 없이 사용하면 모든 workspace에 추가한다. 추가하려는 의존성이 workspace라면 해당 workspace를 제외한 모든 workspace에 추가된다.
현재 여러 의존성을 한번에 추가하는 기능은 지원되지 않는다.
client workspace에 react 패키지를 추가한다.
lerna add react --scope=client
client workspace에 common workspace를 추가한다.
lerna add common@0.0.0 --scope=client
루트 프로젝트에 의존성 추가
아래와 같이 lodash를 루트 프로젝트에 설치할 수 있다.
yarn add lodash --ignore-workspace-root-check
모든 workspace에 대해 npm 스크립트 실행
해당 스크립트가 포함된 각 workspace에서 npm 스크립트를 실행한다.
lerna run <COMMAND_NAME> -- [..args]
다음은 모든 workspace에 대해 test를 실행한다.
lerna run test
모든 workspace에 대해 임의 명령 실행
lerna exec -- <COMMAND> [..args]
다음은 모든 workspace 하위의 node_modules 폴더를 지운다.
lerna exec -- rm -rf ./node_modules
npm publish
자세한 옵션은 공식 문서를 참고한다.
lerna publish ## 마지막 릴리스 이후 변경된 패키지 publish
lerna publish from-git ## 현재 커밋에 태그가 지정된 패키지를 명시적으로 publish
lerna publish from-package ## 레지스트리에 최신 버전이 없는 패키지를 명시적으로 publish
버전 관리 및 CHANGELOG 작성 자동화
workspace에 변경을 가하고 나서 lerna version을 실행하면 각 workspace의 package.json에 명시된 버전을 자동으로 올리고 git에 tag를 남기고 변경에 대한 CHANGELOG도 작성한다.
더 많은 기능은 lerna-changelog를 참고한다.
lerna version
changelogPreset 필드를 이용하면 git commit message를 이용해 CHANGELOG를 자동으로 생성할 수 있다. 예시에서는 conventional-changelog-conventionalcommits 프리셋을 이용했다.
Lerna 명령어
그 외에도 변경 사항을 알아내거나 workspace 목록을 얻거나 lerna, npmClient, OS 등의 환경을 알아내는 등 다양한 기능을 제공한다. Lerna의 자세한 명령어는 다음을 참고한다.
- lerna publish
- lerna version
- lerna bootstrap
- lerna list
- lerna changed
- lerna diff
- lerna exec
- lerna run
- lerna init
- lerna add
- lerna clean
- lerna import
- lerna link
- lerna create
- lerna info
3. Nx
다음은 구글 개발자들이 만든 오픈소스 프로젝트인 Nx이다. Nx는 모노레포 구성을 위한 다양한 개발 도구를 제공하고 Angular, React와 같은 프런트엔드 프레임워크 기반의 개발 환경 구성뿐 아니라 Express, Nest.js와 같은 백엔드 기술 기반의 개발까지 폭넓게 지원하고 있다. 이뿐만 아니라 workspace 생성 시 Cypress, Jest 등을 기반으로 한 테스트 환경까지 설정해주기 때문에, 초기 모노레포 개발 환경 구축 비용을 크게 줄여준다.
그럼 Nx를 사용해서 간단하게 모노레포를 구성해 보고, 그 특징을 몇 가지 살펴보겠다.
새로운 Nx workspace 생성하기
아래의 명령을 입력해 새로운 Nx workspace 생성을 시작할 수 있다.
npx create-nx-workspace
Nx는 workspace 생성을 완료하기 위해, 다음과 같이 추가로 몇 가지 항목을 입력하도록 요구한다.
Nx Cloud의 경우 필수는 아니다.
Workspace name (e.g., org name) My-Workspace
What to create in the new workspace angular
Application name my-app
Default stylesheet format CSS
Use Nx Cloud? No
이 항목을 입력하고 나면 다음과 같은 구조의 Nx workspace가 생성된다.
생성된 workspace 구조에서 이를 구성하는 디렉터리의 특징을 간단히 알아보겠다.
- apps/*: 애플리케이션 프로젝트들이 위치한다. 우리가 처음에 생성한 애플리케이션도 여기에 들어있는 것을 볼 수 있다. Angular 기반으로 workspace를 생성했어도 꼭 Angular 프로젝트만 이 디렉터리에 들어갈 필요는 없으며 React 등 다른 프레임워크 기반의 코드도 공존할 수 있다.
- libs/*: 애플리케이션 전반에서 공통으로 사용할 코드를 여기에 작성한다.
- tools/*: 개발에 필요한 tooling script가 위치한다.
이제 생성한 애플리케이션을 실행해 보겠다. 애플리케이션은 다음과 같은 명령으로 실행할 수 있다.
npx nx serve <APP_NAME> // 주의: workspace 이름이 아닌 애플리케이션 이름
만약 전역(global)에 Nx가 설치되어 있다면 다음과 같은 명령으로 애플리케이션을 실행할 수도 있다.
nx serve <APP_NAME>
전역에 Nx 설치하기: npm install -g nx 또는 yarn global add nx
위의 명령 실행이 완료되었다면, localhost:4200에 접속해서 애플리케이션을 확인해 볼 수 있다.
Nx CLI vs Angular CLI
Angular 프로젝트 개발 경험이 있다면 Angular CLI(aka. ng)에 익숙할 것이다. 실제로, Angular 기반의 Nx workspace를 생성한 경우 대부분의 Nx 명령어는 ng ... 로 대체가 가능하고(예: ng serve) 그 결과 또한 동일하다.
하지만 Nx에서는 Nx CLI 사용을 권장한다. 그 이유는 Nx가 Nx CLI를 통해 제공하는 몇몇 기능이 있기 때문이다. 그 기능 중 하나인 Computation Cache에 대해선 나중에 살펴보겠다.
"Use Nx CLI over Angular CLI" - Nx.dev -
라이브러리 추가해보기
이번엔 애플리케이션 전반에서 사용할 수 있는 라이브러리를 추가해보겠다.
라이브러리는 목적과 특성에 따라 다음과 같이 네 가지로 나눌 수 있다.
- feature 라이브러리
- UI 라이브러리
- data-access 라이브러리
- utility 라이브러리
여기서는 간단하게 UI 라이브러리를 추가해 보겠다.
npx nx g @nrwl/angular:lib ui // ui: 라이브러리 이름
npx nx g @nrwl/react:lib ui // React의 경우
위 명령으로 ui라는 이름의 Angular 컴포넌트가 라이브러리에 추가되어 다음과 같은 구조가 만들어진다.
이제 생성된 UI 라이브러리 구조에 실제 View 역할을 할 컴포넌트를 추가하겠다.
npx nx g component first-lib --project=ui --export
다음 그림과 같이 ui 라이브러리 아래에 우리가 선언한 first-lib 컴포넌트 파일이 추가되었다.
생성한 UI 라이브러리 사용해보기
그럼 이제 라이브러리를 애플리케이션에서 사용하는 방법을 알아보겠다.
1. UI 모듈을 AppModule에 import하기
// in AppModule
...
import { UiModule } from '@my-workspace/ui'; // Import UiModule
...
@NgModule({
...
imports: [BrowserModule, UiModule], // Add UiModule in the importing list
...
})
export class AppModule {}
이때 라이브러리를 import하는 구문은 다음과 같다.
import <UI_MODULE_NAME> from @<WORKSPACE_NAME>/<LIBRARY_NAME>
Nx는 라이브러리가 추가될 때마다 tsconfig.base.json 파일에 TS 경로를 매핑하기 때문에 위와 같이 참조할 수 있다.
2. Application Template에서 UI 라이브러리 컴포넌트 사용하기
1번에서 모듈을 import했다면, 생성한 라이브러리의 컴포넌트를 Application Template에서 다음과 같이 사용할 수 있다.
<!-- in Application Template -->
<my-workspace-first-lib></my-workspace-first-lib>
라이브러리 생성 및 사용 시 주의할 점
에디터로 Visual Studio Code(이하 VS Code)를 사용하는 경우, 라이브러리를 생성 및 참조할 때 컴파일 오류가 발생하는 경우가 있다. 이는 VS Code의 TS 서버가 새로운 라이브러리를 바로 인식하는 데 실패하는 경우로, 이때는 VS Code를 재시작하는 것을 권장한다. 또한 새로운 라이브러리를 추가할 때마다 Nx 서버를 재시작해야 한다는 점을 주의한다.
Nx가 제공하는 도구
지금까지 Nx workspace의 생성, 라이브러리 추가와 사용에 대해서 간단히 알아보았다. 이어서 Nx가 제공하는 몇 가지 기능을 소개하겠다.
1. Project Graph
첫 번째는 Project Graph이다. 대부분의 프로젝트는 성장하면서 수많은 애플리케이션과 라이브러리가 생겨나고 점차 그 구성 요소 간의 의존 관계가 복잡해져 개발자 입장에서는 그 의존 관계를 파악하기가 점점 어려워진다. Nx는 이를 시각화하여 프로젝트 구성 요소 간의 관계를 파악하기 쉽도록 하는 Project Graph를 제공한다.
npx nx graph
위의 명령을 실행하여 현재 workspace의 전체 Project Graph를 확인할 수 있고, Nx가 제공하는 인터페이스를 통해 다양한 필터링도 가능하다. 아래는 위에서 진행한 예시 workspace의 Project Graph이다.
2. Computation Caching
Nx는 Nx CLI를 통해 내부적으로 Computation Caching을 제공한다. 이를 확인해보기 위해 nx build [appName] 명령을 실행해 보겠다.
빌드가 완료된 후, 위와 동일한 명령을 한 번 더 실행해 보았다.
이번에는 빌드가 바로 완료됨(21ms)을 볼 수 있고, 맨 아래 텍스트에서 볼 수 있는 것처럼 Nx는 로컬 캐시에 해당 Artifact가 존재하는 경우 가져다 쓰는 걸 알 수 있다. 단, 캐싱은 Nx CLI에서만 동작하기 때문에, Angular CLI를 사용하는 경우엔 Computation Caching이 적용되지 않는다.
3. Affected
다음은 Affected 스크립트이다.
npx nx affected
Nx는 코드를 수정했을 때 workspace의 어떤 부분이 영향을 받는지 알려주는 명령을 제공한다. Affected 스크립트는 필요에 따라 다음과 같이 다양하게 사용할 수 있다.
- npx nx affected:apps: 수정한 부분이 어떤 애플리케이션에 영향을 주었는지 표시한다.
- npx nx affected:libs: 수정한 부분이 어떤 라이브러리에 영향을 주었는지 표시한다.
- npx nx affected:test: 코드 수정에 의해 영향을 받은 부분에 대해서만 테스트를 실행한다.
다음은 UI 라이브러리를 수정한 뒤 위 명령을 실행한 모습이다.
4. VS Code용 Nx Console
마지막으로 Nx Console이다. Nx Console은 VS Code에서 Nx를 쉽게 사용하기 위한 GUI 도구다. Nx Command로 하는 모든 작업을 Nx Console로도 할 수 있도록 구성되어 있다. VS Code를 사용 중이라면 Nx Console을 사용하는 것을 추천한다.
다음은 VS Code에 Nx Console을 설치한 모습이다.
4. Turborepo
Turborepo는 대규모 모노레포 프로젝트 관리에서 오는 피로감과 부수적인 툴링에 대한 부담을 줄이면서, Google이나 Facebook과 같은 큰 기업에서 사용하는 수준의 개발 경험을 주는 데 포커싱한, Vercel에서(2021년 12월에 인수) 개발 및 운영하고 있는 JavaScript/TypeScript를 위한 모노레포 빌드 시스템이다.
Turborepo는 증분 빌드(incremental build), 원격 캐싱, 병렬 처리 기법을 통해 빌드 성능을 끌어올리고, Pipeline의 쉬운 설정과 profiling, trace 등 다양한 시각화 기능을 제공해 관리 편의성을 높인 것이 특징이다.
그리고 기존의 모노레포 프로젝트에 점진적으로 Turborepo를 적용할 수 있으며, 쉽게 Turborepo로 구성된 새 모노레포 프로젝트를 생성할 수 있도록 스캐폴딩 기능을 제공한다. 또한 패키지 매니저로 yarn, npm, pnpm와 함께 잘 동작하므로 프로젝트 상황에 맞게 선택해 사용할 수 있다.
Turborepo가 어떻게 구성되어 있는지 먼저 살펴보고, 앞서 언급한 특징을 설명하면서, 어떻게 사용하는지 설명하겠다.
create-turbo 구성 확인하기
아래의 명령어를 통해 새 모노레포 프로젝트를 생성해 보자.
$ npx create-turbo@latest <PACKAGE_NAME>
위 그림과 같이 /app 하위에 docs, web 앱과 공통 패키지인 /packages 하위의 ui, config 패키지가 있는 프로젝트가 생성된다. 이 구조는 일반적인 모노레포 프로젝트 구조와 유사해 쉽게 Turborepo를 사용할 수 있도록 한다.
Vercel사에서 운영하는 프로젝트답게 앱은 기본적으로 Next.js(react)로 설정된다. 그외에 Express, Remix, pnpm 등 다양한 기술 스택으로 이루어진 예시도 제공하고 있으므로 필요에 따라 참고할 수 있다.
캐싱
이미 계산한 것들은 다시는 계산하지 않는다는 핵심 컨셉 아래 캐싱이 이루어지고 있다. 이전에 실행한 파일 및 로그를 캐싱해, 만약 현재 실행 태스크에 이미 완료한 작업이 있다면 이것을 건너뛰기 때문에 작업 실행 시간을 효과적으로 줄일 수 있다.
앞서 만든 프로젝트에서 빌드를 진행해 보자.
$ yarn turbo run build
그 다음 위 명령어로 다시 빌드를 하면 다음과 같이 시간이 대폭 줄어든 것을 확인할 수 있다.
그렇다면 이 캐시들은 어떻게 저장되고 있을까?
빌드를 하면 /app 하위 패키지들의 .turbo 디렉터리에 각각 로그 파일이 생기고 이 로그에는 해당 빌드에 대한 hash가 저장된다.(Turborepo는 빌드해야 할 항목을 파악하기 위해 타임스탬프를 확인하고 활용하는 대신 파일의 내용에 따른 hash 정책을 사용한다.) 이 hash 값들은 아래 이미지와 같이 node_modules/.cache/turbo 하위에 hash로 구분된 스냅샷 폴더와 매칭된다.
이렇게 모든 작업에 대한 캐싱을 진행하기 때문에 변화된 부분만 재빌드하고 나머지는 캐싱한 것을 사용하면서 Turborepo는 작업 속도를 높일 수 있다.
원격 캐싱
위에서 설명한 캐시는 로컬 환경에서만 유효하다는 한계가 있다. 그러나 Turborepo는 이 한계를 극복하는 원격 캐싱이라는 강력한 기능을 제공한다.
더 빠른 빌드를 위해 빌드 캐시를 클라우드에 올려서 팀원 및 CI/CD시스템이 공유해 사용할 수 있어 대규모 프로젝트에서 다수의 팀이 협업하는 환경에서 개발 효율성을 높일 수 있다.
실제로 원격 캐싱을 한 상태에서 빌드를 실행하면 "Remote computation caching enable" 문구와 함께 빌드 시간이 9초에서 3초로 비약적으로 감소하는 것을 확인해볼 수 있다.
- 로컬 캐시가 없는 상태에서의 빌드 시간
- 로컬 캐시는 없고 원격 캐시가 있는 상태에서의 빌드 시간
다만 이 기능은 현재 experimental(beta) 상태이며 Vercel 클라우드를 활용하기 때문에 Vercel의 계정이 필요하다.
공식 문서를 참고해 원격 캐싱 방법을 참고해 볼 수 있다.
Pipeline Package Task
Pipeline은 각 패키지의 package.json 스크립트(태스크) 간 작업 관계를 정의한다. 이를 통해 새로 들어온 개발자도 작업 관계를 쉽게 이해할 수 있다.
Pipeline 설정은 Turborepo turbo.json에서 확인할 수 있다.(package.json에도 설정 가능하나 turbo.json에 설정하도록 권장하고 있다.)
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"deploy": {
"dependsOn": ["build", "test", "lint"]
},
"lint": {}
}
}
위 설정에서 dependOn은 pipeline key에 해당하는 작업이 의존하는 작업을 의미한다. 이 부분에 접두사 ^가 붙으면 각 패키지에 있는 작업을 의미한다.
lint는 의존 관계와 상관없이 언제나 수행될 수 있으며, deploy는 Pipeline에서 build, test, lint가 선행되어야 수행됨을 의미한다.
Pipeline으로 인해 Turborepo의 스케줄러는 기존의 모노레포에 비해 성능이 강력하다. 한 번에 한 테스크씩 수행되는 기존 방식과 다르게 의존성이 없는 작업은 유휴 CPU에서 실행되기 때문에 빌드 시간이 긴 경우에 작업 성능이 비약적으로 높아진다.
다음 그림을 살펴보자.
A, B, C 세 개의 패키지로 구성된 프로젝트가 있다. A와 C는 B에 의존한다.
lint, build, test, deploy 작업을 순차적으로 수행한다고 했을 때 Lerna의 경우에 A, C 패키지는 B의 build가 끝날 때까지 build를 수행할 수 없다.
이에 반해 Turborepo는 build에 의존 관계가 없는 test 작업이 병렬적으로 수행된다.
Profile
위에서 언급한 멀티 스레드를 활용한 병렬 작업 처리가 실제로 어떻게 CPU 스레드를 사용해서 작업을 처리하는지 --profile 플래그를 통해 시각화해 확인할 수 있다.
## 이미 작업을 진행한 경우 더 명확한 profiling을 위해 --force 플래그를 추가한다.
$ yarn turbo run build lint --profile [--force]
위 명령을 실행하면 루트 디렉터리에 ****-profile.json 파일이 생성된다. 이 파일을 Chrome 개발자 도구의 Performance 탭에 업로드해 스레드에서 작업이 어떤 순서로 얼마만큼의 시간이 걸리는지 확인할 수 있다.
이 밖에도 --trace, --heap, --cpuprofile 등의 플래그를 사용해 다양한 부분에서 profiling이 가능하다. 자세한 사용 방법은 공식 문서에서 확인할 수 있다.
의존성 그래프 시각화
Nx와 마찬가지로 Turborepo도 프로젝트 내 패키지 간 작업 관계를 쉽게 파악할 수 있게 시각화하는 기능을 제공한다. 기본적으로 JPEG 형식의 이미지로 그래프가 그려지며 SVG, HTML, JSON, PDF 등 다양한 형식의 산출물을 지원한다.
먼저 graphviz가 설치되어 있어야 한다.
$ turbo run build --graph
$ turbo run build test lint --graph=my-graph.html
다음은 위 명령어를 각각 실행해 얻은, 패키지 간 작업 관계를 시각화한 이미지다.
이 밖에도 CPU trace, heap trace, CPU profile에 대한 시각화 기능도 제공한다. 자세한 내용은 CLI Reference를 참고한다.
5. 마치며
위에 소개한 Yarn, Lerna, Nx 그리고 Turborepo를 간단히 비교하면 다음 표와 같다.
표에서 보이는 것처럼 Yarn은 다른 모노레포 도구에 비해 지원하는 것들이 많지는 않지만, 모노레포 사용의 목적이 단순히 공통 요소를 공유하는 것이라면 Yarn으로 workspace을 구성해서 개발을 진행하는 것을 추천한다.
하지만 단순한 코드의 공유 외에 패키지 간 의존성 관리 및 테스트, 배포 등의 작업에 대한 더 나은 무언가를 필요로 한다면 Lerna를 함께 사용해보는 것도 좋은 선택지가 될 것이다.
그리고 프로젝트가 성장하면서 개발, 관리에 유용한 더 많은 기능이 필요하다면 Nx와 Turborepo를 고려해보면 좋을 것 같다. Nx와 Turborepo는 모두 캐싱 기능을 지원해서 빌드 측면에서 우수한 속도를 자랑하고, 의존성 그래프 시각화 기능을 통해 프로젝트의 구성 요소들이 서로 어떤 의존관계를 갖고있는 지 한눈에 파악이 가능하도록 해준다.
둘 중 좀더 가벼운 시작을 원한다면, Zero Config를 지향해 초기 설정이 상대적으로 쉬운 Turborepo를 시도하는 것도 좋을 것이다. 상대적으로 출시된 지 오래되어 관련 레퍼런스, 지원하는 IDE 플러그인 등의 풍부한 생태계가 형성되어 있는 Nx 또한 좋은 선택지가 될 수 있다.
'실전 단아 개발 가이드' 카테고리의 다른 글
github repository 삭제 (0) | 2024.01.18 |
---|---|
react-app-rewired 라이브러리? (0) | 2024.01.18 |
오프라인 캐시 (0) | 2024.01.18 |
React ref 사용, DOM 접근 방법 (0) | 2024.01.18 |
yarn workspace (0) | 2024.01.18 |