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는 같은 패키지의 여러 버전을 허용하지 않기 때문이에요. 패키지들이 함께 쓰이게 될 것이라면(흔한 경우죠) 이 위험은 오히려 유용한 기능이 돼요. 패키지를 쓰기 시작할 때가 아니라, 패키지 사이의 비호환성이 생길 때 해결하도록 강제해 주거든요.
워크스페이스 만들기
- 저장소 루트 디렉토리에
workspace항목이 있는pubspec.yaml을 추가해요.workspace에는 저장소의 패키지(워크스페이스 패키지) 경로를 열거해요.
name: _
publish_to: none
environment:
sdk: ^3.6.0
workspace:
- packages/shared
- packages/client_package
- packages/server_package
- 기존
pubspec.yaml각각에 대해 SDK 제약이 최소^3.6.0인지 확인하고resolution항목을 추가해요.
environment:
sdk: ^3.6.0
resolution: workspace
- 저장소 어디에서든
dart pub get을 실행해요. 그러면 다음이 일어나요.
- 루트
pubspec.yaml옆에, 모든 워크스페이스 패키지의 모든dependencies와dev_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 패키지가 구현을 auth와 api 하위 패키지로 나눈다면, 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.yaml은 packages/server만 나열하면 돼요. pub가 server 패키지의 workspace 항목을 통해 auth와 api를 발견해서 단일 공유 의존성 해석에 포함시켜 줘요.
잔여 파일(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.yaml이 shared에 의존한다고 해 볼게요.
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/