Yarn Berry 플러그인과 “since”

2021. 6. 10

🔌 Yarn Plugins

플러그인은 Yarn Berry에서 새롭게 추가된 기능입니다. 우리가 익히 들어본 다른 플러그인들과 마찬가지로, Yarn 플러그인은 Yarn에 선택적으로 특정한 부가적인 기능을 추가하거나 기존 기능을 개선할 수 있도록 만들어줍니다. 예를 들면,

  • 이벤트를 구독해 사이드 이펙트를 추가할 수 있습니다. Yarn 플러그인 에서는 hooks 라는 API에 접근할 수 있는데 이 hooks가 afterAllInstalled 같은 이벤트를 제공하기 때문에, 이벤트 시점에 특정 코드를 실행할 수 있습니다. Yarn의 공식 플러그인 중 하나인 typescript 는 이런 점을 이용해 새로운 패키지를 설치할 때 자동으로 @types/xxx 같은 타입 정의 패키지를 설치하는, 편리한 기능을 제공하는 플러그인입니다.
  • 새로운 명령어를 추가할 수 있습니다. 마찬가지로 Yarn에서 제공하는 공식 플러그인인 workspace-tools는 워크스페이스 목록을 순회하면서 주어진 명령어를 실행하는 foreach, 특정 워크스페이스에 해당하는 패키지만 설치하는 focus 등의 명령어를 제공합니다.
  • 새로운 프로토콜을 추가할 수 있습니다. Yarn에서는 현재 기본으로 몇 가지의 프로토콜을 제공하고 있습니다. (link: 혹은 workspace: 같은 프로토콜) 플러그인을 만들면 이렇게 Yarn에서 제공하는 프로토콜 이외에 새로운 프로토콜을 해석하게 만들 수 있습니다. 가령, toss: 프로토콜을 새로 만들어 패키지를 Yarn 이나 NPM 레지스트리가 아닌 토스의 S3 버킷에서 다운로드 받도록 만들 수 있습니다. Yarn에서는 공식 플러그인으로 exec 이라는, 스크립트를 즉시 실행하고 그 출력을 패키지로 사용하는 플러그인을 제공합니다. (exec: 으로 사용)
  • 이외에도 패키지를 실제로 파일 시스템의 어디에서 불러올지 결정하는 Linker, 버전을 어떻게 결정할지를 정하는 Resolver도 플러그인으로 구현할 수 있습니다.

그 외의 사용 예는 Yarn 플러그인 소개 페이지를 참고해주세요.

플러그인 사용하기

Yarn 플러그인은 하나의 JavaScript 번들 파일로 구성되기 때문에 쉽게 설치해 사용할 수 있습니다.

공식 플러그인은 다음과 같이 플러그인 이름을 통해 설치할 수 있습니다.

$ yarn plugin import workspace-tools
Code language: Bash (bash)

서드 파티 플러그인은 JavaScript 파일이 제공되는 주소를 그대로 넣으면 됩니다.

$ yarn plugin import <https://raw.githubusercontent.com/toss/yarn-plugin-workspace-since/main/bundles/%40yarnpkg/plugin-workspace-since.js>
Code language: Bash (bash)

플러그인이 제대로 설치되면 .yarnrc.yml 파일에 플러그인이 추가된 것을 확인할 수 있습니다.

plugins: - path: .yarn/plugins/@yarnpkg/plugin-workspace-since.cjs spec: "<https://raw.githubusercontent.com/toss/yarn-plugin-workspace-since/main/bundles/%40yarnpkg/plugin-workspace-since.js>"
Code language: YAML (yaml)

이렇게 설치된 플러그인의 번들 파일은 .yarn/plugins 내에 위치하게 됩니다.

설치할 때 사용할 수 있는 주소가 URL이 아닌 로컬 주소도 가능하기 때문에 다음과 같이 로컬에서 빌드하고 로컬에 있는 상대 경로를 이용해 바로 설치해서 테스트를 하는 것도 편리합니다.

$ yarn plugin import ../yarn-plugin-workspace-since/bundles/@yarnpkg/plugin-workspace-since.js
Code language: Bash (bash)

🛠 플러그인 만들기

Yarn에서는 플러그인을 쉽게 만들도록 도와주기 위해서 create-react-app 같은 스캐폴딩 도구를 제공합니다. @yarnpkg/builder 라는 이름의 도구인데, 설치 후 다음과 같이 명령어를 이용해 플러그인 코드를 간단하게 스캐폴딩할 수 있습니다.

$ builder new plugin some-plugin-name
Code language: Bash (bash)

이와 동시에, 플러그인 튜토리얼을 제공하고 있습니다. 튜토리얼인만큼 기초적인 내용이기는 하지만, 튜토리얼만 따라가더라도 대략적으로 감을 잡기에는 부족하지 않습니다.

커맨드라인 인터페이스 라이브러리, Clipanion

Clipanion은 Yarn의 리드 메인테이너이기도 한 Maël Nison이 만든 라이브러리입니다. 흔히 커맨드라인 인터페이스를 만들기 위해서 사용하는 Yargs 혹은 Commander.js와 같이 커맨드라인 인터페이스를 쉽게 만들 수 있도록 도와주는 라이브러리입니다. 다만 좀 더 선언적이고 클래스를 이용한 모던한 문법으로 커맨드라인 명령어를 만들 수 있어서 개발자 경험이 좋습니다. 가령 이런 형태로 명령어를 정의할 수 있습니다.

import { Command, Option } from 'clipanion'; class SomeCommand extends Command { static paths = [[ 'some' ]]; static usage = Command.Usage({ description: '테스트 명령어 입니다.', }) option = Option.String('--option') async execute() { this.context.stdout.write(`명령어 호출 성공! ${this.option}`); } }
Code language: TypeScript (typescript)

위와 같이 명령어를 만들었다면 다음과 같이 사용할 수 있을 겁니다.

$ some --option=안녕하세요 # "명령어 호출 성공! 안녕하세요"
Code language: Bash (bash)

Clipanion은 Yarn과 무관한 커맨드라인 인터페이스를 만드는데도 사용할 수 있지만, Yarn 명령어 플러그인을 만들기 위해서는 반드시 Clipanion 라이브러리를 사용해야 합니다. 실제로 Yarn 코어 커맨드라인 인터페이스 자체가 Clipanion 을 사용하고 있으니 안정성면에서는 이미 검증이 되었다고 볼 수 있겠죠.

만약 Clipanion을 사용하실 예정이라면 버전에 유의해주세요. 현재 최신 버전이 3.0 RC라서 사용이 꺼려질 수 있지만 실제로는 Yarn 코어에서 쓰고 있는 버전이기 때문에 안정성은 충분합니다. 2버전과 3버전의 API가 다른 부분이 많기 때문에 가급적 3버전을 선택하시기를 권장합니다.

문서화의 부재

Yarn 플러그인을 만들 때 가장 크게 느껴지는 문제는 레퍼런스가 거의 없다는 것입니다. 우선 제공되는 공식 문서 자체가 거의 없다시피 합니다. 위에 설명드린 튜토리얼이 거의 공식문서의 전부에요.

이 때문에 저는 플러그인을 만들때 다른 플러그인이 어떻게 만들어져 있는지를 레퍼런스 삼아 개발을 해야했고, 오픈소스 코드를 많이 참고해야만 했습니다.

대표적으로 참고할 수 있는 코드는 Yarn 의 공식 플러그인, workspace-tools가 있습니다. 그 외에도 Dcard/yarn-plugins 혹은 yarn.build 와 같은 플러그인도 참고할만 합니다.

Yarn core API 문서가 tsdoc을 이용해 생성되어 있지만 자동으로 생성된 문서가 늘 그렇듯 단순 나열되어있고 자세한 설명은 없어서 큰 도움은 되지 않았습니다. (core 쪽 사용할 일이 많지 않았지만, 몰라서 못 쓰는 거랑 알고도 안 쓰는 건 다르니까요.)

🏁 “since” 플러그인

토스 프론트엔드 챕터에서 가지고 있는 문제를 해결하기 위해서 Yarn 플러그인을 만들게 되었습니다. “since” 라는 이름의 플러그인인데요. 어떤 문제를 해결하는지, 왜 만들게 되었는지 알아보겠습니다.

모노레포의 문제

토스의 프론트엔드 챕터는 모노레포를 사용하고 있습니다. 모노레포 안에는 다양한 서비스들을 이루는 코드가 공존하고 있습니다. 이 때문에 해결해야할 과제가 생기는데, 바로 실제로 코드가 변경된 서비스만 통합/배포를 수행해야한다는 것입니다. 항상 모든 서비스를 대상으로 통합/배포가 일어난다면 걸리는 시간도 너무 길고 혼란스럽겠죠.

물론 이런 모노레포의 문제를 해결하기 위한 도구는 이미 존재할 뿐만 아니라 여러가지가 있어요. 대표적으로 Lerna가 잘 알려져 있습니다.

실제로 코드가 변경된 서비스만 통합/배포를 수행하는 것은 Lerna로 해결 가능한 문제이기도 합니다. 바로 @lerna/run 명령어와 @lerna/filter-options를 함께 사용하면 되죠.

$ lerna run test --since main
Code language: Bash (bash)

Lerna의 문제

하지만 그렇다고 Lerna를 그대로 사용할 수는 없었습니다. Lerna는 Yarn Berry를 지원하지 않고 있기 때문이죠. 정확하게는, workspace: 프로토콜을 지원하고 있지 않습니다.

이 문제는 어쩌면 필연적이기도 합니다. Yarn 등의 의존성 관리 도구와 Lerna 등의 모노레포 관리 도구가 의존성을 해석하는 방식에 대한 표준이 정해져 있지 않기 때문에 각 도구는 저마다의 방식으로 의존성을 해석하게 되는 것입니다. 결과적으로, 두 개의 서로 다른 의존성 그래프가 생기게 되겠죠.

이런 문제를 해결하기 위해서는 모노레포 관리 도구가 의존성을 직접 해석하려고 하면 안됩니다. 의존성을 해석하는 책임은 Yarn이라는 의존성 관리 도구에게 맡기도록 하고, 모노레포 관리 도구는 이렇게 만들어진 의존성 목록을 해석해서 원하는 동작을 실행시키는데 집중해야겠죠.

이미 Yarn Berry를 사용하고 있는 Babel도 마찬가지 접근 방식을 취했는데, 원래는 Yarn Berry의 patch: 프로토콜을 이용해서 Lerna를 패치해서 사용하고 있었지만, 이제는 Lerna를 걷어내고 Yarn Berry용 퍼블리싱 플러그인을 직접 만들어 사용하고 있습니다.

뿐만 아니라 Lerna는 더 이상 제대로 유지보수가 잘 되고 있지 않습니다. 꼭 Yarn Berry를 사용하지 않더라도 Lerna를 사용하는 것은 장기적으로 좋은 선택이 아닙니다.

토스 프론트엔드 챕터에서는 이 문제를 해결하기 위해서 Yarn Berry 기반의 플러그인을 만들게 되었습니다.

“since” 플러그인을 만든 과정

토스페이먼츠의 프론트엔드 레포지토리도 모노레포로 구성되어 있습니다. 원래는 Yarn v1 기반에 Lerna를 사용하고 있었어요. 하지만 Yarn Berry로 넘어가면서 마찬가지 문제점을 맞닥뜨리게 되었고, Lerna를 제거하게 되었습니다.

하지만 Lerna에서 제공하는 필터 옵션인 --since 가 필요했기 때문에, 그것과 비슷하게 동작하는 스크립트를 만들게 되었습니다. 처음엔 스크립트로 사용하다가 점점 다른 레포지토리에서도 비슷하게 동작하는 스크립트가 필요하게 되면서 스크립트를 분리해 Yarn Berry 플러그인으로 만들어야 겠다고 생각하게 되었어요.

그렇게 해서 yarn-plugin-workspace-since 라는 이름의 플러그인을 만들게 되었어요. 여기까지 보셨다면 이름에 왜 “since”가 들어가게 되었는지도 쉽게 이해하실 것 같네요. 이름이 길어서 “since”라고 줄여서 부르고 있습니다.

플러그인으로 만들어서 좋았던 점

  1. 코드 공유가 쉬웠습니다. 앞서 말씀드린대로 Yarn 플러그인은 명령어 한 줄에 쉽게 설치가 되니 플러그인 코드 공유가 정말 편리하다고 느꼈어요.
  2. 레포지토리와의 결합도가 낮아졌습니다. 플러그인은 범용적으로 사용할 것을 고려하고 만들게 되니, 플러그인의 역할에 대해 고민하게 되었고 결과적으로 사용하는 레포지토리와의 결합도가 낮아졌어요. 원하는 동작을 새로 구현할 때도 플러그인과 레포지토리, 둘 중 어느 쪽의 코드를 수정하는게 역할에 맞는지 고민하는 과정에서 좀 더 좋은 설계를 만들 수 있었다고 생각합니다.

이렇게 보니 라이브러리를 만들었을 때의 장점과 거의 동일하게 느껴지네요.

“since”의 기능

현재 “since” 플러그인은 세 가지 명령어를 제공하고 있어요.

  1. run: 앞서 언급드렸던 것처럼, 주어진 git ref부터 현재 HEAD 커밋까지의 변경사항을 감지하고 변경된 워크스페이스에서만 주어진 명령어를 실행하는 명령어입니다.
  2. listrun 이 명령어를 실행한다면, list 는 변경된 워크스페이스 목록을 단순히 반환합니다.
  3. version: 커밋을 읽어들인 후 Conventional Commit을 기반으로 패키지의 버전을 변경합니다. 가령 커밋에 fix 가 포함된 경우 그 스코프에 해당하는 워크스페이스 패키지에 patch 버전을 올립니다. semantic-release 도구와 비슷한 역할으로 봐주시면 될 것 같아요.

자세한 사용 방식은 레포지토리 README를 참고해주세요.

마치며

지금까지 Yarn Berry 플러그인과 “since” 플러그인에 대해서 알아보았습니다. 플러그인은 Yarn에서 지원하지 않는 동작을 구현할 수 있게 만들어주니, 현재 Yarn으로 풀리지 않는 문제가 있다면 해결하는 플러그인이 있는지 찾아보거나 직접 만드는 것을 고려해보시면 좋을 것 같아요.

“since” 플러그인을 만들게 된지도 벌써 3개월이 되었는데요. 그 동안 많은 시행착오를 하면서 이제는 꽤 안정적으로 동작한다고 생각합니다. 앞으로도 토스 프론트엔드 챕터에서 개밥먹기 하면서 지속적으로 버그 수정과 기능 개선을 할 예정입니다. 버그 제보, 기능 제안, 풀 리퀘스트도 언제든 환영합니다. 감사합니다.

이현섭

Frontend Developer

토스페이먼츠에서 가맹점을 위해 편리한 결제 SDK와 연동 문서를 개발하고 있습니다.

토스팀이 만드는 수많은 혁신의 순간들

당신과 함께 만들고 싶습니다.
지금, 토스팀에 합류하세요.
채용 중인 공고 보기