대규모 Dart 프로젝트의 단계적 null-safety 마이그레이션
대규모 Dart 프로젝트의 단계적 null-safety 마이그레이션
Dart null-safety 마이그레이션은 단순하고 작은 패키지라면 1-2시간이면 끝나지만, 대규모 프로젝트라면 몇 달이 걸리는 마라톤이 될 수 있어요. 이상적으로는 프로젝트를 단계적으로 마이그레이션하고 싶을 거예요 — 마라톤 동안에도 프로젝트를 확장 가능하고, 유지보수 가능하며, 출시하기 쉽게 유지하고 싶으니까요. 저는 대규모 프로젝트를 null-safety로 마이그레이션하면서, 이 과정을 안정적이고 효율적으로 만드는 단계와 팁을 정리했어요. 여러분의 시간을 아껴주길 바라요.
본문
1단계: unsound null-safety로 전환
먼저 프로젝트를 unsound null safety로 마이그레이션해요. 의존성을 null-safe 버전으로 업그레이드하는 것부터 시작해요. Unsound null safety는 모든 의존성이 null-safe일 것을 요구하지 않아요. 하지만 상위 의존성이 모두 마이그레이션될 때까지 기다리는 것을 강력히 권장해요. 의존성을 마이그레이션하면 자체 코드의 마이그레이션 결정을 다시 검토해야 할 수도 있으니까요. 서로 의존하는 패키지의 경우 순서를 벗어나 마이그레이션하거나 패키지를 동시에 마이그레이션해야 할 수도 있어요(서로 의존하는 많은 패키지는 대부분 테스트에서만 서로를 참조해요). 코드를 마이그레이션하기 전에 dart.dev의 지침에 따라 업그레이드할 수 있는 의존성을 최대한 많이 업그레이드해요. 다음으로 패키지의 Dart SDK를 업데이트하고 다음 단계에 따라 마이그레이션되지 않은 각 라이브러리를 legacy로 표시해요.
- IDE(VSCode, IntelliJ / Android Studio)에 Dart 플러그인이 설치되어 있는지 확인해요.
- IDE에서 패키지를 열고 컴파일 오류가 없는지 확인해요.
pubspec.yaml파일의dart_sdk의존성을 버전 범위'>=2.12.0 <3.0.0'을 요구하도록 업데이트해요.- IDE가 아직 null-safe가 아닌 라이브러리의 null-safe 관련 오류를 강조 표시할 거예요. 각 영향받는 파일 맨 위에
'// @dart=2.9'주석을 추가해 오류를 제거해요. - 오류가 없더라도 main.dart 파일에 주석을 추가해서, 전환할 준비가 될 때까지 앱이 unsound 모드로 계속 실행되도록 해요.
- 모든 테스트가 통과하는지 확인하고 변경 사항을 메인 브랜치에 제출해요.
- 테스트가 이미 null-safe라면 null-safe 오류를 억제하기 위해 명령줄 플래그
--no-sound-null-safety가 필요할 거예요. - 애플리케이션 시작 시 콘솔에
Running with unsound null-safety가 보이는지 확인해요.
이제 한 라이브러리씩 프로젝트를 sound null-safety로 마이그레이션할 준비가 됐어요.
2단계: sound null-safety를 향해 반복
마이그레이션할 라이브러리(또는 라이브러리 집합)를 선택해요. 프로 팁: 선택한 라이브러리가 크다면 마이그레이션 전에 더 작은 라이브러리로 나누는 게 좋을 수 있어요. dart pub deps를 사용해 프로젝트의 의존성 그래프를 만들어요. 패키지는 아래에서 위로 마이그레이션하는 것이 가장 좋아요. 의존성 트리의 잎(leaf)에서 시작해 루트까지 반복해요. 하지만 프로젝트에 의존성 순환이 있다면 이 순서가 불가능할 수 있는데, 이 순서를 따르지 않아도 괜찮아요. 마이그레이션 도구를 사용해 라이브러리(또는 라이브러리 집합)를 마이그레이션해요.
dart migrate --skip-import-check를 실행해 대화형 마이그레이션 도구를 시작해요. 트리 탐색을 쉽게 하기 위해 선택한 라이브러리가 있는 디렉토리로cd해도 좋아요.- 왼쪽 패널에서 파일 뷰 트리의 루트를 선택 해제해 모든 것을 선택 해제해요. (관심이 있다면 Deselect All 버튼에 투표해 주세요.)
Control+F를 사용해 마이그레이션하려는 파일을 찾아요.- 파일을 선택하고 Apply Migration을 클릭해요.
조정하는 방법은 두 가지가 있어요: (1) 마이그레이션 적용 전에 주석으로 도구의 선택을 조정하거나, (2) 마이그레이션 적용 후에 IDE를 사용해 필드, 매개변수, 변수의 null 가능성(nullability)을 평가해요.
- IDE에서 패키지를 열어요.
- 오류를 고치고, 도구가 부정확할 수 있는 경우를 파일에서 검색해요(아래 잠재적 문제 목록 참조).
- upstream 및 downstream 코드를 대화형으로 정리하기 위해 lint 경고를 사용해 수정해요.
두 가지 lint 오류는 고칠 수 없을 거예요.
import_of_legacy_library_into_null_safe(마이그레이션된 라이브러리에서)avoid_redundant_argument_values(레거시 라이브러리에서)
지금은 이 오류들을 주석으로 비활성화해요. 마이그레이션이 끝난 후에 정리할 거예요.
주의해야 할 잠재적 도구 부정확성:
dynamic또는num타입이 추가된 경우. 대신 어떤 특정 타입을 사용해야 하는지 아마 알고 있을 거예요. 대부분의 경우bool?은 기본값과 함께bool이 될 수 있어요.- 타입 캐스트(
' as '검색)는 도구가 제네릭 타입 매개변수를 추가하지 않았다는 뜻일 수 있어요. 추가하면 캐스트가 불필요해졌다는 것을 lint가 나타내므로 제거할 수 있어요. - 어떤 경우 도구는 제네릭 매개변수의 바운드를 nullable로 만들기도 하는데, non-nullable로 만드는 게 더 나을 수 있어요(
?>와?,검색). - 도구는
late나late final로 더 잘 표현되거나, 생성자의 이니셜라이저 리스트에서 초기화할 수 있도록 리팩터링할 수 있는 것들을 nullable로 만들 수도 있어요. (관심이 있다면 lint에 투표해 주세요.) - null 값 확인 없이 null 단언 연산
!을 사용하는 것은 변수나 매개변수가 실제로 non-nullable이어야 한다는 뜻일 수 있어요. (관심이 있다면 lint에 투표해 주세요.) - 도구는
collection as Iterable<TheType>형태로 컬렉션에 캐스팅을 추가해요. 때로는 이 변경이 이미 암시적이던 캐스트를 명시적으로 만들 뿐이에요. 하지만 다른 경우에는 이런 캐스트가 제네릭 인자의 nullability 불일치 때문에 런타임 오류를 도입할 수 있어요. 의심스러우면 캐스트를 명시적 요소별 변환(예:collection.cast<TheType>())으로 바꾸거나,package:collection의whereNotNull확장 메서드 사용을 고려해 보세요. - 변수, 필드, 매개변수가 nullable이지만 nullability가 테스트에서만 사용된다면, 그 식별자에서 nullability를 제거하도록 코드를 리팩터링하는 게 좋을 수 있어요. (이 케이스들을 찾는 데 도움을 준 Kenzie Davisson에게 감사드려요.)
3단계: 정리
모든 라이브러리를 마이그레이션한 후 최종 정리를 해요.
- lint 비활성화 주석을 정리해요.
- 남은 의존성을 null-safe 버전으로 업그레이드해요.
- 앱에
//@dart = 2.9주석이 남아 있지 않은지 확인해요. - 이 시점에 앱 시작 시 콘솔에
Running with sound null-safety가 보여야 해요. 보이지 않는다면 아직 마이그레이션되지 않은 라이브러리(// @dart = 2.9검색) 또는 마이그레이션되지 않은 의존성이 있는 거예요. - 앱이 여전히 올바르게 실행되고 테스트가 통과하는지 확인해요. sound 모드는 더 강한 런타임 보장을 가능하게 하므로, sound null safety를 켤 때 고쳐야 할 새로운 런타임 오류가 (드물지만) 보일 수 있어요. 보통은 nullable 컬렉션(예:
List<int?>)을 non-nullable 컬렉션 타입(예:List<int>)으로 캐스팅한 결과예요.
즐거운 마이그레이션 되세요!
더 알아보기
- Null safety migration guide — 소리 있는(null-safety) 마이그레이션 가이드
- Unsound null safety — unsound null safety 개념
- package:collection
whereNotNull— nullable 이터러블에서 null을 걸러내는 확장 메서드