본문으로 건너뛰기

오픈소스 패키지 배포 시 고려해야할 것들

소개

Flutter 환경에서 커스터마이징 가능한 움직이는 아이콘 개발과 배포를 진행하면서 찾아본 내용들을 정리하였다. 어떻게 하면 좀 더 많은 사람들이 편하게 활용할 수 있도록 작성할 수 있을 지, 어떻게 코드를 작성해야 다른 사람들이 코드를 확인하고 수정하기 더 편할지를 생각하며 성공한 여러 오픈소스 패키지들을 참고하고 관련 글을 찾아본 내용들을 정리하였다.

Flutter의 패키지 배포 환경

Flutter는 다른 개발자가 Flutter 및 Dart 생태계에 기여한 공유 패키지를 사용하도록 지원한다. 이를 통해 모든 것을 처음부터 개발하지 않고도 앱을 빠르게 빌드할 수 있다.

패키지와 플러그인의 차이점

플러그인은 패키지의 한 유형으로 전체 명칭은 플러그인 패키지이며 일반적으로 플러그인이라고 줄여 부른다.

패키지

Dart 패키지는 최소한 pubspec.yaml 파일을 포함하는 디렉토리이다. 또한 패키지에는 종속성(pubspec에 나열됨), Dart 라이브러리, 앱, 리소스, 테스트, 이미지, 글꼴 및 예시가 포함될 수 있다. pub.dev 사이트에는 Google 엔지니어와 Flutter 및 Dart 커뮤니티의 구성원들이 개발한 패키지들이 소개되어 있으며, 앱에서 활용할 수 있다.

플러그인

플러그인 패키지는 플랫폼 기능을 앱에서 사용할 수 있게 해주는 특별한 종류의 패키지이다. 플러그인 패키지는 Android(Kotlin 또는 Java 사용), iOS(Swift 또는 Objective-C 사용), 웹, macOS, Windows, Linux 또는 이들의 조합으로 작성될 수 있다. 예를 들어, 플러그인은 Flutter 앱에 기기의 카메라를 사용할 수 있는 기능을 제공할 수 있다.

Packages versus plugins | Decoding Flutter

패키지 사용

패키지는 pub.dev에 게시된다. pub.devFlutter 랜딩 페이지는 Flutter와 호환되는 상위 패키지(일반적으로 Flutter와 호환되는 종속성을 선언한 패키지)를 표시하고 게시된 모든 패키지를 검색하는 기능을 지원한다.

pub.dev의 Flutter Favorites 페이지에는 앱을 작성할 때 크게 도움이 되는 패키지들이 나열되어있어 이를 활용하면 보다 빠르게 애플리케이션을 개발할 수 있다.

또한 Android, iOS, web, Linux, Windows, macOS를 필터링하여 현재 환경에서 활용할 수 있는 패키지 목록도 찾아볼 수 있다.

앱에 패키지 종속성 추가하기

예시로 css_colors를 추가해보자

  1. dependencies 추가

    • 프로젝트 폴더에 있는 pubspec.yaml 파일을 열고 dependencies 항목에 css_colors를 추가한다.
  2. Install

    • 터미널: flutter pub get을 실행한다
    • VS Code: 다운로드 아이콘으로 표시된 상단 작업 목록의 패키지 가져오기를 클릭한다.
    • Android Studio/IntelliJ: pubspec.yaml 상단의 작업 목록에서 Pub get을 클릭한다.
  3. Import

    • Dart code에 import를 사용하여 패키지를 추가한다.
  4. 앱 재실행하기

    • hot reload/restart의 경우 Dart 코드만 업데이트 하므로 패키지가 플랫폼별 코드 (Android의 경우 Kotlin/Java, iOS의 경우 Swift/Objective-C)를 가져오는 경우 앱을 재실행하여 빌드해야한다.

터미널 명령어를 통한 추가

flutter pub add "some-package"를 통해서도 패키지 종속성을 추가할 수 있다. 마찬가지로 css_colors를 예시로 확인해보자.

  1. 프로젝트 디렉토리 내부 터미널에서 명령어 실행

    • flutter pub add css_colors
  2. Import

    • Dart code에 import를 사용하여 패키지를 추가한다.
  3. 앱 재실행하기

    • hot reload/restart의 경우 Dart 코드만 업데이트 하므로 패키지가 플랫폼별 코드 (Android의 경우 Kotlin/Java, iOS의 경우 Swift/Objective-C)를 가져오는 경우 앱을 재실행하여 빌드해야한다.

패키지 종속성 제거

flutter pub remove를 통해 패키지를 제거할 수 있다.

  1. 프로젝트 디렉토리 내부 터미널에서 명령어 실행
    • flutter pub add css_colors

문제 해결

앱에서 some_packageanother_package를 사용하려 한다. 두 패키지는 모두 url_launcher를 종속성으로 가지고 있는데 url_launcher의 버전이 다르다고 가정해보자. 그러면 두 패키지를 가져올 때 충돌이 발생하게 될 것이다.

이를 피하는 가장 좋은 방법은 패키지 작성자가 종속성을 지정할 때 특정 버전 대신 범위 버전을 설정하는 것 이다.

Caret syntax & Traditional syntax

버전 범위를 설정해주는 방법은 caret syntaxtraditional syntax 방식이 있다.

caret syntax

caret syntax의 경우 다음과 같이 사용한다. ex. ^1.2.3

이는 >=1.2.3 <2.0.0의 의미를 가지며 메이저 버전이 바뀌지 않는 모든 버전을 포함하는 의미를 가진다.

traditional syntax

traditional syntax의 경우 단어 그대로 이전부터 사용되던 범위 설정 방식이며 아래와 같이 설정한다.

Value허용 범위사용가능 여부메모
any모든 버전No비어있는 버전 제약 조건을 명시적으로 보여주는 역할이다.
1.2.3해당 버전만No패키지를 사용하는 앱에 제약이 걸리기 때문에 패키지가 채택되지 않는다.
>=1.2.3주어진 버전 이상Yes
>1.2.3주어진 버전 이후No
<=1.2.3주어진 버전 이하No
<1.2.3주어진 버전 이전No

패키지의 경우 여러 사용자들이 제약없이 활용할 수 있어야하므로 >= 만 허용되는 것에 유의하자.

종속성을 범위 버전으로 설정해주면 pub은 자동으로 문제를 해결해준다.

만약 해당 패키지를 직접 수정할 수 없는 경우에는 어떻게 할까? pubspec.yaml파일에서 dependency_overrides를 설정하여 종속성을 재정의 할 수 있다.

pubspec.yaml
dependencies:
some_package:
another_package:
dependency_overrides:
url_launcher: "5.4.0"

충돌하는 종속성이 패키지 자체가 아니라 Android나 iOS 전용 라이브러리일 경우에는 어떻게 할까? Android의 경우 종속성 재정의 선언을 Gradle 빌드 로직에 추가하여 강제할 수 있다. guava 패키지를 예시로 보자.

configuration.all {
resolutionStrategy {
force 'com.google.guava:guava:28.0-android'
}
}

CocoaPods의 경우 현재(2024-11-19) 종속성 재정의 기능을 제공하지 않고 있다.

Flutter 패키지 개발하기

원하는 패키지가 없는 경우 새로운 패키지를 작성하는 방법은 어떻게 될까?

패키지 종속성 및 버전 관리

우선 버전 충돌 위험을 최소화하기 위해 pubspec.yaml 파일에서 버전 범위를 설정해줘야한다.

패키지 버전

모든 패키지에는 pubspec.yaml 파일에 지정된 패키지 버전 번호가 있다. 패키지의 현재 버전은 이름 옆에 표시된다.(예시 url_launcher: url_launcher 6.3.1) 또한 모든 이전 버전 목록을 확인할 수 있다.(예시 url_launcher 버전)

패키지를 업데이트할 때 마이그레이션 작업 없이 앱이 동작하도록 하기 위해 종속성들을 버전 범위로 설정해준다.

  • 범위 제약 조건: 최소 및 최대 버전 지정

    dependencies:
    url_launcher: ">=5.4.0 <6.0.0"
  • 캐럿 구문을 이용한 범위 제약: 최소 버전을 설정한다. 이는 해당 버전부터 다음 메이저 버전 이전까지의 모든 버전을 포함한다.

    dependencies:
    collection: "^5.4.0"

    이 구문은 위 범위 제약 조건 과 같은 의미를 가진다.

상세 내용은 다음에 살펴볼 패키지 버전 관리 가이드에서 확인하자.

공개되지 않은 패키지에 대한 종속성

패키지는 pub.dev에 게시되지 않은 경우에도 활용할 수 있다. 비공개 패키지 또는 로컬 패키지와 같은 게시할 준비가 되지 않은 패키지의 경우 추가 종속성 옵션을 사용할 수 있다.

경로 종속성

Flutter 앱은 파일 시스템 path: 종속성을 사용하여 패키지에 종속될 수 있다. 경로는 상대 경로 또는 절대 경로일 수 있다. 상대 경로는 pubspec.yaml을 포함하는 디렉토리를 기준으로 평가된다. 예를 들어, 옆 디렉토리에 있는 패키지에 종속되려면 다음과 같이 설정한다.

상대 경로 종속성
dependencies:
packageA:
path: ../pacakgeA/
Git 종속성

Git 저장소에 저장된 패키지에 의존할 수도 있다. 패키지가 저장소 루트에 있는 경우 다음과 같이 설정한다.

Git 종속성
dependencies:
packageA:
git:
url: https://github.com/flutter/packageA.git
SSH를 사용한 Git 종속성

저장소가 비공개이고 SSH를 사용하여 연결할 수 있는 저장소의 SSH URL을 사용하여 설정한다.

SSH를 사용한 Git 비공개 저장소
dependencies:
packageA:
git:
url: git@github.com:flutter/packageA.git
폴더 내 패키지에 대한 Git 종속성

Pub은 패키지가 Git 저장소의 루트에 있다고 가정한다. 그렇지 않은 경우 path로 위치를 지정한다.

폴더 내 패키지에 대한 Git 종속성
dependencies:
packageA:
git:
url: https://github.com/flutter/packages.git
path: packages/packageA
특정 커밋, 브랜치 고정 Git 종속성

마지막으로 ref인수를 사용하여 종속성을 특정 git 커밋, 브랜치 또는 태그에 고정한다.

dependencies:
kittens:
git:
url: git@github.com:munificent/kittens.git
ref: some-branch

추가적인 내용은 패키지 종속성을 확인하자.

패키지 버전 관리 가이드

pub 패키지 관리자는 버전 관리를 하는데 도움을 준다. 이번에는 버전 관리의 역사와 pub의 버전 관리 접근 방식에 대해 간략히 알아보자.

버전 관리의 역사

현대 소프트웨어 개발, 특히 웹 개발은 많은 기존 코드를 재사용하는 데 크게 의존한다. 여기에는 과거 작성한 코드뿐만 아니라 타사의 코드, 큰 프레임 워크에서 작은 유틸리티 라이브러리에 이르기까지 많은 것이 포함된다. 애플리케이션이 수십 개의 서로 다른 패키지와 라이브러리에 의존하는 것은 이처럼 드문 일이 아니다.

이는 생각보다도 매우 강력한 효력을 발휘한다. 몇 주 만에 수백만 명의 사용자를 확보한 사이트를 구추하는 소규모 웹 스타트업의 이야기를 볼 때, 그들이 이를 달성할 수 있는 이유는 오픈 소스 커뮤니티 덕분이라 할 수 있다.

하지만 이는 무료로 제공되지는 않는다. 코드 재사용, 특히 유지 관리하지 않는 코드 재사용에는 어려움이 있다. 앱에서 다른 사람이 개발한 코드를 사용하는 경우, 그들이 코드를 변경하면 어떻게 될까? 그들은 앱을 망가뜨리고 싶어하지 않으며, 이는 다들 마찬가지일 것이다. 따라서 이를 버전관리를 통해 해결하고자 한다.

Name과 Number

외부 코드에 의존할 때, "내 앱은 widgets를 사용합니다." 라고만 말하지는 않는다. "내 앱은 widgets 2.0.5를 사용합니다." 라고 말한다. 이름과 버전 번호의 조합은 변경 불가능한 코드 덩어리를 고유하게 식별한다. 업데이트하는 사람들은 원하는 모든 변경을 할 수 있지만 이미 릴리스된 버전은 건드리지 않겠다고 약속한다. 그들은 또 다른 widgets 3.0.0를 내놓을 수 있고 2.0.5에서는 사용하는 버전이 변경되지 않았기 때문에 전혀 영향을 미치지 않는다.

변경 사항을 적용하고 싶을 때는 항상 앱을 최신 버전으로 지정할 수 있으며 이를 위해 해당 개발자와 협력할 필요가 없다. 그러나 그렇다고 해서 문제가 완전히 해결되지는 않는다.

아래 내용에서 논의하는 버전 번호는 패키지 파일 이름에 설정된 버전 번호와 다를 수 있다. 여기에는 -0또는 -beta가 포함될 수 있다. 이러한 표기법은 종속성 해결에 영향을 미치지 않는다.

공유 종속성 해결

종속성 그래프가 실제로 종속성 트리일 때 특정 버전에 따라 잘 변경된다. 앱이 여러 패키지에 종속되고 해당 패키지가 자체 종속성을 갖는 경우와 같이 종속성이 겹치지 않는 한 모두 잘 동작한다.

다음 예시를 참고해보자

Dependencies Tree

그래서 my_appwidgetstemplates를 사용하고 이 둘은 모두 collection을 사용한다. 이 것을 공유 종속성 이라 한다. 하지만 widgets에서 collection 2.3.5 종속성을 사용하고 싶지 않고 templates에서 collection 2.3.7을 사용하고 싶다면 어떻게 할까? 버전에 동의하지 않으면 어떻게 될까?

Unshared libraries (npm 접근 방식)

한 가지 옵션은 앱이 collection의 두 버전을 모두 사용하도록 하는 것이다. widgetstemplates는 다른 버전의 복사본을 각각 따로 가지게 된다.

이것이 npmnode.js를 위해 하는 일이다. 다트에서는 어떨까? 다음 시나리오를 따라가며 보자.

  1. collection이 일부 Dictionary 클래스를 정의한다.
  2. widgetscollection 2.3.5의 복사본에서 인스턴스를 얻는다. 이 후 my_app으로 전달한다.
  3. my_apptemplatesDictionary를 넘긴다.
  4. 이 후 순차적으로 collection 2.3.7에 전달된다.
  5. 이를 사용하는 메서드에는 해당 Dictionary 형식 주석이 있다.

Dart에서는 collection 2.3.5collection 2.3.7은 완전히 관련없는 라이브러리이다. 한 쪽에서 클래스 인스턴스를 가져와 다른쪽의 Dictionary 메서드에 전달하면 두 Dictionary는 완전히 다른 유형이므로 일치하지 않는다.

이러한 이유로 Dart에서는 npm 모델이 적합하지 않다고 판단하고 다른 접근 방식을 가진다.

Version lock (사용되지 않음)

대신, 패키지에 의존할 때 앱은 해당 패키지의 단일 사본만 사용한다. 공유 종속성이 있는 경우 이에 의존하는 모든 것이 사용할 버전에 대해 동의해야 한다. 만약 그렇지 않으면 오류가 발생한다.

하지만 이러한 접근 방법으로는 문제가 해결되지 않는다. 우리는 오류가 발생하면 이를 해결할 수 있어야 한다. 이전 npm의 예에서와 같은 상황에 처했다고 가정해보자. widgetstemplates를 사용하고 싶지만 서로 다른 collection 버전을 사용하고 있다. 어떻게 해결해야 할까?

이 접근 방식에서 제시할 수 있는 답은 둘 중 하나를 업그레이드 하는 것이다. templates에 맞추기 위해 collection 2.3.7 버전이 적용된 widgets 버전을 찾을 수 있을까?

대부분의 경우 답은 "No"일 것이다. 개발하는 입장에서 생각해보자 우리는 widgets 코드에 새로운 변경사항을 적용한 새 버전을 출시하려 한다. 어떻게 하면 보다 많은 사람들이 문제 없이 업그레이드 하도록 할 수 있을까? collection의 버전을 유지한다면 모든 사람이 문제 없이 새 버전으로 업그레이드할 수 있을 것이다.

우리가 종속성을 업그레이드 한다면 widgets를 업그레이드 하는 모든 사람은 좋든 싫든 collection을 업그레이드 해야한다. 이는 때에 따라 매우 힘든일이므로 종속성 버전을 올리고자 하는 동기가 저하된다. 이를 버전 잠금 이라고 한다.

버전 제약 (Dart의 접근 방식)

버전 잠금을 해결하기 위해 패키지가 종속성에 두는 제약을 느슨하게 한다. widgetstemplates가 모두 작업하는 버전의 범위를 나타낼 수 있다면 종속성을 새 버전으로 옮길 수 있는 충분한 여유가 생긴다. 범위에 겹치는 부분이 있는 한 둘다 만족하는 버전을 찾을 수 있다.

이는 bundler가 따르는 모델이며 pub의 모델이기도 하다. pubspec.yaml에 종속성을 추가 할 때 허용할 수 있는 버전 범위를 지정할 수 있다. widgetspubspec.yaml이 다음과 같다면

dependencies:
collection: ">=2.3.5 <2.4.0"

collection 2.3.7에 대한 버전을 선택할 수 있을 것이다. 해당 버전은 widgetstemplates 모두의 제약 조건을 충족한다.

의미론적 버전

패키지에 종속성을 추가할 때 허용할 버전 범위를 지정하고 싶을 때가 있다. 어떤 범위를 선택해야 할지 어떻게 알 수 있을까? 전방 호환이 되어야 하므로 이상적으로는 범위가 아직 출시되지 않은 미래 버전을 포함한다. 하지만 아직 존재하지도 않는 새로운 버전에서 패키지가 작동할 것이라고 어떻게 확신할 수 있을까?

이를 해결하려면 버전 번호가 무엇을 의미하는지 합의해야 한다. 의존하는 패키지 개발자가 "이전 버전과 호환되지 않는 변경 사항을 만들면 주요 버전 번호를 올리겠다고 약속합니다."라고 선언했다고 가정해보자. 그들을 신뢰하고 2.3.5 패키지가 그들의 버전과 호환된다는 것을 알고 있다면 최대 3.0.0까지 작동할 것이라고 믿을 수 있다. 이를 pubspec.yaml에서 보면 다음과 같다.

dependencies:
collection: ^2.3.5
노트

이 예에서는 캐럿 구문을 사용하여 다양한 버전을 표현한다. ^2.3.5 문자열은 "3.0.0을 제외한 2.3.5에서 3.0.0까지의 모든 버전 범위"를 의미한다. 자세한 내용은 캐럿 구문을 참고하자.

이 작업을 하려면 우리는 약속들을 정리해 규칙을 만들어야 한다. 다행히도 이미 이전의 똑똑한 사람들이 모든 작업을 끝냈고, 이를 의미론적 버전 관리라 명명했다.

이는 버전 번호의 형식과 이후 버전 번호로 증가할 때의 정확한 API 동작 차이를 설명한다. Pub에서는 버전이 이런 방식으로 포맷되어야 하며, Pub 커뮤니티와 잘 어울리려면 패키지가 지정한 의미 체계를 따라야 한다. 또한 의존하는 패키지도 이를 따른다고 가정해야 한다.

의미론적 버전 관리가 1.0.0 이전 버전들 사이의 호환성을 약속하지는 않지만 Dart 커뮤니티 규칙에서는 이러한 버전들도 의미론적으로 처리한다. 각 숫자별로 해석해보면 0.1.2에서 0.2.0은 중대한 변경사항이 추가되었음을 나타내고 0.1.3은 새로운 기능이 추가되었음을 나타낸다. 0.1.2+1과 같이 표시할 수도 있는데 이는 공개 API에 영향을 미치지 않는 변경을 나타낸다. 하지만 보다 단순하고 명확하게 관리하기 위해 0.1.2+1과 같은 명명 방식은 1.0.0 이후부터는 사용하지 않도록 하자.

제약 조건 해결

패키지를 정의할 때 사용하는 직접 종속성(Immediate dependency) 목록들을 직접 나열할 수 있다. 이러한 각 패키지에 대해 패키지에서 허용하는 버전 범위를 지정한다. 이러한 종속 패키지 각각은 고유한 종속성을 가질 수 있다. 이를 이적 종속성(transitive dependency)이라 한다. Pub은 이를 탐색하여 앱의 전체 종속성 그래프를 빌드한다.

그래프의 각 패키지에 대해 pub는 종속된 모든 것을 살펴보고, 모든 버전 제약 조건을 모아서 동시에 해결하려 한다. 우선 중첩되는 범위를 교차하여 얻어내고 해당 패키지에 릴리스된 실제 버전들 중 모든 제약 조건을 충족하는 가장 최신 버전을 선택한다.

collection을 예로 3개의 패키지가 다음과 같이 종속되어 있다고 가정해보자.

>=1.7.0
^1.4.0
<1.9.0

collection의 개발자들은 다음 버전을 아래와 같이 출시했다.

1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0

모든 범위에 맞는 가장 높은 버전 번호는 1.8.2이므로 pub에서는 이를 선택한다. 이 후 앱과 앱에서 사용되는 모든 패키지들은 모두 collection 1.8.2를 사용한다.

제약 컨텍스트

패키지 버전을 선택할 때 그에 종속된 모든 패키지를 고려한다는 사실은 중요한 결과를 낳는다. 패키지에 대해 선택되는 특정 버전은 해당 패키지를 사용하는 앱의 전역 속성이다.

다음의 예는 이것의 무엇을 의미하는지 보여준다. 앱이 두 개가 있다고 가정해보자. 다음은 그 것들의 pubspec.yaml이다.

name: my_app
dependencies:
widgets:
name: other_app
dependencies:
widgets:
collection: "<1.5.0"

두 패키지는 아래와 같은 pubspec.yaml을 갖는 widgets에 의존한다.

name: widgets
dependencies:
collection: ">=1.0.0 <2.0.0"

other_app 패키지는 그 자체가 가지는 collection에 의존한다. 흥미로운 점은 widgets 패키지가 다른 버전 제약 조건을 가지고 있다는 것이다.

즉, widgets 패키지를 단독으로 보고 collection의 어떤 버전을 사용할지 알아낼 수는 없다. 이는 콘텍스트 상황에 따라 달라진다. my_app에서는 widgetscollection 1.9.9를 사용할 것이다. 하지만 other_appwidgets은 다른 제약 때문에 collection 1.4.9를 사용할 수 있다.

각 앱이 자체 package_config.json 파일을 갖는 이유가 여기있다. 각 패키지에 대해 선택된 구체적인 버전은 포함된 전체 종속성 그래프에 따라 달라진다.

Exported dependencies에 대한 제약 조건 해결

패키지 작성자는 패키지 제약 조건을 신중하게 정의해야 한다. 아래 시나리오를 예시로 보자.

Exported dependencies tree

bookshelf 패키지는 widgets에 의존한다. 현재 1.2.0widgets패키지는 collection에서 export package:collection/collection.dart를 통해 내보내고, 이 버전은 2.4.0이다. pubspec.yaml로 보면 아래와 같다.

name: bookshelf
dependencies:
widgets: ^1.2.0
name: widgets
dependencies:
collection: ^2.4.0

패키지 collection2.5.0으로 업데이트 되었을 때를 보자. collection 2.5.0에는 sortBackwards() 라는 새 메서드가 포함되어 있다. 이는 widgets의 일부분이므로 bookshelfcollection 종속성만 가지고 있더라도 sortBackwards()를 호출할 수 있다.

widgets API에 버전 번호가 명시되어 있지 않기 때문에 bookshelf를 사용하는 앱은 sortBackwards()를 호출할 때 충돌이 발생할 수 있다.

이러한 이유로 Exported dependencies를 다룰 때 패키지 작성자는 종속성의 상한과 하한에 대해 더 엄격한 제한을 유지하는 것이 좋다. 이 경우 widgets 패키지의 범위를 좁혀야 한다.

name: bookshelf
dependencies:
widgets: ">=1.2.0 <1.3.0"
name: widgets
dependencies:
collection: ">=2.4.0 <2.5.0"

widgets1.2.0이상 1.3.0 미만, collection2.4.0이상 2.5.0 미만으로 제약 조건을 수정했다.

이 규칙을 사용하면 하나가 직접적인 종속성이 아니더라도 사용자가 두 패키지의 올바를 버전을 모두 사용할 수 있다.

잠금 파일

그럼 pub에서 앱의 버전 제약을 해결하면 그 다음은 어떻게 될까? 최종 결과는 앱이 직접 또는 간접적으로 의존하는 모든 패키지의 전체 목록과 앱의 제약과 함께 작동하는 해당 패키지의 최상위 버전이다.

각 패키지에 대해 pub은 해당 정보를 가져와서 콘텐츠 해시를 계산하고, 둘다 앱의 디렉토리에 있는 pubspec.lock 잠금 파일에 쓴다. pub이 앱의 파일을 빌드할 때, dart_tool/package_config.json 잠금 파일을 사용하여 각 패키지의 어떤 버전을 참조해야 하는지 알아낸다. (그리고 어떤 버전을 선택했는지 궁금하다면 잠금 파일을 열어서 확인할 수 있다.)

다음으로 중요한 pub의 작업은 잠금 파일을 건드리지 않는 것이다. 앱에 대한 잠금 파일을 얻으면 pub은 사용자가 잠금 파일을 건드리라고 지시할 때까지 건들지 않는다. 이는 매우 중요하다. 의도하지 않고 앱에서 무작위 패키지의 새 버전을 자발적으로 사용하지 않는다는 의미이다. 앱이 잠기면 수동으로 잠금 파일을 업데이트하라고 지시할 때까지 잠긴 상태로 유지된다.

패키지가 앱용이라면 소스 제어 시스템에 잠금 파일을 체크인하자. 이렇게 하면 팀의 모든 구성원이 앱을 빌드할 때 모든 종속성의 정확히 동일한 버전을 사용하게 된다. 앱을 배포할 때도 이를 사용하여 프로덕션 서버에서 개발 중인 것과 정확히 동일한 패키지를 사용하고 있는지 확인할 수 있다.

문제가 생기는 경우들

물론, 모든 것은 종속성 그래프가 완벽하고 흠이 없다는 것을 전제로 한다. 버전 범위와 pub의 제약 조건 해결 및 의미론적 버전 관리를 사용하더라도 버전 충돌의 위험으로 부터 완전히 자유로울 수는 없다.

다음은 문제가 발생하는 예시들이다.

분리된 제약 조건을 가질 수 없음

앱이 widgetstemplates를 사용한다고 가정해 보자. widgets 에서는 collection >=1.0.0 <2.0.0을 사용하고 templates에서는 collection >=3.0.0 <4.0.0을 사용한다면 겹치는 버전 범위가 없다.

릴리스된 버전이 포함되지 않은 범위를 가지는 경우

공유 종속성에 대한 모든 제약 조건을 함께 넣은 좁은 범위가 있다고 가정해보자. 종속성의 버전이 >=1.2.4 <1.2.6 로 설정되어있다면 1.2.4 버전은 문제없이 동작할 것 이다. 하지만 만약 1.3.0을 사용한다면 범위를 벗어날 것이다.

불안정한 그래프 구조

이는 지금까지 pub의 버전 해결 프로세스에서 가장 어려운 부분이다. 이 프로세스는 종속성 그래프를 구축한 다음 모든 제약 조건을 해결하고 버전을 선택하는 것으로 설명되었다. 하지만 실제로는 그런식으로 동작하지 않는다. 버전을 선택하기 전에 전체 종속성 그래프를 어떻게 구축할 수 있을까? pubspec.yaml 자체로는 버전에 따라 다르다. 동일한 패키지라도 다른 버전을 불러온다면 다른 종속성 집합을 가지고 있을 수 있다.

패키지 버전을 선택하면 종속성 그래프 모양 자체가 변경된다. 그래프가 변경되면 제약 조건이 변경되어 다른 버전을 선택하게 되고, 이후 다시 실행된다.

때때로 이 과정은 결코 안정적인 해결책으로 정착되지 않는다. 예시를 보자

name: my_app
version: 0.0.0
dependencies:
yin: ">=1.0.0"
name: yin
version: 1.0.0
dependencies:
name: yin
version: 2.0.0
dependencies:
yang: "1.0.0"
name: yang
version: 1.0.0
dependencies:
yin: "1.0.0"

이 모든 경우에, 앱에 맞는 구체적인 버전은 없으며, 이런 일이 발생하면 pub에서 오류를 보고하고 무슨 일이 일어나고 있는지 알려준다. 뭔가가 작동할 수 있다고 생각하지만 작동하지 않는 이상한 상태에 빠지지는 않을 것이다.

정리

  • 코드 재사용에는 장점도 있지만, 패키지는 독립적으로 수정될 수 있어야한다.
  • 버전 관리로 독립성을 확보할 수 있다. 구체적인 단일 버전에 의존하는 것은 유연성을 해친다. 공유 종속성을 활용하면 버전 잠금을 활용한다.
  • 버전 잠금을 처리하려면 패키지가 다양한 버전에 의존해야한다. 그런 다음 Pub은 종속성 그래프를 탐색하여 사용자에게 가장 적합한 버전을 선택한다. 적절한 버전을 선택할 수 없는 경우 Pub에서 경고한다.
  • 앱이 종속성에 대한 견고한 버전 세트를 갖추면 해당 세트는 잠금 파일에 고정된다. 그럼 앱을 실행하는 모든 머신이 동일한 종속성 버전을 사용하게 된다.