Lerna

모노레포 관리 도구 - Lerna

Seongmin 2024. 12. 22. 21:28
반응형

1. Lerna란?

Lerna는 JavaScript 및 TypeScript 프로젝트에서 모노레포(Monorepo)를 효율적으로 관리하기 위한 도구입니다. 대규모 프로젝트에서 여러 패키지를 하나의 리포지토리에서 관리할 수 있도록 설계된 Lerna는 개발 및 배포 프로세스를 간소화하고, 의존성 관리의 복잡성을 해결하는 데 도움을 줍니다. 특히, NPM 또는 Yarn 워크스페이스를 기반으로 패키지 간 의존성을 최적화하고, 배포를 자동화하는 데 강력한 기능을 제공합니다.

 

2. Lerna의 주요 기능

  1. 패키지 관리: 패키지 간 의존성을 설치 및 연결하고 의존성 업데이트를 자동화합니다.
  2. 배포 관리: 변경된 패키지만 배포하여 시간과 리소스 절약합니다. 또한, 버전 관리 및 자동 배포 기능을 지원합니다.
  3. 빌드 및 테스트 관리: 특정 패키지 또는 전체 패키지에 대해 빌드 및 테스트가 가능하고 병렬 실행, 캐싱을 지원합니다.
  4. NPM과 Yarn 워크스페이스 통합: 기존 워크스페이스와 쉽게 연동할 수 있습니다.

3. Lerna 설치 및 사용법

Lerna는 패키지 매니저를 통해서 간단히 설치할 수 있습니다. 

npm install -g lerna

 

Lerna 프로젝트를 초기화 합니다. 이 명령을 실행하면 워크스페이스 설정이 추가된 package.json과 Lerna 관련 설정 파일인 lerna.json이 생성됩니다.

npx lerna init

 

이제 워크스페이스에 새 패키지를 생성합니다. 이 명령을 실행하면 packages 폴더에 패키지를 생성하고 package.json을 포함하는 기본적인 파일들이 생성됩니다. package.json에 포함될 내용은 명령을 실행하면 대화형으로 입력하게 됩니다.

npx lerna create <package-name>

 

4. Lerna 의존성 관리

원래 Lerna는 의존성 관리를 위한 명령어를 지원했지만, v7부터 이러한 명령어들이 제거되었습니다. Lerna가 처음 개발되었을 당시에는 패키지 매니저에서 워크스페이스를 구성하거나 여러 패키지를 관리하는 기능이 제공되지 않았습니다. 따라서 Lerna의 의존성 관리 명령어는 프로젝트 운영에서 중요한 역할을 했고, 오랜 시간 유지되었습니다. 하지만 시간이 지나면서 npm, Yarn, pnpm과 같은 패키지 매니저에 워크스페이스 관리 기능이 추가되었고 이에 따라 Lerna는 중복 기능을 제거하고, 더 가벼운 도구로 발전하기 위해 해당 명령어들을 삭제하기로 결정했습니다. 자세한 내용은 Lerna 공식 문서를 참고하면 확인할 수 있습니다.

Lerna는 JavaScript 생태계에서 최초의 모노레포/워크스페이스 도구입니다. 2015/2016년에 만들어졌을 때 생태계는 완전히 달랐고, 단일 저장소("워크스페이스")에서 여러 패키지 작업을 처리할 수 있는 기본 제공 기능이 없었습니다. lerna bootstrap, lerna add, lerna link와 같은 명령은 모두 lerna 프로젝트의 중요한 부분이었습니다. 다른 옵션이 없었기 때문입니다. 중요한 정신적 변화는 lerna가 repo에 종속성을 설치하고 연결하는 데 책임이 없다는 것을 인식하는 것입니다. 패키지 관리자가 그 작업에 훨씬 더 적합합니다. 

 

하지만 관련 명령어를 사용하고 싶다면 @lerna/legacy-package-management 패키지를 설치하면 됩니다. 이것은 단지 임시방편일 뿐이며, 이 새로운 패키지는 유지 관리 모드에 있는 것으로 생각할 수 있습니다. 기존 패키지 관리 문제로 인한 새로운 기능은 고려되지 않으며, 중요한 패치와 보안 업데이트만 병합할 것입니다.

npm install -g @lerna/legacy-package-management

npx lerna bootstrap

npx lerna add

npx lerna link

 

5. Lerna 주요 명령어

1. lerna run <script>

lerna run은 패키지의 스크립트를 실행하기 위한 명령어 입니다. 아래의 명령은 모든 패키지에 대해서 "start" 스크립트를 실행합니다.

# npx lerna run <script>
npx lerna run start

 

--scope 옵션을 사용하여 특정 패키지를 선택하거나 --ignore 옵션을 사용하여 특정 패키지를 제외할 수 있습니다. glob 패턴을 사용하기 때문에 여러 개의 패키지를 선택할 수 있습니다.

# package-a의 "start" 스크립트를 실행
npx lerna run start --scope=package-a

# package-로 시작하는 패키지의 "start" 스크립트를 실행
npx lerna run start --scope="package-*"

# package-a를 제외한 패키지의 "start" 스크립트를 실행
npx lerna run start --ignore=package-a

# package-로 시작하는 패키지를 제외한 패키지의 "start" 스크립트를 실행
npx lerna run start --ignore="package-*"

 

--since 옵션을 사용하면 특정 브랜치나 태그 이후 변경된 패키지만을 선택할 수 있습니다. --since 옵션에 아무 값도 전달하지 않으면 기본적으로 가장 최신 태그를 기준으로 실행합니다. 테스트를 위해서 일부 패키지를 생성하고 커밋한 후 새로운 패키지를 추가하고 테스트를 해보겠습니다. 이때 새로운 패키지의 내용은 git에 staged된 상태이어야 합니다.

# 패키지를 추가합니다.
lerna create package-a
lerna create package-b

# 추가한 패키지를 커밋합니다.
git add *
git commit -m "first commit"

# 새로운 패키지를 추가하고 git에 staging 합니다.
lerna create package-c
git add *

# package-a,b,c에 "start" 스크립트를 추가하고 아래의 명령어를 실행합니다.
# package-a,b는 변경사항이 없기 때문에 실행되지 않고 package-c만 실행되는 것을 확인할 수 있습니다.
lerna run start --since master

# 태그를 추가했다면 태그를 지정하면 됩니다.
lerna run start --since v1.0.0

 

npm에서 비슷한 동작을 실행하려면 --workspace, --workspaces 옵션을 사용하면 됩니다. glob 패턴을 지원하지 않아서 여러 패키지를 대상으로 하려면 --workspace 옵션을 추가적으로 입력하거나 --workspaces를 사용해야 합니다. --since와 같은 옵션은 지원하지 않습니다.

# package-a의 "start" 스크립트를 실행
npm run start --workspace=package-a

# package-a, package-b의 "start" 스크립트를 실행
npm run start --workspace=package-a --workspace=package-b

# 모든 패키지의 "start" 스크립트를 실행
npm run start --workspaces

 

--scope, --ignore, --since와 같은 필터 옵션은 모든 명령어에서 사용할 수 있습니다.

 

2. lerna exec -- <command>

lerna exec은 특정 명령어를 실행하기 위해 사용합니다. 주로 CLI에서 실행하던 명령어를 실행하기 위해 사용합니다. 아래의 명령은 모든 패키지의 파일 목록 출력합니다.

# lerna exec -- <command
lerna exec -- ls -al

 

lerna exec을 사용해서 스크립트를 실행할 수 있습니다. 하지만 lerna run을 쓰는게 더 효율적이니 그냥 "실행이 가능하구나" 정도로 참고하시면 됩니다. 차이점이라면 lerna run은 스크립트가 없는 패키지는 자동으로 건너뜁니다. 반면, lerna exec은 해당 스크립트가 없는 경우 관련 에러를 그대로 출력합니다.

lerna exec -- npm run start

 

3. lerna version

lerna version은 현재까지 변경사항이 발생한 패키지의 버전을 수정하고 커밋합니다. 그리고 수정한 버전에 해당하는 태그를 생성하고 리모트에 푸시합니다. 여기서 변경사항은 이전 태그가 지정된 릴리스 이후에 발생한 커밋을 말합니다.

lerna version

 

위와 같이 아무 매개변수 없이 실행하면 릴리즈 버전을 선택하는 프롬프트 표시됩니다. 아래와 같이 매개변수를 전달하면 버전을 선택하는 프롬프트를 건너뛰고 키워드에 해당하는 버전으로 수정합니다. 이때도 버전이 어떻게 변경될 지 프롬프트에 표시됩니다. 만약에 모든 프롬프트를 건너뛰고 싶다면 --yes 옵션을 추가합니다.

# lerna version [major | minor | patch | premajor | preminor | prepatch | prerelease]

# 1.0.0 -> 2.0.0
lerna version major

# 1.0.0 -> 1.1.0
lerna version minor

# 1.0.0 -> 1.0.1
lerna version patch

# 1.0.0 -> 2.0.0-alpha.0
lerna version premajor

# 1.0.0 -> 1.1.0-alpha.0
lerna version preminor

# 1.0.0 -> 1.0.1-alpha.0
lerna version prepatch

# 1.0.0 -> 1.0.1-alpha.0 -> 1.0.1-alpha.1 ...
lerna version prerelease

# 1.0.0 -> 1.0.1-next.0
lerna version prerelease --preid next

 

--amend 옵션을 사용하면 새로운 커밋을 생성하지 않고 기존 커밋에 변경사항을 반영합니다. 또한 의도치 않은 덮어씌기를 막기 위해서 리모트에 푸시하지 않습니다.

lerna version major --amend

# 아래와 같은 메시지가 출력되는 것을 확인할 수 있습니다.
...
lerna info execute Skipping git push
lerna info execute Skipping releases
lerna success version finished

 

--conventional-commits 옵션은 커밋 메시지를 분석하여 변경 내용을 파악하고 이를 통해 버전 증가를 자동으로 결정합니다. 또한 CHANGELOG.md를 생성하거나 업데이트 합니다. 만약에 CHANGELOG.md를 생성하지 않으려면 --no-changelog 옵션을 추가합니다.

lerna version --conventional-commits --no-changelog

 

커밋 메시지는 Conventional Commits 규칙을 기반으로 작성되어야 하며 커밋 메시지에 따라 증가되는 버전은 대략적으로 아래와 같습니다.

타입 설명 버전 증가
feat 새로운 기능 추가 Minor 버전 증가
fix 버그 수정 Patch 버전 증가
perf 성능 개선 Patch 버전 증가
docs 문서 업데이트 버전 증가 없음
style 코드 스타일 변경 (기능에 영향 없음) 버전 증가 없음
refactor 코드 리팩터링 (기능 변경 없음) 버전 증가 없음
test 테스트 추가 또는 수정 버전 증가 없음
BREAKING CHANGE 호환되지 않는 변경 사항 Major 버전 증가

 

--conventional-commits와 함께 --conventional-prerelease를 사용하면 프리릴리즈 버전을 생성합니다. 이후 프리릴리즈를 정식 버전으로 릴리즈할 때 --conventional-graduate를 사용하면 됩니다.

# 1.0.0 -> 1.1.0-alpha.0
lerna version --conventional-commits --conventional-prerelease

# 1.1.0-alpha.0 -> 1.1.0
lerna version --conventional-commits --conventional-graduate

 

프리릴리즈 버전에서 한번 더 프리릴리즈를 수행하면 프리릴리즈 버전이 증가하게 됩니다. 만약에 새로운 버전으로 프리릴리즈 하고 싶다면 --conventional-bump-prerelease를 사용하면 됩니다.

# 1.0.0-alpha.0 -> 1.0.0-alpha.1
lerna version --conventional-commits --conventional-prerelease

# 1.0.0-alpha.0 -> 1.1.0-alpha.0
lerna version --conventional-commits --conventional-bump-prerelease

 

lerna version을 통해 생성되는 커밋에는 버전이 메시지로 입력됩니다. 만약에 커밋 메시지를 지정하고 싶다면 --message를 사용하면 됩니다. 메시지 내에서 %s(v1.0.0)나 %v(1.0.0)를 통해 버전을 참조할 수 있습니다.

lerna version --message "chore(release): publish %s"

lerna version -m "chore(release): publish %s"

 

4. lerna publish

lerna publish는 패키지들을 NPM에 배포하는 데 사용하는 명령어 입니다. 이 명령어를 실행하면 아래의 작업 중 하나를 실행합니다.

  • 이전 릴리스 이후 업데이트된 패키지를 배포 (내부적으로 lerna version 호출)
  • 현재 커밋에 태그가 있는 패키지 배포 (from-git)
  • 레지스트리에 없는 최신 커밋의 패키지 배포 (from-package)
  • 버전이 없는 "canary" 릴리스 배포

lerna publish를 실행하면 내부적으로 lerna version을 호출합니다. 그렇기 때문에 lerna publish를 실행하면 lerna version이 실행된 후 패키지를 배포합니다. 이 명령어의 작업은 위에서 설명한 작업 중 첫 번째인 "이전 릴리스 이후 업데이트된 패키지를 배포 (내부적으로 lerna version 호출)"에 해당합니다.

lerna publish

 

만약 lerna version을 수동으로 실행했다면, 이후 lerna publish를 실행할 때 from-git 매개변수를 추가해야 합니다. 이 명령어의 작업은 위에서 설명한 작업 중 두 번째인 "현재 커밋에 태그가 있는 패키지 배포 (from-git)"에 해당합니다. lerna version을 실행한 후 from-git 매개변수 없이 lerna publish를 실행하면 아무 작업도 수행하지 않습니다. 그 이유는 lerna version이 실행되면서 이미 새로운 Git 태그를 생성했고, 이 태그는 이전 릴리스의 기준점으로 설정되었기 때문입니다. 이후로 추가적인 변경사항이 발생하지 않았으므로 배포 대상 패키지가 없다고 판단합니다.

git commit -m "feat: some feature"

lerna version --conventional-commits -m "chore(release): publish %s"

lerna publish from-git

 

from-git은 태그를 기반으로 동작하기 때문에 현재 커밋에 태그가 없다면 배포되지 않습니다. 이럴 때 from-package 매개변수를 사용할 수 있습니다. from-package는 package.json의 version을 확인하여 NPM 레지스트리와 비교하고 해당 버전이 NPM 레지스트리에 아직 배포되지 않았다면 배포합니다. 이 명령어의 작업은 위에서 설명한 세 번째인 "레지스트리에 없는 최신 커밋의 패키지 배포 (from-package)"에 해당합니다.

git commit -m "feat: some feature"

# package.json의 버전을 수동으로 수정한 후 from-package를 통해 배포합니다.
lerna publish from-package

 

만약에 lerna version을 실행한 후 lerna publish를 실행하지 않고 업데이트 후 다시 lerna version을 실행했다면 현재 커밋이 마지막 릴리즈가 되기 때문에 이전 버전은 배포할 수 없습니다. 이런 경우에 이전 버전을 배포하고 싶다면 해당 커밋으로 돌아간 후 배포를 실행해야 합니다. 

# 태그 v1.0.1을 배포하지 않았다고 가정합니다.
# 태그 v1.0.1이 존재하는 커밋으로 이동합니다.
git checkout v1.0.1

# 다시 배포를 실행합니다.
lerna publish from-git

 

--canary 옵션은 임시 테스트 배포할 때 사용합니다. Prerelease는 정식 릴리즈가 될 가능성이 있는 버전을 시험 배포하는 것으로, 그 대상은 정식 릴리즈와 크게 다르지 않습니다. 반면, Canary는 내부 개발자 또는 제한된 그룹을 대상으로 빠르게 변경 사항을 테스트하고 문제를 수정하기 위한 배포라고 생각하면 됩니다. Canary 배포는 정식 릴리스와 별개의 임시 버전으로 관리되며, 문제 발견 시 쉽게 폐기할 수 있습니다.

# 1.0.0 => 1.0.1-alpha.0+${SHA}
lerna publish --canary

# 1.0.0 => 1.0.1-beta.0+${SHA}
lerna publish --canary --preid beta

 

5. lerna changed

lerna changed는 커밋 내역을 기반으로 다음 릴리즈 대상이 되는 패키지 목록을 출력합니다.

lerna changed

 

--conventional-graduate 옵션을 사용하면 프리릴리즈 상태에서 정식 릴리즈 대상으로 승격할 패키지를 확인할 수 있습니다.

# 0.x.x, 1.0.0-alpha.1, 2.1.0-beta.2 등이 포함됩니다.
lerna changed --conventional-graduate

 

6. lerna diff

lerna diff는 마지막 릴리즈 이후 변경사항을 출력합니다. 내부적으로 git diff를 실행합니다.

# 모든 패키지의 마지막 릴리즈 이후의 변경사항을 출력합니다.
lerna diff

# package-a의 마지막 릴리즈 이후의 변경사항을 출력합니다.
lerna diff package-a

 

6. 왜 Lerna를 사용해야 할까?

Lerna는 무엇이고 어떠한 기능을 가지고 있는지, 그리고 이러한 기능을 수행하기 위해서 어떠한 명령어를 지원하는지 알아보았습니다. Lerna의 기능 대부분은 사실 npm이나 git만으로도 수행할 수 있습니다. 그러나 Lerna는 이러한 작업들을 더 편리하고 체계적으로 수행할 수 있도록 지원한다는 점에서 매우 유용합니다. 특히, npm과 git을 조합하여 작업해야 하는 경우에도, Lerna만으로 모든 작업을 간단하게 처리할 수 있다는 것이 큰 장점입니다.

개인적으로는 모노레포를 처음 운영해본다면 먼저 Lerna와 같은 도구 없이 운영해보는 것을 추천합니다. 모노레포를 직접 운영해보면서 필요한 점과 불편함을 체감한 후, Lerna 같은 도구를 도입하면 더 큰 효과를 얻을 수 있기 때문입니다. 반대로, 이미 모노레포를 운영해본 경험이 있고, 이 글을 보면서 "그때 이런 불편함이 있었지"라는 생각이 드셨다면, Lerna 도입을 적극 검토해보는 것을 추천합니다. Lerna는 모노레포 관리의 복잡함을 효과적으로 줄여줄 도구입니다.

반응형