Riverpod 2.x → 3.x 마이그레이션 7단계 생존기
저는 두 개의 Flutter 앱을 운영하고 있습니다. BrainFit은 우주 테마의 두뇌 훈련 RPG로 lib/ 안에 332개 파일, 약 115,900줄의 코드가 있습니다. BloomCard는 정원 테마의 플래시카드 앱으로 329개 파일, 약 36,000줄입니다. 두 앱 모두 같은 의존성 스택을 공유합니다: 상태관리에 Riverpod, 라우팅에 GoRouter, 로컬 DB에 Drift, 분석에 Firebase, 수익화에 AdMob, 구독에 RevenueCat, 백엔드에 Supabase.
2026년 3월 초, flutter pub outdated를 실행했더니 거의 모든 의존성이 뒤처져 있었습니다. 패치 버전 하나 차이인 것도 있었지만, 메이저 버전이 두 단계나 밀린 것도 있었습니다. Riverpod 생태계만 해도 2.x에서 3.x로 넘어가면서 StateNotifier의 동작 방식 자체가 바뀌었고, GoRouter는 15.1에서 17.1로, AdMob은 5.3에서 7.0으로, Firebase core는 3.13에서 4.5로 올라가 있었습니다.
선택지는 두 가지였습니다. 한 번에 전부 올리고 며칠간 깨진 빌드와 씨름하거나, 단계별로 나눠서 매 단계마다 컴파일 가능한 상태를 유지하거나. 저는 후자를 선택했습니다. 이 글은 그 과정에 대한 기록입니다.
왜 단계별 마이그레이션인가
패키지 업그레이드의 유혹은 그냥 전부 올리고, flutter pub get 돌리고, 깨지는 걸 고치는 것입니다. 작은 앱이라면 이 방식도 괜찮습니다. 하지만 합쳐서 660개 이상의 Dart 파일, 약 152,000줄의 코드를 가진 두 앱에서 이걸 하면 재앙입니다.
이유는 간단합니다. 15개 패키지를 동시에 올리고 빌드가 깨지면, 어떤 업그레이드가 문제를 일으켰는지 알 수 없습니다. Riverpod 어노테이션 변경이 새 build_runner와 충돌한 건지, GoRouter API가 StatefulShellRoute 시그니처를 바꾼 건지, Firebase core 버전이 호환되지 않는 전이적 의존성을 끌어온 건지 알 길이 없습니다. 코드베이스 전체를 뒤지며 두더지 잡기 게임을 하게 됩니다.
단계별 마이그레이션은 각 업그레이드 스텝을 원자적으로 만들어 이 문제를 해결합니다. 3단계에서 뭔가 깨지면, 정확히 어떤 패키지가 변경됐는지 알 수 있습니다. 5단계에서 테스트가 실패하면, 4단계와 diff를 비교해서 정확히 뭐가 바뀌었는지 볼 수 있습니다.
심리적 이점도 있습니다. 빅뱅 마이그레이션은 벽처럼 느껴지지만, 7단계 계획은 체크리스트처럼 느껴집니다. 3단계까지 하고 빌드를 배포한 뒤, 내일 4단계를 이어서 할 수도 있습니다. 매 체크포인트에서 앱이 동작합니다.
전략: 리스크 순서 정렬
모든 outdated 패키지를 두 가지 차원으로 정렬했습니다: 브레이킹 체인지 리스크와 의존성 커플링. 리스크가 낮고 독립적인 패키지를 먼저, 리스크가 높고 깊게 연결된 패키지를 나중에. 논리는 단순합니다: 쉬운 업그레이드에서 예상치 못한 문제가 발생하면, Riverpod 리라이트와 동시에 싸우고 있을 때보다 훨씬 쉽게 잡을 수 있습니다.
BrainFit을 기준으로 세운 계획을 공유합니다. BloomCard도 비슷한 구조를 따랐지만, 같은 교훈이 적용되기에 더 적은 단계로 통합했습니다.
Phase 1: 마이너/패치 업데이트
패키지: drift 2.25 → 2.28, drift_dev 2.25 → 2.28, purchases_flutter 9.12 → 9.13
API 변경 없는 마이너 버전 업데이트였습니다. 커밋에서 변경된 파일은 딱 두 개: pubspec.yaml과 pubspec.lock. Dart 코드는 한 줄도 안 바뀌었고, 모든 테스트가 첫 시도에 통과했습니다.
별도 단계로 나눈 게 낭비처럼 보일 수 있지만, 중요한 목적이 있었습니다: 기본 업그레이드 워크플로가 정상 작동하는지 확인한 것입니다. Flutter의 의존성 리졸버는 전이적 버전 제약 때문에 예상 밖으로 실패할 수 있습니다. 쉬운 것들을 먼저 처리해서 툴체인이 건강한지 확인한 셈입니다.
중요한 점이 하나 있습니다. 이 단계에서 build_runner, freezed, json_serializable, mockito도 같이 올리고 싶었는데, 차단됐습니다. 기존 riverpod_generator 2.x가 build ^2.0.0 제약을 가지고 있어서 이 패키지들의 새 버전과 충돌했습니다. Riverpod 자체가 업그레이드되는 Phase 7까지 미뤄야 했습니다. 단계별 계획이 왜 중요한지 보여주는 실전 사례입니다 -- 의존성 충돌을 일찍 발견하고 그에 맞춰 계획할 수 있었습니다.
Phase 2: 저위험 메이저 업데이트
패키지: connectivity_plus 6.1 → 7.0, device_info_plus 11.3 → 12.3, package_info_plus 8.3 → 9.0, flutter_lints 5.0 → 6.0
메이저 버전 업데이트이긴 하지만, API 변경이 거의 없는 유틸리티 패키지들입니다. _plus 계열 플러그인(connectivity, device info, package info)은 플랫폼 채널 구조 변경 때문에 메이저 버전을 올리는 경향이 있지, Dart API를 바꾸지는 않습니다. 예상대로 코드 변경이 전혀 필요 없었습니다.
유일한 조정은 analysis_options.yaml이었습니다. 새 flutter_lints 6.0이 unnecessary_underscores 린트 규칙을 도입했는데, Flutter 콜백에서 흔히 사용하는 __ 패턴("이 파라미터는 신경 안 써" 관례)을 잡아냈습니다. 코드베이스 전체의 콜백 시그니처를 다시 쓰는 대신 해당 린트만 비활성화했습니다. 순수함보다 실용성을 택한 것입니다.
Phase 3: 중위험 메이저 업데이트
패키지: google_sign_in 6.2 → 7.2, flutter_local_notifications 18.0 → 21.0, fl_chart 0.70 → 1.1
여기서부터 실제 코드 변경이 시작됐습니다. 세 파일이 수정되었습니다:
google_sign_in은 생성자 기반 API(GoogleSignIn())에서 싱글톤 패턴(GoogleSignIn.instance.authenticate())으로 전환됐습니다. accessToken 속성은 완전히 제거됐습니다. supabase_auth_service.dart에서 Google 로그인 플로를 새 authenticate 메서드를 사용하도록 다시 작성했습니다. 변경량은 적었지만 개념적으로는 상당한 변화였습니다 -- 플러그인이 더 이상 raw OAuth 토큰을 제공하지 않습니다.
flutter_local_notifications는 핵심 메서드들(initialize(), periodicallyShow(), cancel())에서 위치 기반 파라미터를 이름 기반 파라미터로 전환했습니다. notification_service.dart에서 간단한 찾기-바꾸기로 처리할 수 있었지만, 다른 열 가지 브레이킹 체인지와 동시에 발생했다면 디버깅하기 까다로웠을 유형의 변경입니다.
fl_chart는 tooltipRoundedRadius를 tooltipBorderRadius로 이름을 변경하고 타입을 double에서 BorderRadius로 바꿨습니다. BQ 개요 차트 위젯에서 한 줄이 변경됐습니다. 사소하지만, 더 큰 문제들과 섞여 있었다면 혼란스러운 컴파일 에러가 됐을 것입니다.
Phase 4: Firebase 생태계
패키지: firebase_core 3.13 → 4.5, firebase_analytics 11.6 → 12.1.3
Firebase에 별도 단계를 부여한 이유는 Firebase 패키지들이 내부적으로 촘촘한 버전 커플링을 가지고 있기 때문입니다. firebase_core만 올리고 firebase_analytics를 안 올리면, 버전 리졸버가 호환되지 않는 플랫폼 의존성에 대해 불평합니다. 둘을 함께 올리고 다른 건 건드리지 않음으로써 Firebase 관련 이슈를 격리했습니다.
결과적으로 이 단계에서는 코드 변경이 전혀 필요 없었습니다. Dart API가 완전히 호환됐습니다. 메이저 버전 업은 네이티브 SDK 업데이트에 의한 것이었습니다. 2,490개 테스트 전부 수정 없이 통과했습니다.
Phase 5: AdMob 5.3 → 7.0
패키지: google_mobile_ads 5.3 → 7.0
광고 SDK의 2-메이저-버전 점프는 별도 단계를 받을 자격이 있었습니다. BrainFit에는 광고 관련 서비스 파일이 네 개 있습니다: 배너용 AdService, 전면 광고용 InterstitialAdService, 리워드 광고용 RewardedAdService, 그리고 AppConfig의 광고 설정. BloomCard도 비슷한 광고 인프라를 가지고 있습니다.
상당한 API 변경을 예상했습니다 -- 새 광고 형식 클래스, 초기화 변경, 콜백 시그니처 업데이트. google_mobile_ads 5.x와 7.x 사이의 변경 로그에는 광고 인스펙터 API와 미디에이션 어댑터 관련 여러 브레이킹 체인지가 나열되어 있었습니다.
실제로는 우리 사용 방식에 영향을 주는 변경이 없었습니다. 핵심 BannerAd, InterstitialAd, RewardedAd 클래스는 우리가 사용하는 표준 로드-앤-쇼 패턴에 대해 하위 호환성을 유지했습니다. 브레이킹 체인지는 우리가 사용하지 않는 고급 기능(미디에이션, 광고 인스펙터)에 있었습니다. Dart 파일 변경 제로. 2,490개 테스트 전부 통과.
기분 좋은 놀라움이었지만, AdMob에 별도 단계를 부여한 걸 후회하지 않습니다. 만약 브레이킹 체인지가 있었다면, 광고 로딩 이슈를 GoRouter 라우팅 변경과 동시에 디버깅하는 건 정말 고통스러웠을 것입니다. 광고는 테스트하기 특히 까다롭습니다 -- 실패가 런타임에서만 발생하고 기기별로 다른 경우가 많으니까요.
Phase 6: GoRouter 15.1 → 17.1
패키지: go_router 15.1 → 17.1
역시 2-메이저-버전 점프입니다. BrainFit은 GoRouter를 광범위하게 사용합니다: 4개 탭(Home, Training, Social, Profile) 아래 StatefulShellRoute로 구성된 32개 라우트. BloomCard도 비슷한 쉘 기반 라우팅 구조를 가지고 있습니다.
GoRouter 16.x와 17.x는 StatefulShellRoute 파라미터, 내비게이션 옵저버 API, 리다이렉트 동작에 변경을 도입했습니다. 특히 StatefulShellRoute 변경이 걱정됐는데, BrainFit의 전체 탭 내비게이션이 이것에 의존하기 때문입니다. 분석 서비스도 화면 트래킹을 위해 GoRouter 옵저버를 사용하고 있어서 또 하나의 잠재적 파손 지점이었습니다.
다시 한번, 실제 마이그레이션에 코드 변경이 필요 없었습니다. 우리의 StatefulShellRoute와 GoRouterObserver 사용이 안정적으로 유지된 API 부분에 해당했습니다. 변경된 파일은 두 개: pubspec.yaml과 pubspec.lock. 2,490개 테스트 전부 통과.
돌아보면, 이건 주목할 만한 패턴입니다. Flutter 생태계에서 많은 "메이저 버전" 업데이트가 내부 구조 변경이나 니치 API의 브레이킹 체인지에 의해 주도됩니다. 사용 방식이 일반적이라면 메이저 업데이트를 무사히 통과하는 경우가 많습니다. 하지만 시도해보기 전까지는 그걸 알 수 없고, 이것이 바로 단계별 마이그레이션이 가치 있는 이유입니다.
Phase 7: Riverpod 2.x → 3.x
패키지: flutter_riverpod 2.6 → 3.3, riverpod_annotation 2.6 → 4.0, riverpod_generator 2.6 → 4.0, 그리고 이전에 차단됐던 패키지들: build_runner 2.4 → 2.12, drift 2.28 → 2.31, drift_dev 2.28 → 2.31, freezed 3.0 → 3.2, json_serializable 6.9 → 6.13, json_annotation 4.9 → 4.11, mockito 5.4 → 5.6
이것이 본 게임이었습니다. Riverpod을 마지막으로 남긴 이유는 모든 곳에 닿기 때문입니다. 상태 관리는 Flutter 앱의 신경계입니다. 모든 화면, 모든 프로바이더, 모든 반응형 상태가 Riverpod을 통해 흐릅니다. BrainFit만 해도 14개 기능 모듈에 걸쳐 수십 개의 프로바이더가 퍼져 있습니다.
StateNotifier에서 Notifier로
Riverpod 3.x의 핵심 변경은 StateNotifier를 더 이상 사용하지 않고(deprecated) 새로운 Notifier 클래스로 대체한 것입니다. 이것은 단순한 이름 변경이 아닙니다 -- API 표면이 근본적으로 다릅니다:
StateNotifier는 생성자에서 초기 상태를 설정하고 변경 가능한state속성을 노출합니다Notifier는build()메서드로 초기 상태를 계산하며 코드 생성과 더 자연스럽게 통합됩니다
BrainFit에서는 4개 파일에 걸친 10개 클래스가 마이그레이션이 필요했습니다: settings providers, senior mode provider, achievement popup provider, translations provider. 각각 클래스 정의를 다시 작성하고, 상태 초기화를 생성자에서 build() 메서드로 옮기고, 프로바이더 선언을 StateNotifierProvider에서 NotifierProvider로 변경해야 했습니다.
AsyncValue.valueOrNull에서 .value로
Riverpod 3.x는 AsyncValue.valueOrNull을 폐지하고 단순히 .value로 대체했습니다. 사소하게 들리지만, 20개 파일에 걸쳐 43곳에 영향을 미쳤습니다. 비동기 프로바이더를 읽고 값에 접근하는 모든 화면이 업데이트가 필요했습니다. 찾기-바꾸기로 대부분 처리했지만, 각 변경마다 null-safety 의미가 보존되었는지 빠르게 확인해야 했습니다.
Override 타입 참조 변경
테스트와 main.dart에서 프로바이더 오버라이드를 선언하는 방식이 바뀌었습니다. 6개 테스트 파일과 main.dart에서 오버라이드 선언을 업데이트해야 했습니다. 특히 main.dart가 까다로웠는데, BrainFit은 스크린샷 모드(Pro 구독 강제 활성화)와 BETA_MODE 스테이징 빌드를 위해 프로바이더 오버라이드를 사용하기 때문입니다.
비동기 안전성: ref.mounted 가드
새 Riverpod은 프로바이더가 dispose된 후 ref에 접근하는 것에 대해 더 엄격합니다. 노티파이어의 여러 비동기 _load 메서드에서 await 포인트 이후에 ref.mounted 가드를 추가해야 했습니다. 이것은 실제로 좋은 변화입니다 -- 실제 버그를 잡아주니까요 -- 하지만 마이그레이션에 마찰을 더했습니다.
코드 생성 재빌드
build_runner와 riverpod_generator 모두 업그레이드됐기 때문에 모든 코드 생성 파일을 재생성해야 했습니다. Drift만 해도 15개 DAO 파일의 재생성이 필요했습니다. 생성된 코드가 변경된 건 스키마 변경 때문이 아니라 코드 생성기 자체의 새 출력 패턴 때문이었습니다.
결과
Phase 7은 BrainFit에서 49개 파일을 건드렸습니다. 마이그레이션에서 단연 가장 큰 단일 커밋이었습니다. 하지만 Phase 1부터 6까지 이미 안정적이었기 때문에, 어떤 컴파일 에러나 테스트 실패든 Riverpod 마이그레이션이 원인이지, Firebase 업그레이드나 GoRouter 변경과의 상호작용이 아님을 알 수 있었습니다. 그 확신이 디버깅을 극적으로 빠르게 만들었습니다.
BloomCard 쪽 이야기
BrainFit에서 7개 단계를 모두 완료한 후, BloomCard에도 같은 업그레이드를 적용했습니다. 두 앱이 같은 스택을 공유하기 때문에 어떤 패키지에 브레이킹 체인지가 있고 어떤 게 드롭인 교체인지 이미 알고 있었습니다. BloomCard의 마이그레이션은 7단계 대신 5단계로 통합됐고, 총 소요 시간은 BrainFit의 대략 절반이었습니다.
BloomCard에는 고유한 특이사항이 있었습니다. share_plus 패키지가 Share.share()에서 SharePlus.instance.share(ShareParams())로 마이그레이션됐는데, BrainFit에는 공유 기능이 없어서 영향이 없었습니다. purchases_flutter 업그레이드는 CustomerInfo 접근 패턴을 PurchaseResult.customerInfo로 변경했습니다. SQLite 패키지는 db.dispose()를 db.close()로 이름을 바꿨습니다.
패키지 업그레이드 후, BloomCard에서 flutter analyze가 78개 린트 이슈를 잡아냈습니다. 분류하면: flutter_riverpod 중복 import에서 남은 미사용 import 12개, unnecessary_underscores 경고 38개, use_build_context_synchronously 이슈 15개, 그리고 기타 소소한 수정들. 전용 정리 단계에서 모두 해결했습니다.
flutter analyze: 52개 경고에서 0으로
이 전체 과정에서 가장 만족스러운 순간은 7개 단계를 모두 완료한 후 BrainFit에서 flutter analyze를 실행한 순간이었습니다. 마이그레이션 전에는 52개의 경고와 정보 메시지가 있었습니다 -- 폐지된 API 사용, 이전 패키지 버전의 린트 위반, 그리고 수개월간의 기능 개발 중 축적된 다양한 코드 품질 이슈들.
마이그레이션과 정리 후, 카운트는 제로였습니다. 깨끗한 분석 결과. 이것은 단순히 외관상의 변화가 아닙니다. 그 경고들 중 많은 것이 결국 제거될 폐지된 API에 대한 것이었습니다. 마이그레이션 도중에 처리함으로써 미래 기술 부채의 한 카테고리 전체를 제거한 셈입니다.
안전망: 3,057개 테스트
이 마이그레이션 성공의 가장 중요한 요소는 테스트 스위트였습니다. BrainFit에는 234개 테스트 파일에 걸쳐 2,273개 테스트가 있습니다. BloomCard에는 784개. 합치면 3,057개의 테스트로 핵심 비즈니스 로직, 데이터베이스 연산, 상태 관리, UI 렌더링, 엔드투엔드 플로를 커버합니다.
매 단계마다 전체 테스트 스위트를 돌렸습니다. 일부가 아니라, 변경한 파일만이 아니라 -- 전체 스위트를. 이것이 수동 테스트로는 놓쳤을 이슈를 잡아냈습니다. 예를 들어, Riverpod 마이그레이션은 위젯 테스트에서 Drift 데이터베이스 스트림이 제대로 정리되지 않는 타이밍 이슈를 드러냈습니다. 테스트 teardown에 명시적 타이머 클린업을 추가할 때까지 테스트가 불안정해졌습니다. 테스트 스위트가 없었다면, 이것은 프로덕션에서 간헐적 크래시로 나타났을 것입니다.
테스트 스위트는 빠르게 움직일 수 있는 자신감도 줬습니다. Phase 5(AdMob 5 → 7)가 변경 없이 2,490개 테스트를 전부 통과했을 때, 진짜로 안전하게 커밋하고 다음으로 넘어갈 수 있다는 걸 알았습니다. 테스트 없이는 다양한 기기, 다양한 네트워크 조건, 다양한 계정 상태에서 광고 로딩을 수동으로 몇 시간 테스트해야 했을 것입니다.
BETA_MODE: 스테이징 검증
마이그레이션 전에 스테이징 빌드를 위한 BETA_MODE 플래그를 구현해 두었습니다. 활성화하면 BETA_MODE가 Pro 기능을 해제하고 테스트 광고 ID를 사용해서, 실제 구독이나 실제 광고 없이 전체 앱 경험을 검증할 수 있습니다. 7개 단계를 모두 완료한 후, BETA_MODE APK를 빌드하고 핵심 사용자 여정을 직접 돌아봤습니다: 온보딩, 게임플레이, 일일 미션, 소셜 기능, 구매, 광고 표시.
이 스테이징 단계에서 추가 이슈는 하나도 발견되지 않았습니다(테스트 스위트가 이미 다 잡았으니까요). 하지만 프로덕션에 푸시하기 전의 필수적인 신뢰 확인이었습니다. 광고 SDK 초기화 타이밍, 인앱 구매 플로 완료, Firebase Analytics 이벤트 전달 같은 것들은 유닛 테스트 환경에서 완전히 테스트하기 어렵습니다.
배운 교훈
편의가 아닌 리스크 순서로 정렬하라. 흥미로운 업그레이드(Riverpod 3!)부터 시작하고 싶은 유혹이 있지만, 지루하고 저위험한 패키지부터 시작하면 툴체인 이슈를 일찍 잡고 모멘텀을 쌓을 수 있습니다.
단계당 하나의 관심사. Phase 4, 5, 6은 각각 하나의 업그레이드만 했습니다: Firebase, AdMob, GoRouter. 이것이 디버깅을 간단하게 만들었습니다. 전체 단계가 "한 가지를 업그레이드"일 때, 뭔가 깨지면 정확히 어디를 봐야 하는지 압니다.
의존성 충돌은 정보다. Phase 1에서 riverpod_generator 제약 때문에 build_runner를 업그레이드할 수 없다는 걸 발견했을 때, 그건 후퇴가 아니라 가치 있는 계획 정보였습니다. Phase 7이 그 지연된 패키지들을 함께 묶어야 한다는 걸 알려줬습니다.
대규모 마이그레이션에서 테스트는 협상 불가다. 두 앱에 걸쳐 3,057개 테스트로 모든 단계가 명확한 통과/실패 시그널로 끝났습니다. 이 커버리지 없이는 이 규모의 마이그레이션이 집중된 하루 대신 수 주간의 수동 리그레션 테스트가 필요했을 것입니다.
메이저 버전이 항상 브레이킹 체인지를 의미하지 않는다. 7단계 중 4단계는 메이저 버전 업데이트임에도 코드 변경이 전혀 필요 없었습니다. Flutter 생태계는 Dart API 변경에 보수적인 편이며, 메이저 버전은 종종 플랫폼 SDK 업데이트나 내부 구조 변경에 의해 주도됩니다. 하지만 시도해보기 전까지는 이걸 알 수 없고, 그래서 격리가 중요합니다.
더 어려운 앱을 먼저 하라. BrainFit이 더 크고, 더 복잡하며, 각 패키지의 더 많은 기능을 사용합니다. BrainFit을 먼저 마이그레이션함으로써 BloomCard의 마이그레이션을 더 빠르고 예측 가능하게 만드는 로드맵을 만들었습니다.
뒷정리를 하라. 마이그레이션 자체가 테스트 통과로 끝나는 게 아닙니다. flutter analyze를 실행하고 모든 경고를 수정하고, 모든 폐지된 API 호출을 제거하고, 모든 린트 이슈를 정리하는 것이 마이그레이션을 진정한 개선으로 만듭니다. 52개 경고에서 0으로 가는 것은 몇 달간 이자를 주는 품질 향상입니다.
마치며
두 개의 프로덕션 Flutter 앱의 전체 의존성 스택을 업그레이드하는 건 부담스럽게 들립니다. 솔직히, 한 번에 하려 하면 정말 그렇습니다. 하지만 7개 단계로 나누고, 리스크 순서로 정렬하고, 강력한 테스트 스위트로 검증하고, 스테이징 빌드로 확인하면, 스트레스 가득한 도박이 아니라 체계적인 체크리스트가 됩니다.
총 마이그레이션은 두 앱에 걸쳐 약 100개 파일을 건드렸고, 25개 이상의 패키지를 업그레이드했으며, 핵심 아키텍처 패턴(StateNotifier에서 Notifier로)을 마이그레이션했고, 두 코드베이스를 이전보다 깨끗한 상태로 남겼습니다. 집중된 작업 하루 정도가 소요됐습니다. 단계별 접근은 단순히 관리 가능하게 만든 것이 아니라 -- 신뢰할 수 있게 만들었습니다.
Flutter 프로젝트에서 outdated 의존성의 벽을 바라보고 있다면, 제 조언은 단순합니다: 코끼리를 한입에 먹으려 하지 마세요. 리스크 순서로 정렬하고, 한 번에 하나의 관심사만 업그레이드하고, 매 단계마다 테스트를 돌리고, 매 체크포인트에서 앱이 빌드 가능한 상태를 유지하세요. 미래의 당신이 고마워할 것입니다.