pub 워크스페이스(Workspaces)

pub 워크스페이스(Workspaces)

하나의 버전 관리 저장소(모노레포)에서 여러 Dart 패키지를 함께 개발할 때가 있죠. pub의 워크스페이스는 모든 패키지에 대해 하나의 공유 의존성 해석을 사용해서 저장소를 정리할 수 있게 해 줘요. 어떻게 쓰는지 살펴볼게요.

출처: Pub workspaces

본문

프로젝트를 진행하다 보면 같은 버전 관리 저장소(모노레포)에서 여러 Dart 패키지를 개발할 수 있어요. 예를 들어 다음과 같은 디렉토리 구조를 가질 수 있겠죠.

/
├─ packages/
│  ├─ shared/
│  │  ├─ pubspec.yaml
│  │  ├─ pubspec.lock
│  │  ├─ .dart_tool/
│  │  └─ package_config.json
│  ├─ client_package/
│  │  ├─ pubspec.yaml
│  │  ├─ pubspec.lock
│  │  ├─ .dart_tool/
│  │  └─ package_config.json
│  └─ server_package/
│     ├─ pubspec.yaml
│     ├─ pubspec.lock
│     ├─ .dart_tool/
│     └─ package_config.json
└─ pubspec.yaml
   ├─ pubspec.lock
   └─ .dart_tool/
      └─ package_config.json

이 구성에는 몇 가지 단점이 있어요.

  • 패키지 하나마다 dart pub get을 한 번씩 실행해야 해요.
  • 각 패키지가 서로 다른 버전의 의존성을 갖게 될 위험이 있어서, 패키지 사이를 오갈 때 혼란이 생길 수 있어요.
  • IDE에서 루트 폴더를 열면 dart 분석기가 패키지마다 별도의 분석 컨텍스트를 만들어 메모리 사용량이 늘어나요.

pub는 모든 패키지에 대해 하나의 공유 해석을 사용해 저장소를 워크스페이스로 구성할 수 있게 해 줘요. 큰 저장소에서 워크스페이스를 쓰면 분석에 필요한 메모리 양이 줄어들어 성능이 좋아져요.

참고: 모든 패키지에 대해 하나의 공유 의존성 해석을 사용하면 의존성 충돌 위험이 커져요. Dart는 같은 패키지의 여러 버전을 허용하지 않기 때문이에요. 패키지들이 함께 쓰이게 될 것이라면(흔한 경우죠) 이 위험은 오히려 유용한 기능이 돼요. 패키지를 쓰기 시작할 때가 아니라, 패키지 사이의 비호환성이 생길 때 해결하도록 강제해 주거든요.

워크스페이스 만들기

  1. 저장소 루트 디렉토리에 workspace 항목이 있는 pubspec.yaml을 추가해요. workspace에는 저장소의 패키지(워크스페이스 패키지) 경로를 열거해요.
name: _
publish_to: none
environment:
  sdk: ^3.6.0
workspace:
  - packages/shared
  - packages/client_package
  - packages/server_package
  1. 기존 pubspec.yaml 각각에 대해 SDK 제약이 최소 ^3.6.0인지 확인하고 resolution 항목을 추가해요.
environment:
  sdk: ^3.6.0
resolution: workspace
  1. 저장소 어디에서든 dart pub get을 실행해요. 그러면 다음이 일어나요.
  • 루트 pubspec.yaml 옆에, 모든 워크스페이스 패키지의 모든 dependenciesdev_dependencies의 해석이 담긴 하나의 pubspec.lock을 만들어요.
  • 패키지 이름을 파일 위치에 매핑하는 하나의 공유 .dart_tool/package_config.json을 만들어요.
  • 워크스페이스 패키지 옆에 있던 기존의 다른 pubspec.lock.dart_tool/package_config.json 파일을 삭제해요.

이제 파일 구조는 이렇게 보여요.

/
├─ packages/
│  ├─ shared/pubspec.yaml
│  ├─ client_package/pubspec.yaml
│  └─ server_package/pubspec.yaml
├─ pubspec.yaml
├─ pubspec.lock
└─ .dart_tool/
   └─ package_config.json

버전 참고: pub 워크스페이스 지원은 Dart 3.6.0에서 도입됐어요. pub 워크스페이스를 쓰려면 모든 워크스페이스 패키지(의존성은 아니에요)가 ^3.6.0 이상의 SDK 버전 제약을 가져야 해요.

Glob 패턴 지원

버전 참고: Glob 패턴 지원은 Dart 3.11 이상이 필요해요. 이전 버전을 쓰면 명시적 경로를 대신 사용하세요.

workspace 항목은 glob 패턴을 지원해서 패키지를 자동으로 포함시킬 수 있어요.

workspace:
  - packages/*

이렇게 하면 packages/ 안에서 pubspec.yaml 파일을 가진 모든 하위 디렉토리가 포함돼요. 패키지를 하나하나 나열할 필요가 없어지죠. 크거나 계속 커지는 모노레포에서 특히 유용해요. 다음과 같이 쓰는 대신:

workspace:
  - packages/shared
  - packages/client_package
  - packages/server_package

새 패키지를 자동으로 집어 올리는 glob 패턴을 쓰면 돼요.

중첩 워크스페이스(Nested workspaces)

큰 저장소에서 패키지를 계층적으로 정리하려면 워크스페이스를 중첩할 수 있어요. 워크스페이스 멤버는 루트 pubspec.yaml처럼 자기 자신의 workspace 필드를 선언할 수 있어요.

예를 들어 server 패키지가 구현을 authapi 하위 패키지로 나눈다면, server 패키지의 workspace 필드에 그 둘을 나열해요.

# packages/server/pubspec.yaml
name: server

resolution: workspace
environment:
  sdk: ^3.6.0

workspace:
  - auth
  - api

그런 다음 각 하위 패키지를 워크스페이스 해석을 사용하도록 표시해요.

# packages/server/auth/pubspec.yaml
name: auth

resolution: workspace
environment:
  sdk: ^3.6.0

루트 pubspec.yamlpackages/server만 나열하면 돼요. pub가 server 패키지의 workspace 항목을 통해 authapi를 발견해서 단일 공유 의존성 해석에 포함시켜 줘요.

잔여 파일(Stray files)

기존 모노레포를 pub 워크스페이스로 마이그레이션하면 각 pubspec 옆에 기존의 "잔여(stray)" pubspec.lock.dart_tool/package_config.json 파일이 있을 거예요. 이 파일들은 루트 옆에 놓인 pubspec.lock.dart_tool/package_config.json을 가리게 돼요.

따라서 pub get은 루트와 (포함해서) 워크스페이스 패키지 사이의 디렉토리들에 있는 pubspec.lock.dart_tool/package_config.json은 삭제해요.

/
└─ packages/
   └─ foo/
      ├─ pubspec.yaml          # 워크스페이스 멤버
      ├─ pubspec.lock          # pub get이 삭제
      └─ .dart_tool/package_config.json  # pub get이 삭제
   ├─ pubspec.lock             # pub get이 삭제
   └─ .dart_tool/package_config.json     # pub get이 삭제
pubspec.yaml                   # 루트

그런데 워크스페이스 루트와 워크스페이스 패키지 사이의 어떤 디렉토리가 워크스페이스 멤버가 아닌 "잔여" pubspec.yaml을 갖고 있다면, pub get이 오류를 보고하고 해석에 실패해요. 그런 pubspec.yaml을 해석하면 루트의 것을 가리는 .dart_tool/package_config.json 파일이 생기기 때문이에요. 예를 들면:

/
└─ packages/
   └─ foo/
      ├─ pubspec.yaml          # 워크스페이스 멤버
      └─ pubspec.yaml          # 워크스페이스 멤버 아님 => 오류
pubspec.yaml                   # 루트 `workspace: ['packages/foo']`

워크스페이스 패키지 사이의 상호 의존성

워크스페이스 패키지 중 어떤 것이 서로 의존한다면, 소스에 관계없이 자동으로 워크스페이스 안의 것으로 해석돼요. 예를 들어 packages/client_package/pubspec.yamlshared에 의존한다고 해 볼게요.

dependencies:
  shared: ^2.3.0

워크스페이스 안에서 해석되면 로컬 버전의 shared가 사용돼요. 물론 로컬 버전의 shared도 제약(^2.3.0)과 일치해야 해요. 하지만 그 패키지가 워크스페이스의 일부가 아닌 의존성으로 소비되면 원래 소스(여기서는 암시적으로 hosted)가 사용돼요. 그래서 client_package가 pub.dev에 게시되고 누군가 그 패키지에 의존하면, 그들은 transitive 의존성으로 shared의 hosted 버전을 받게 돼요.

워크스페이스에서의 의존성 오버라이드

워크스페이스 패키지에 있는 모든 dependency_overrides 섹션이 존중돼요. 또한 워크스페이스 pubspec.yaml 아무 곳의 옆에 pubspec_overrides.yaml 파일을 둘 수 있어요.

워크스페이스에서 패키지는 한 번만 오버라이드할 수 있어요. 오버라이드를 정리된 상태로 유지하려면 dependency_overrides를 루트 pubspec.yaml에 두는 게 좋아요.

특정 워크스페이스 패키지에서 명령 실행하기

dart pub add, dart pub publish 같은 일부 pub 명령은 "현재" 패키지에 대해 동작해요. 디렉토리를 바꾸거나 -C를 사용해 pub가 가리킬 디렉토리를 지정할 수 있어요.

$ dart pub -C packages/client_package publish
# 아래와 같음
$ cd packages/client_package ; dart pub publish ; cd -

워크스페이스 밖에서 패키지 임시 해석하기

가끔은 예를 들어 의존성 제약을 검증하려고 워크스페이스 패키지를 단독으로 해석하고 싶을 수 있어요. 이를 위한 한 가지 방법은 resolution 설정을 초기화하는 pubspec_overrides.yaml 파일을 만드는 거예요.

# packages/client_package/pubspec_overrides.yaml
resolution:

이제 packages/client_package 안에서 dart pub get을 실행하면 독립적인 해석이 만들어져요.

모든 워크스페이스 패키지 나열하기

dart pub workspace list를 실행하면 워크스페이스의 패키지들을 나열할 수 있어요.

$ dart pub workspace list
Package        Path
_              ./
client_package packages/client_package/
server_package packages/server_package/
shared         packages/shared/

더 알아보기