Thursday, August 10, 2023

플러터 프로젝트의 다트 버전 관리: 실전 핵심 원칙

Flutter 개발 환경에서 Dart SDK 버전은 프로젝트의 안정성, 호환성, 그리고 최신 기능 활용 가능성을 결정하는 핵심적인 요소입니다. 단순히 코드를 작성하는 것을 넘어, 버전을 정확히 이해하고 관리하는 능력은 프로젝트의 성패를 좌우할 수 있습니다. 많은 개발자들이 버전 충돌 문제로 인해 예상치 못한 시간과 노력을 소모하곤 합니다. 이 글에서는 Flutter 프로젝트 내에서 Dart 버전을 확인하고, 안전하게 변경하며, 발생할 수 있는 다양한 문제들을 해결하는 실질적인 방법론을 체계적으로 다룹니다. 초보 개발자부터 숙련된 개발자까지 모두가 자신의 프로젝트를 한 단계 더 높은 수준으로 관리할 수 있도록, 버전 관리의 근본적인 원리부터 고급 관리 도구 활용법까지 폭넓게 살펴보겠습니다.

1. 모든 것의 시작: Flutter와 Dart 버전의 관계 이해하기

본격적인 버전 관리에 앞서, Flutter와 Dart가 어떻게 상호작용하는지, 그리고 버전 번호가 무엇을 의미하는지 명확히 이해해야 합니다. 많은 개발자들이 Flutter 버전과 Dart 버전을 별개의 것으로 생각하지만, 실제로는 매우 긴밀하게 연결되어 있습니다.

1.1 Flutter SDK에 내장된 Dart SDK

가장 중요한 사실은, Flutter SDK를 설치하면 특정 버전의 Dart SDK가 함께 설치된다는 것입니다. Flutter 프레임워크는 특정 Dart 언어 기능과 API에 의존하여 빌드되므로, 각 Flutter 릴리스는 호환성이 검증된 특정 버전의 Dart SDK를 포함하고 있습니다. 즉, 개발자가 별도로 Dart를 설치할 필요 없이, Flutter만으로 개발 환경이 구성됩니다.

터미널에서 다음 명령어를 실행하면 현재 활성화된 Flutter 버전과 그에 포함된 Dart 버전을 동시에 확인할 수 있습니다.

flutter --version

이 명령어의 결과는 다음과 유사한 형태로 나타납니다:

Flutter 3.16.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 78666c8dc5 (6 weeks ago) • 2023-12-15 15:04:33 -0800
Engine • revision 3f3e560236
Tools • Dart 3.2.3 • DevTools 2.28.4

위 예시에서 우리는 현재 사용 중인 Flutter 버전이 3.16.5이며, 이 버전에는 Dart 3.2.3 버전이 포함되어 있음을 명확히 알 수 있습니다. 이는 시스템 전반에서 사용되는 Dart 버전을 의미하며, 개별 프로젝트에서 요구하는 버전과는 다를 수 있습니다.

1.2 프로젝트 레벨의 Dart 버전 제약: `pubspec.yaml`

Flutter SDK에 포함된 Dart 버전이 '설치된' 버전이라면, 각 Flutter 프로젝트는 자신이 호환되는 Dart 버전의 '범위'를 지정합니다. 이 정보는 프로젝트의 루트 디렉토리에 있는 `pubspec.yaml` 파일에 명시됩니다.

새로운 Flutter 프로젝트를 생성하면 `pubspec.yaml` 파일 내에 `environment` 섹션이 자동으로 생성됩니다.

# flutter create my_app 명령어로 프로젝트 생성
cd my_app

그리고 `pubspec.yaml` 파일을 열어보면 다음과 같은 내용을 확인할 수 있습니다.

name: my_app
description: "A new Flutter project."
publish_to: 'none' 
version: 1.0.0+1

environment:
  sdk: '>=3.2.3 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

여기서 가장 주목해야 할 부분은 `environment` 섹션의 `sdk: '>=3.2.3 <4.0.0'` 입니다. 이 구문은 다음과 같은 의미를 가집니다.

  • >=3.2.3: 이 프로젝트는 Dart SDK 3.2.3 버전 이상에서만 호환됩니다.
  • <4.0.0: 이 프로젝트는 Dart SDK 4.0.0 미만 버전에서만 호환됩니다. 즉, 잠재적인 브레이킹 체인지(Breaking Change)가 포함될 수 있는 메이저 버전 4.0.0은 지원하지 않겠다는 의미입니다.

이처럼 Flutter 개발 환경의 버전 관리는 '시스템 레벨(Flutter SDK)의 Dart 버전''프로젝트 레벨(pubspec.yaml)의 Dart 버전 제약'이라는 두 가지 축으로 이루어집니다. 프로젝트를 성공적으로 실행하고 빌드하기 위해서는 시스템에 설치된 Dart 버전이 프로젝트에서 요구하는 버전 범위 안에 포함되어야 합니다.

2. `pubspec.yaml` 버전 제약 구문 심층 분석

Dart 버전 변경의 핵심은 `pubspec.yaml` 파일의 `environment` 섹션을 정확하게 수정하는 것입니다. 이를 위해서는 유의적 버전 관리(Semantic Versioning, SemVer)와 캐럿(Caret) 구문 등 버전 제약 표기법을 깊이 있게 이해해야 합니다.

2.1 유의적 버전 관리(SemVer)의 원칙

Dart와 Pub(Dart의 패키지 매니저)는 SemVer 2.0.0 표준을 따릅니다. 버전 번호는 `MAJOR.MINOR.PATCH` (예: `3.2.3`) 형식으로 구성되며 각 번호는 다음과 같은 의미를 가집니다.

  • MAJOR (주 버전): 기존 버전과 호환되지 않는 API 변경이 있을 때 올립니다. 예를 들어, 기존 함수가 삭제되거나 매개변수가 변경되는 등 코드를 수정해야만 하는 변경이 포함됩니다.
  • MINOR (부 버전): 기존 버전과 호환성을 유지하면서 새로운 기능이 추가될 때 올립니다. 기존 코드를 수정할 필요 없이 새로운 기능을 사용할 수 있습니다.
  • PATCH (수 버전): 기존 버전과 호환성을 유지하면서 버그를 수정했을 때 올립니다. 기능 추가 없이 내부적인 오류만 수정한 경우입니다.

이 원칙을 이해하면 패키지나 SDK 버전을 업데이트할 때 어떤 종류의 변화를 예상해야 할지 미리 파악할 수 있습니다.

2.2 다양한 버전 제약(Version Constraint) 구문

`pubspec.yaml`에서는 단순한 범위 지정 외에도 다양한 구문을 사용하여 의존성을 정밀하게 제어할 수 있습니다.

모든 버전 허용 (`any`)
sdk: any

어떤 버전이든 허용하지만, 호환성 문제를 야기할 수 있어 프로덕션 환경에서는 절대 사용해서는 안 됩니다.

범위 지정 (`>=`, `>`, `<=`, `<`)
sdk: '>=3.0.0 <4.0.0'

가장 명시적인 방법으로, 허용할 버전의 하한과 상한을 직접 지정합니다. 안정성이 매우 중요할 때 유용합니다.

캐럿 구문 (`^`)
dependencies:
  some_package: ^1.2.3

가장 널리 사용되는 구문입니다. `^1.2.3`은 >=1.2.3 <2.0.0 과 동일한 의미를 가집니다. 즉, 하위 호환성이 보장되는 마이너 및 패치 버전 업데이트는 자동으로 허용하지만, 브레이킹 체인지가 발생할 수 있는 메이저 버전 업데이트는 허용하지 않습니다. 이를 통해 안정성을 유지하면서도 새로운 기능과 버그 수정을 손쉽게 적용할 수 있습니다.

주의: `^0.x.y` 버전의 경우, `>=0.x.y <0.(x+1).0` 으로 해석됩니다. 초기 개발 단계(메이저 버전 0)에서는 API가 수시로 변경될 수 있기 때문입니다.

특정 버전 고정
dependencies:
  another_package: 1.5.2

정확히 `1.5.2` 버전만 사용하도록 고정합니다. 다른 패키지와의 의존성 충돌을 피하기 위해 특정 버전을 강제해야 할 때 사용하지만, 유연성이 떨어져 신중하게 사용해야 합니다.

3. 실전: 안전한 Dart 버전 변경 절차

이론을 바탕으로, 이제 실제 프로젝트에서 Dart 버전을 변경하는 안전하고 체계적인 절차를 알아보겠습니다. 단순히 `pubspec.yaml` 파일의 숫자만 바꾸는 것은 위험하며, 다음과 같은 단계를 따르는 것이 좋습니다.

1단계: 사전 준비 및 백업

가장 중요한 첫 단계는 **변경 전 상태를 보존하는 것**입니다. 버전 관리 시스템(예: Git)을 사용하고 있다면, 모든 변경 사항을 커밋하여 깨끗한 상태(clean state)를 만드세요.

git status # 변경된 파일이 없는지 확인
git add .
git commit -m "Chore: Prepare for Dart SDK version upgrade"

만약의 사태에 대비해 새로운 브랜치를 생성하여 작업하는 것도 매우 좋은 습관입니다.

git checkout -b feature/upgrade-dart-sdk

이렇게 하면 문제가 발생했을 때 언제든지 원래의 안정적인 상태로 쉽게 되돌아갈 수 있습니다.

2단계: 목표 버전 결정 및 변경 사항 검토

어떤 버전으로 업그레이드할지 결정해야 합니다. 단순히 최신 버전을 선택하기보다는, 목표 버전의 공식 릴리스 노트(Release Notes)를 반드시 읽어보아야 합니다. Dart나 Flutter 공식 블로그, GitHub 릴리스 페이지에서 주요 변경 사항, 새로운 기능, 그리고 가장 중요한 **브레이킹 체인지(Breaking Changes)** 목록을 확인할 수 있습니다.

예를 들어, Dart 2.12 버전은 Null Safety라는 매우 큰 변화를 도입했습니다. 만약 2.12 미만 버전에서 Null Safety를 지원하는 버전으로 업그레이드한다면, 단순한 버전 번호 변경 이상의 대규모 코드 마이그레이션이 필요하다는 것을 미리 인지할 수 있습니다.

3단계: `pubspec.yaml` 파일 수정

목표 버전을 결정했다면, `pubspec.yaml` 파일의 `environment` 섹션을 수정합니다. 예를 들어, Dart 3.0 이상을 사용하기로 결정했다면 다음과 같이 변경할 수 있습니다.

environment:
  sdk: '>=3.0.0 <4.0.0'

4단계: 의존성 패키지 해결

파일을 저장한 후, 터미널에서 `flutter pub get` 명령어를 실행합니다. 이 명령어는 단순한 패키지 다운로드가 아니라, 다음과 같은 복잡하고 중요한 작업을 수행합니다.

  1. `pubspec.yaml`에 명시된 모든 의존성(dependencies)과 개발 의존성(dev_dependencies)을 분석합니다.
  2. 새롭게 설정된 Dart SDK 버전 제약(`>=3.0.0 <4.0.0`)을 만족하는 패키지 버전을 찾습니다.
  3. 동시에, 패키지들 간의 상호 의존성(예: A 패키지가 B 패키지의 특정 버전을 요구하는 경우)을 모두 만족시키는 최적의 조합을 찾으려 시도합니다.
  4. 이 과정에서 호환되는 버전을 찾지 못하면, 버전 해결(version solving) 실패 오류를 출력합니다.
  5. 성공적으로 버전을 찾으면, 결정된 패키지들의 정확한 버전 정보를 `pubspec.lock` 파일에 기록하고, 실제 패키지 파일들을 시스템 캐시에 다운로드합니다.
flutter pub get

5단계: 테스트 및 검증

`flutter pub get`이 성공적으로 완료되었다고 해서 끝이 아닙니다. 이제 변경된 버전 환경에서 프로젝트가 정상적으로 동작하는지 철저히 검증해야 합니다.

  1. 정적 분석(Static Analysis): IDE나 터미널에서 `flutter analyze` 명령어를 실행하여 새로운 SDK 버전의 린트 규칙(lint rules)에 위배되는 코드가 없는지 확인합니다.
  2. 자동화 테스트 실행: 프로젝트에 단위 테스트(Unit tests), 위젯 테스트(Widget tests), 통합 테스트(Integration tests)가 있다면 모두 실행하여 통과하는지 확인합니다.
    flutter test
        
  3. 수동 테스트(QA): 애플리케이션을 실제 기기나 에뮬레이터에서 실행하여 주요 기능들이 예상대로 동작하는지 직접 확인합니다. 특히 UI가 깨지거나 특정 기능에서 크래시가 발생하는지 집중적으로 살펴봐야 합니다.

모든 테스트와 검증을 통과했다면, 비로소 Dart 버전 변경 작업이 안전하게 완료되었다고 할 수 있습니다. 변경 사항을 커밋하고 원래의 개발 브랜치에 병합합니다.

4. 버전 충돌 문제 해결 시나리오

버전 변경 과정에서 가장 흔하게 마주치는 문제는 '버전 해결 실패(version solving failed)' 오류입니다. 이는 `pubspec.yaml`에 명시된 제약 조건을 모두 만족하는 패키지 버전 조합을 찾을 수 없을 때 발생합니다.

시나리오: 의존성 패키지의 SDK 버전 미지원

가장 흔한 경우입니다. 프로젝트의 Dart SDK 버전을 `'>=3.0.0 <4.0.0'`으로 올렸는데, 내가 사용 중인 `some_package: ^1.5.0`가 Dart 3.0을 지원하도록 업데이트되지 않은 상황을 가정해 보겠습니다. `flutter pub get`을 실행하면 다음과 유사한 오류 메시지를 보게 될 것입니다.

Resolving dependencies...
Because my_app depends on some_package ^1.5.0 which doesn't support null safety,
  and my_app requires SDK version >=3.0.0 <4.0.0, version solving failed.

해결 방안:

  1. 패키지 업데이트: 가장 먼저 시도할 방법은 해당 패키지를 최신 버전으로 업데이트하는 것입니다. 패키지 개발자가 이미 Dart 3.0을 지원하는 새로운 버전을 출시했을 수 있습니다.
    flutter pub upgrade some_package
        
    또는 `pubspec.yaml`에서 버전 제약을 `any`로 잠시 바꾸거나 최신 버전을 명시하고 `pub get`을 시도해볼 수 있습니다.
  2. `pub.dev`에서 호환성 확인: pub.dev에서 `some_package`를 검색하여 'Versions' 탭을 확인합니다. 각 버전별로 지원하는 Dart SDK 범위를 볼 수 있습니다. 만약 최신 버전도 여전히 Dart 3.0을 지원하지 않는다면 다른 해결책을 찾아야 합니다.
  3. 대체 패키지 검색: 해당 패키지가 더 이상 유지보수되지 않거나 업데이트가 늦어진다면, 유사한 기능을 제공하는 다른 최신 패키지를 찾아 교체하는 것을 고려해야 합니다.
  4. (최후의 수단) `dependency_overrides` 사용:

    특정 하위 의존성 때문에 문제가 발생하고, 이를 강제로 특정 버전으로 지정해야 할 때 `dependency_overrides`를 사용할 수 있습니다. 이는 매우 신중하게 사용해야 하는 기능이며, 예기치 않은 부작용을 낳을 수 있습니다.

    # pubspec.yaml
    dependency_overrides:
      some_dependency_of_a_package: '1.2.0'
    

5. 고급 주제: Flutter SDK 버전 자체 관리하기 (FVM)

지금까지는 단일 Flutter SDK 환경 내에서 프로젝트의 Dart 제약을 관리하는 법을 다루었습니다. 하지만 실제 개발 환경에서는 여러 프로젝트를 동시에 진행하며, 각 프로젝트가 요구하는 Flutter SDK 버전 자체가 다른 경우가 많습니다. 예를 들어, A 프로젝트는 Flutter 3.10.x 버전이 필요하고, B 프로젝트는 최신 안정 버전인 3.16.x를 사용해야 할 수 있습니다.

이런 상황에서 수동으로 Flutter SDK를 여러 개 다운로드하고 PATH를 변경하는 것은 매우 번거롭고 실수하기 쉽습니다. 이때 FVM (Flutter Version Management) 이라는 도구가 매우 유용합니다.

FVM이란?

FVM은 컴퓨터에 여러 버전의 Flutter SDK를 설치하고, 프로젝트별로 사용할 SDK 버전을 간단한 명령어로 지정할 수 있게 해주는 CLI 도구입니다. 이를 통해 "내 컴퓨터에서는 잘 되는데, 동료 컴퓨터에서는 안 돼요"와 같은 '환경 차이'로 인한 문제를 근본적으로 해결할 수 있습니다.

FVM 기본 사용법

  1. 설치:
    # Dart가 설치되어 있다면
    dart pub global activate fvm
    
    # Homebrew (macOS)
    brew tap leoafarias/fvm
    brew install fvm
    
  2. 원하는 Flutter SDK 버전 설치:
    # 특정 버전 설치
    fvm install 3.10.6
    
    # 최신 stable 버전 설치
    fvm install stable
    
  3. 프로젝트별 버전 지정:

    해당 프로젝트의 루트 디렉토리로 이동한 후, 사용할 버전을 지정합니다.

    cd my_project
    fvm use 3.10.6
    

    이 명령을 실행하면 프로젝트 내에 `.fvm`이라는 폴더가 생성되며, 이 폴더는 FVM이 설치한 Flutter SDK 3.10.6 버전을 가리키는 심볼릭 링크를 포함합니다. 이제 이 프로젝트에서는 `flutter` 명령어 대신 `fvm flutter`를 사용하면 됩니다.

  4. FVM을 통한 명령어 실행:
    # 3.10.6 버전의 flutter run 실행
    fvm flutter run
    
    # 3.10.6 버전의 pub get 실행
    fvm flutter pub get
    

IDE(VS Code, Android Studio)와 FVM을 연동하면 `fvm` 접두사 없이도 자동으로 프로젝트에 지정된 Flutter SDK를 사용하도록 설정할 수 있어 더욱 편리합니다. FVM을 도입하면 팀원 모두가 정확히 동일한 Flutter/Dart SDK 버전으로 개발하게 되어, 버전 불일치로 인한 수많은 잠재적 문제를 사전에 방지할 수 있습니다.

결론: 체계적인 버전 관리가 프로젝트의 품질을 결정한다

Flutter 프로젝트에서 Dart 버전을 관리하는 것은 단순히 숫자를 바꾸는 행위가 아니라, 프로젝트의 현재와 미래의 안정성을 책임지는 중요한 엔지니어링 활동입니다. Flutter와 Dart의 버전 관계를 명확히 이해하고, `pubspec.yaml`의 제약 구문을 능숙하게 사용하며, 변경 사항을 체계적으로 테스트하는 절차를 따르는 것이 중요합니다.

또한, FVM과 같은 도구를 활용하여 개발 환경 자체를 일관성 있게 관리함으로써, 개인 프로젝트뿐만 아니라 팀 단위 협업에서도 생산성과 안정성을 크게 향상시킬 수 있습니다. 오늘 다룬 원칙들을 실제 프로젝트에 적용하여, 버전 문제로부터 자유롭고 더욱 견고한 애플리케이션을 만들어 나가시길 바랍니다.


0 개의 댓글:

Post a Comment