패키지 의존성

패키지 의존성 (Package dependencies)

의존성(dependency)은 pub 패키지 매니저의 핵심 개념 중 하나예요. 의존성이란 패키지가 동작하기 위해 필요한 다른 패키지를 말해요. 의존성은 pubspec에 지정해요. 직접 의존성(direct dependency, 패키지가 직접 사용하는 소프트웨어)만 나열하면 pub이 전이적 의존성(Transitive dependency — 어떤 패키지가 다른 의존성을 필요로 해서 간접적으로 사용하게 되는 의존성)을 대신 처리해줘요.

이 페이지에서는 의존성을 지정하는 방법에 대해 자세히 다룰게요. 마지막에는 패키지 의존성에 대한 모범 사례 목록이 있어요.

출처: Package dependencies

본문

개요

각 의존성에 대해 의존할 패키지의 이름과 그 패키지의 허용 가능한 버전 범위를 지정해요. 또한 의존성 소스(Dependency source — pub이 패키지를 가져올 수 있는 종류의 장소)를 지정할 수도 있어요. 소스는 pub이 패키지를 어떻게 찾을지 알려줘요.

예를 들어 의존성은 이런 형식으로 지정해요.

dependencies:
  transmogrify: ^1.0.0

이 YAML 코드는 기본 패키지 저장소(pub.dev)를 사용해 transmogrify 패키지에 대한 의존성을 만들고, 1.0.0부터 2.0.0(단, 2.0.0은 포함하지 않음)까지 모든 버전을 허용해요. 이 문법을 알아보려면 version constraints를 확인하세요.

pub.dev가 아닌 다른 소스를 지정하려면 sdk, hosted, git, path를 사용해요. 예를 들어 다음 YAML 코드는 path를 사용해 transmogrify를 로컬 디렉터리에서 가져오도록 pub에 알려줘요.

dependencies:
  transmogrify:
    path: /Users/me/transmogrify

다음 절에서는 각 의존성 소스의 형식을 설명할게요.

의존성 소스

pub은 패키지를 찾기 위해 다음과 같은 소스를 사용할 수 있어요.

  • Hosted packages (호스팅 패키지)
  • Git packages (Git 패키지)
  • Path packages (경로 패키지)
  • SDK

또한 몇몇 dart 도구 명령어는 package descriptor를 사용해 명령줄에서 직접 의존성 소스를 지정하는 것도 지원해요.

Hosted packages

호스팅 패키지란 pub.dev 사이트(또는 같은 API를 말하는 다른 HTTP 서버)에서 다운로드할 수 있는 패키지예요. 호스팅 패키지에 대한 의존성을 선언하는 예시는 이래요.

dependencies:
  transmogrify: ^1.4.0

이 예시는 transmogrify라는 호스팅 패키지에 의존하며 1.4.0부터 2.0.0(단 2.0.0 자체 제외)까지 모든 버전과 동작한다는 뜻이에요.

자체 패키지 저장소를 사용하고 싶다면 hosted로 그 URL을 지정할 수 있어요. 다음 YAML 코드는 hosted 소스를 사용해 transmogrify 패키지에 대한 의존성을 만들어요.

environment:
  sdk: '^2.19.0'

dependencies:
  transmogrify:
    hosted: https://some-package-server.com
    version: ^1.4.0

버전 제약은 선택 사항이지만 권장돼요. 버전 제약을 주지 않으면 any로 간주돼요.

Git packages

때로는 아직 공식 릴리즈되지 않은 패키지를 써야 하는 '블리딩 에지' 상황에 있기도 해요. 패키지 자체가 아직 개발 중이고, 동시에 개발 중인 다른 패키지들을 사용하고 있을 수도 있죠. 그럴 땐 Git 저장소에 저장된 패키지에 직접 의존할 수 있어요.

dependencies:
  kittens:
    git: https://github.com/munificent/kittens.git

여기서 git은 이 패키지가 Git을 통해 찾아진다는 뜻이고, 그 뒤의 URL은 패키지를 clone하는 데 사용할 수 있는 Git URL이에요.

패키지 저장소가 비공개여도 HTTPS 액세스 키나 SSH 키 페어를 사용해 저장소에 접근하도록 git 설정을 구성할 수 있어요. 그러면 저장소의 해당 URL을 사용해 패키지에 의존할 수 있어요.

dependencies:
  kittens:
    # SSH URL:
    git: [email protected]:munificent/kittens.git

dart pub 명령어는 git clone을 하위 프로세스로 호출해요. 그래서 git clone <url>을 실행했을 때 동작하는 <url>을 제공하기만 하면 돼요.

특정 커밋, 브랜치, 또는 태그에 의존하려면 설명에 ref 키를 추가해요.

dependencies:
  kittens:
    git:
      url: [email protected]:munificent/kittens.git
      ref: some-branch

ref는 Git이 커밋을 식별할 수 있게 허용하는 어떤 값이든 될 수 있어요.

의존하는 패키지가 각 버전의 리비전에 태그를 붙였다면, ref 대신 tag_pattern을 버전 제약과 함께 사용할 수 있어요.

그러면 pub이 Git에 일치하는 모든 태그를 질의하고, 그 버전들을 버전 해석기(version solver)에 전달해요.

dependencies:
  kittens:
    git:
      url: [email protected]:munificent/kittens.git
      tag_pattern: v{{version}} # 'v'로 접두사된 버전 태그를 찾습니다
    version: ^2.0.1

pub은 패키지가 Git 저장소의 루트(root)에 있다고 가정해요. 저장소의 다른 위치를 지정하려면 저장소 루트 기준의 path를 지정해요.

dependencies:
  kittens:
    git:
      url: [email protected]:munificent/cats.git
      path: path/to/kittens

이 path는 Git 저장소의 루트 기준 상대 경로예요.

Git 의존성은 pub.dev에 업로드되는 패키지의 의존성으로 허용되지 않아요.

Path packages

여러 관련 패키지를 동시에 작업하는 상황에 있기도 해요. 프레임워크를 만들면서 그것을 사용하는 앱을 만드는 경우가 그 예시죠. 그런 경우 개발 중에는 로컬 파일 시스템에 있는 그 패키지의 라이브(가장 최신) 버전에 의존하고 싶어져요. 그래야 한 패키지의 변경이 그에 의존하는 패키지에 즉시 반영되니까요.

이를 위해 pub은 path 의존성을 지원해요.

dependencies:
  transmogrify:
    path: /Users/me/transmogrify

이것은 transmogrify의 루트 디렉터리가 /Users/me/transmogrify라는 뜻이에요. 이 의존성에 대해 pub은 참조된 패키지 디렉터리의 lib 디렉터리로 가는 심볼릭 링크(symlink)를 직접 생성해요. 의존하는 패키지에 가한 어떤 변경도 즉시 반영돼요. 의존하는 패키지를 바꿀 때마다 pub을 실행할 필요가 없어요.

상대 경로도 허용되며, pubspec이 있는 디렉터리를 기준으로 간주돼요.

Path 의존성은 로컬 개발에 유용하지만, 외부 세계와 코드를 공유할 때는 동작하지 않아요 — 모든 사람이 내 파일 시스템에 접근할 수는 없으니까요. 이런 이유로 pubspec에 path 의존성이 있는 패키지는 pub.dev 사이트에 업로드할 수 없어요.

대신 일반적인 워크플로우는 이래요.

  • 로컬에서 pubspec을 편집해 path 의존성을 사용해요.
  • 메인 패키지와 그 패키지가 의존하는 패키지를 함께 작업해요.
  • 둘 다 동작하게 되면, 의존하는 패키지를 게시해요.
  • pubspec을 바꿔서 이제 호스팅된 버전의 의존 대상을 가리키게 해요.
  • 원하면 메인 패키지도 게시해요.

SDK packages

SDK 소스는 패키지와 함께 배포되며 그 자체가 의존성이 될 수 있는 SDK에 사용돼요. 현재 지원되는 SDK는 Flutter가 유일해요.

문법은 이렇게 생겼어요.

dependencies:
  flutter_driver:
    sdk: flutter

sdk: 뒤의 식별자는 패키지가 어느 SDK에서 오는지 나타내요. flutter라면 다음 조건이 유지되는 한 의존성이 충족돼요.

  • pub이 flutter 실행 파일의 컨텍스트에서 실행 중이고
  • Flutter SDK에 해당 이름의 패키지가 포함되어 있을 때

식별자가 알 수 없는 값이라면 의존성은 항상 충족되지 않은 것으로 간주돼요.

Package descriptors

dart pub adddart pub unpack 같은 여러 dart 도구 명령어는 패키지 이름 뒤에 package descriptor를 받아 패키지의 버전이나 의존성 소스를 지정해요.

package descriptor의 가장 단순한 형태는 pub.dev 사이트의 기본 소스를 가정한 버전 제약이에요. 예를 들어 다음 명령어는 ^1.2.3 descriptor를 사용해 package:foo^1.2.3 제약으로 의존성을 추가해요.

dart pub add foo:^1.2.3

사용자 정의 의존성 소스를 지정하려면 pubspec.yaml과 같은 구조를 쓰되 flow-style YAML로 작성해요.

{<source>: <descriptor>, [version: <constraint>]}

다음 절에서는 각 pub 의존성 소스 유형에 대한 package descriptor 문법을 보여줄게요.

Hosted 의존성 descriptor

호스팅 패키지를 지정하려면:

{hosted: my-pub.dev}

버전 제약도 지정할 수 있어요:

{hosted: my-pub.dev, version: ^1.2.3}

Git 의존성 descriptor

Git 패키지를 지정하려면:

{git: https://github.com/foo/foo}

저장소 URL, 브랜치 또는 커밋 참조, 저장소 내 패키지까지의 경로를 지정할 수 있어요:

{git: {url: ../foo.git, ref: branch, path: subdir}}

Path 패키지 descriptor

Path 패키지를 지정하려면:

{path: ../foo}

SDK 패키지 descriptor

SDK 패키지를 지정하려면:

{sdk: flutter}

Version constraints (버전 제약)

내 패키지 A가 패키지 B에 의존한다고 해 봐요. 다른 개발자에게 Package B의 어떤 버전이 Package A 특정 버전과 호환되는지 어떻게 알려줄 수 있을까요?

개발자에게 버전 호환성을 알려주려면 버전 제약(version constraint)을 지정해요. 패키지 사용자에게 유연성을 주기 위해 가능한 한 넓은 버전 범위를 허용하고 싶을 거예요. 그 범위는 동작하지 않거나 테스트되지 않은 버전은 제외해야 해요.

Dart 커뮤니티는 시맨틱 버저닝(semantic versioning)을 사용해요.

버전 제약은 traditional 문법이나 (Dart 2.19부터의) 캐럿(caret) 문법으로 표현할 수 있어요. 두 문법 모두 호환되는 버전의 범위를 지정해요.

traditional 문법은 '>=1.2.3 <2.0.0' 같은 명시적인 범위를 제공해요. 캐럿 문법은 명시적인 시작 버전 ^1.2.3을 제공해요.

environment:
  # 이 패키지는 3.2부터 시작하는 Dart SDK의 3.x 버전을 사용해야 합니다
  sdk: ^3.2.0

dependencies:
  transmogrify:
    hosted:
      name: transmogrify
      url: https://some-package-server.com
    # 이 패키지는 1.4부터 시작하는 transmogrify의 1.x 버전을 사용해야 합니다
    version: ^1.4.0

pub의 버전 시스템에 대해 더 알아보려면 package versioning 페이지를 참고하세요.

Traditional 문법

traditional 문법을 사용하는 버전 제약은 다음 값들 중 어느 것이든 사용할 수 있어요.

허용 범위 사용? 비고
any 모든 버전 아니요 빈 버전 제약의 명시적 선언 역할을 합니다
1.2.3 주어진 버전만 아니요 패키지를 사용하는 앱에 추가 제한을 둬서 패키지의 채택을 제한합니다
>=1.2.3 주어진 버전 이상
>1.2.3 주어진 버전보다 최신 아니요
<=1.2.3 주어진 버전 이하 아니요
<1.2.3 주어진 버전보다 이전 아니요 패키지와 동작하지 않는 상한 버전을 알 때 사용해요. 이 버전이 어떤 호환성 파괴(breaking change)를 처음 도입한 버전일 수 있어요

버전 값들의 범위가 교차하므로 어떤 조합이든 지정할 수 있어요. 예를 들어 버전 값을 '>=1.2.3 <2.0.0'으로 설정하면 두 제한이 결합되어 의존성이 1.2.3부터 2.0.0(단 2.0.0 자체는 제외)까지 모든 버전이 될 수 있어요.

Caret 문법

캐럿 문법은 버전 제약을 간결하게 표현해요. ^version은 주어진 버전과 역호환(backwards compatible)이 보장되는 모든 버전의 범위를 의미해요. 이 범위는 다음으로 호환성 파괴를 도입하는 버전까지의 모든 버전을 포함해요. Dart가 시맨틱 버저닝을 사용하므로, 1.0 이상의 패키지 버전이라면 다음 메이저 버전이 되고, 1.0 이전의 버전이라면 다음 마이너 버전이 돼요.

버전 값 범위가 덮는 곳 Caret 문법 Traditional 문법
>=1.0 다음 메이저 ^1.3.0 '>=1.3.0 <2.0.0'
<1.0 다음 마이너 ^0.1.2 '>=0.1.2 <0.2.0'

캐럿 문법의 예시는 이래요.

dependencies:
  # 1.3.0부터 1.y.z까지 모든 버전을 포함, 2.0.0은 미포함
  path: ^1.3.0
  # 1.1.0부터 1.y.z까지 모든 버전을 포함, 2.0.0은 미포함
  collection: ^1.1.0
  # 0.1.2부터 0.1.z까지 모든 버전을 포함, 0.2.0은 미포함
  string_scanner: ^0.1.2

Dev dependencies

pub은 일반 의존성과 dev 의존성 두 종류의 의존성을 지원해요. dev 의존성은 내가 의존하는 패키지들의 dev 의존성이 무시된다는 점에서 일반 의존성과 달라요. 예를 들어 볼게요.

transmogrify 패키지가 테스트에서만 test 패키지를 사용한다고 해 봐요. 누군가 transmogrify를 — 그 라이브러리를 import해서 — 사용하고 싶다면 실제로 test는 필요 없어요. 이 경우 test를 dev 의존성으로 지정해요. 그 pubspec은 대략 이렇게 생겼어요.

dev_dependencies:
  test: ^1.25.0

pub은 내 패키지가 의존하는 모든 패키지와, 그 패키지들이 의존하는 모든 것을 전이적으로 가져와요. 또한 내 패키지의 dev 의존성도 가져오지만, 의존하고 있는 패키지들의 dev 의존성은 무시해요. pub은 내 패키지의 dev 의존성만 가져와요. 그래서 내 패키지가 transmogrify에 의존할 때 transmogrify는 가져오지만 test는 가져오지 않아요.

일반 의존성과 dev 의존성을 구분하는 규칙은 간단해요. lib이나 bin 디렉터리의 무언가에서 import된다면 일반 의존성이어야 해요. test, example 등에서만 import된다면 dev 의존성이 될 수 있고 되어야 해요.

dev 의존성을 사용하면 의존성 그래프가 더 작아져서 pub 실행이 빨라지고, 모든 제약을 만족하는 패키지 버전 조합을 찾기도 더 쉬워져요.

Dependency overrides (의존성 override)

dependency_overrides를 사용해 모든 의존성 참조를 일시적으로 override할 수 있어요.

예를 들어 게시된 패키지인 transmogrify의 로컬 복사본을 업데이트하는 상황을 생각해 봐요. 내 의존성 그래프의 다른 패키지들이 transmogrify를 사용하지만, 내 로컬 transmogrify 복사본을 테스트하기 위해 각 패키지를 로컬로 clone하고 각 pubspec을 바꾸고 싶지는 않을 거예요.

이런 상황에서 dependency_overrides를 사용해 그 패키지의 로컬 복사본이 있는 디렉터리를 지정해서 의존성을 override할 수 있어요.

pubspec은 대략 이렇게 생겼어요.

name: my_app
dependencies:
  transmogrify: ^1.2.0
dependency_overrides:
  transmogrify:
    path: ../transmogrify_patch/

dart pub get이나 dart pub upgrade를 실행하면 pubspec의 lockfile이 의존성의 새 경로를 반영하도록 업데이트되고, transmogrify가 사용되는 곳마다 pub이 로컬 버전을 대신 사용해요.

dependency_overrides로 패키지의 특정 버전을 지정할 수도 있어요.

name: my_app
dependencies:
  transmogrify: ^1.2.0
dependency_overrides:
  transmogrify: '3.2.1'

패키지 해석 중에는 그 패키지 자신의 pubspec에 있는 dependency overrides만 고려돼요. 의존되는 패키지 안의 dependency overrides는 무시돼요.

결과적으로 패키지를 pub.dev에 게시한다면, 내 패키지의 dependency overrides는 패키지의 모든 사용자에게 무시된다는 점을 명심하세요.

pub 워크스페이스를 사용한다면 각 워크스페이스 패키지에 dependency_overrides를 둘 수 있지만, 워크스페이스에서 단일 패키지는 한 번만 override할 수 있어요.

pubspec_overrides.yaml

pubspec.yaml 파일의 해석(resolution) 중 특정 부분을 바꾸고 싶은데 실제 파일은 바꾸고 싶지 않다면, pubspec.yaml 옆에 pubspec_overrides.yaml이라는 파일을 둘 수 있어요.

그 파일의 속성들이 pubspec.yaml의 속성들을 override해요.

override할 수 있는 속성은 이래요.

  • dependency_overrides
  • workspace
  • resolution

이것은 임시 override를 실수로 버전 컨트롤에 체크인하는 것을 피하는 데 유용해요. 또한 스크립트에서 override를 생성하기도 더 쉬워져요.

pub 워크스페이스에서는 각 워크스페이스 패키지가 pubspec_overrides.yaml 파일을 가질 수 있어요.

모범 사례

의존성을 관리하는 데 적극적으로 임하세요. 가능하면 패키지가 가장 최신 버전의 패키지들에 의존하도록 하세요. 패키지가 오래된(stale) 패키지에 의존한다면, 그 오래된 패키지도 의존성 트리 안의 다른 오래된 패키지들에 의존할 수 있어요. 오래된 버전의 패키지들은 앱의 안정성, 성능, 품질에 부정적인 영향을 줄 수 있어요.

패키지 의존성에 대해 권장하는 모범 사례는 이래요.

캐럿 문법 사용

의존성을 캐럿 문법으로 지정해요. 이렇게 하면 pub 도구가 패키지의 새 버전이 사용 가능해질 때 그것을 선택할 수 있어요. 또한 허용 버전에 상한을 두는 효과도 있어요.

최신 안정 패키지 버전에 의존

dart pub upgrade를 사용해 pubspec이 허용하는 최신 패키지 버전으로 업데이트해요. 앱이나 패키지에서 최신 안정 버전이 아닌 의존성을 찾으려면 dart pub outdated를 사용해요.

dev 의존성 버전 제약 조이기

dev 의존성은 개발할 때만 필요한 패키지를 정의해요. 완성된 앱에는 이런 패키지가 필요하지 않아요. 이런 패키지의 예로는 테스트나 코드 생성 도구가 있어요. dev_dependencies 안의 패키지 버전 제약을 내 패키지가 의존하는 최신 버전을 하한으로 설정해요.

dev 의존성의 버전 제약을 조이면 대략 이렇게 생겨요.

dev_dependencies:
  build_runner: ^2.15.1
  lints: ^6.1.0
  test: ^1.31.1

이 YAML은 dev_dependencies를 최신 패치 버전으로 설정해요.

패키지 의존성을 업데이트할 때마다 테스트

pubspec을 수정하지 않고 dart pub upgrade를 실행한다면 API는 그대로 유지되고 코드는 예전처럼 돌아가야 해요 — 하지만 확실히 하려면 테스트해 보세요. pubspec을 수정하고 새 메이저 버전으로 업데이트한다면 호환성 파괴(breaking change)를 만날 수도 있으니, 더 철저하게 테스트해야 해요.

낮춘(downgraded) 의존성으로 테스트

게시용 패키지를 개발할 때는 보통 가능한 한 넓은 의존성 제약을 허용하는 편이 좋아요. 넓은 의존성 제약은 패키지 소비자가 버전 해석 충돌을 마주할 가능성을 줄여줘요.

예를 들어 foo: ^1.2.3에 의존하고 foo의 1.3.0 버전이 릴리즈됐다면 기존 의존성 제약(^1.2.3)을 유지하는 게 타당할 수 있어요. 하지만 패키지가 1.3.0에 추가된 기능을 사용하기 시작하면 제약을 ^1.3.0으로 올려야 해요.

그런데 필요해졌을 때 의존성 제약을 올리는 것을 잊기 쉽죠. 그래서 게시하기 전에 낮춘 의존성으로 패키지를 테스트하는 것이 모범 사례예요.

낮춘 의존성으로 테스트하려면 dart pub downgrade를 실행하고 패키지가 오류 없이 분석되고 모든 테스트를 통과하는지 확인해요.

dart pub downgrade
dart analyze
dart test

낮춘 의존성으로 테스트하는 것은 최신 의존성으로 하는 일반 테스트와 함께 이루어져야 해요. 의존성 제약을 올려야 한다면 직접 바꾸거나, dart pub upgrade --tighten으로 의존성을 최신 버전으로 업데이트하세요.

다운로드한 패키지 무결성 확인

새 의존성을 가져올 때 --enforce-lockfile 옵션을 사용해 압축을 푼 패키지 내용물이 원본 아카이브의 내용물과 일치하는지 확인해요. lockfile(Lockfile — 각 의존성의 버전을 지정하는 pubspec.lock 파일)을 수정하지 않고 이 플래그는 다음 경우에만 새 의존성을 해석해요.

  • pubspec.yaml이 만족되고
  • pubspec.lock이 빠지지 않았으며
  • 패키지의 콘텐츠 해시(Pub content hash — 패키지 무결성을 검증하기 위해 pub.dev가 유지하는 SHA256 해시)가 일치할 때

더 알아보기