Don't use 'BuildContext's across async gaps.
Try rewriting the code to not use the 'BuildContext', or guard the use with a 'mounted' check.
- 블록으로 로그인 이벤트를 처리하는 함수를 작성하고 있었다.
- 로그인을 성공하면 메인화면으로 이동해야했기 때문에 view에서 context를 받아야만 했다.
- 하지만 로그인 함수(비동기)안에서는 context를 건네주지 말라는 문구가 떴다.
문제 원인
BuildContext
BuildContext 는 위젯트리에서 해당 위젯이 트리 어느부분에 위치하고 있는지 알 수 있는 정보이다. 즉 네이밍 그대로 빌드하는데 필요한 정보(문맥)이라고 볼 수 있다. 플러터에서 UI를 그려낼 때 build context는 핵심정보로 작용한다. 이유는 렌더링 방식 때문이다.
공식문서에서 플러팅의 렌더링 단계는 크게 3가지 Layout, Paint, Composition 단계로 소개하고 있다. Layout phase에서는 RenderTree가 상위위젯부터 하위위젯까지 constraints를 전달하고 다시 하위위젯부터 상위위젯까지 size를 전달한다. 당연한 이야기이지만 상위부터 최소/최대 제약조건이 마지막 하위 위젯 제약까지 계산이 되고나서야 사이즈가 결정되고 결정된 사이즈는 하위부터 상위까지 전달한다. 이때 BuildContext 를 사용해 하위위젯에 넘어가고 정보를 뿌리고 혹은 상위위젯에 넘어가서 정보를 뿌리는 방식으로 동작한다. 즉 buildcontext가 UI를 그리는 데 필요한 정보전달에 있어 핵심적인 역할을 하는 것이다.
async Build Context
이때 만약 비동기함수 내부로직에서 build context를 비동기로 주고받으면 어떤문제가 생길까? 먼저 기본전제는 비동기 작업은 작업의 완료시점을 예측할 수 없다. 따라서 작업이 완료되었을 때 어떤 상태변화가 일어났는지 알 수 없다. 여기서 모든 이슈가 출발한다.
가장 첫번째로 Stale Data 이슈이다. Stale은 "약간 상한" 이란 뜻으로 네이밍 그대로 업데이트 되지 않는 "상해버린" 데이터(context)를 참조하고 있을 수도 있다는 것이다. 내부로직에서 context를 받아서 I/O 작업을 하러 떠났는데 그사이에 context 변화가 있게 된다면 I/O 작업을 하러 떠난 작업은 여전히 "상한" context를 통해 작업을 하고 결과물을 리턴하며 그 사이 context 변화를 반영하지 못하는 것이다.
만약 비동기 작업이 실행되는 동안 다른 화면으로 이동해서 context 가 소멸되었다면 비동기작업은 소멸된 context를 참조하면서 런타임 크래쉬가 발생할 수도 있다. 위의 코드에서 비동기 작업(Future.delayed)이 완료된 후, context가 여전히 유효하지 않을 경우를 생각해보자. 예를 들어, 사용자가 버튼을 클릭한 후 화면이 변경되어 해당 위젯이 트리에서 제거되었다면, context는 더 이상 유효하지 않다. 이 상황에서 Navigator.of(context).pop()을 호출하면 런타임 크래시가 발생할 것이다.
두번째는 Memory Leak 가능성이다. 위젯트리가 제거되면 컨텍스트 또한 제거되면서 해당 위젯은 메모리에서 해제되어야한다. 하지만 위젯 제거 과정이 비동기 작업 사이에 발생한다면 비동기 작업은 여전히 사라진 context를 참조하게 되면서 메모리 누수가 발생할 수도 있다. 앞선 예시처럼 런타임 크래쉬가 발생하면 그나마 알 수 있지만 만약 보이지 않게 메모리 누수를 발생시키고 있다면 찾기 쉽지 않을 것이다.
문제 해결
문제 해결방법은 크게 3가지가 있다.
- Navigator Key (혹은 Global Key)
- then 메서드
- mounted 체크 (공식문서 추천)
1. Navigator key (or Global Key)
Navigator key (or Global Key)는 위젯 트리 내에서 특정 위젯을 고유하게 식별한다. (key를 통한 위젯 식별원리는 이곳에 정리)
참조하려는 위젯에 Key를 선언하여 비동기가 쓰이는 곳에서 키를 통해 context에 접근하는 방법이다. 위 비동기 함수에서 currentState != null 임을 확인하고 작업을 진행하고 있다. 이렇게 참조 상태를 Key로 확인하기 때문에 참조로 인한 크래쉬와 메모리누수를 방지할 수 있다. 개인적으로 만약 프로젝트 룰을 Key사용으로 정하면 특정 파일에 Key를 모두 저장해서 Apps_Key 형태로 사용하면 좋겠다는 생각이 들었다.
2. Then method
then 을 이용한 방법은 Closure 함수의 캡쳐를 이용한 방법이다. 클로저는 함수가 정의 될 때 함수 스코프에 있는 변수를 "캡쳐"해서 나중에 사용한다. 이러한 특성 덕분에, 함수가 실행되는 시점에 원본 스코프의 변수를 참조할 수 있다. 위 예제에서 ScaffoldMessenger는 context 참조를 "캡쳐"하게 된다. 함수가 실행되기 전에 원본 컨텍스트를 캡쳐함으로써 비동기 함수가 끝난 이후 원본 컨텍스트를 참조하며 유효성 판단을 하게 된다.
(24.08.02 수정)
then 메서드를 사용하더라도 함수 내부에서 mount 조건을 걸거나 try-catch로 적어주어야한다. 원본 context참조를 캡쳐하면 변경 사항은 반영하지만 만약 컨텍스트가 아예 사라진 경우에는 크래쉬가 나기 때문이다.
3. chek mounted
mounted 속성은 Flutter에서 상태(State) 객체가 트리에 연결되어 있는지 여부를 나타내는 Bool 값이다. 위젯이 트리에 추가되었을 때 mounted는 true가 되며, 위젯이 트리에서 제거될 때 mounted는 false가 된다. 즉 mounted는 State 객체의 속성 중 하나로, 해당 위젯이 트리에 제대로 마운트되어(올라와)있는지 여부를 나타낸다. mounted는 위젯이 생명주기 동안 트리 내에 존재하고 있는지를 확인하는 데 유용한 속성이라고 볼 수 있다. (하지만 Stateful 위젯에서만 쓸 수 있다는게 단점)
mounted 여부를 통해 위젯트리에 context 존재 여부를 확인할 수 있다. mounted 는 위젯 생명주기에 따라 아래와 같이 값이 변한다.
이러한 mounted 값 변화를 통해 위젯 생명주기에 따라 비동기 함수안에서 context 이용을 안전하게 할 수 있다.
'Flutter' 카테고리의 다른 글
Flutter 3.24 업데이트! 이제 shared preference 사용해도 됩니다! (0) | 2024.08.07 |
---|---|
Freezed 패키지 사용이유 알아보기 (0) | 2024.08.03 |
Flutter로 SwiftUI 처럼 모서리 둥글게하기 (feat. Squircle) (0) | 2024.07.27 |
Shared Preference 패키지를 쓰지말아야하는 이유 (0) | 2024.07.25 |
flutter 네비게이션바와 버튼의 물결효과 없애기 (feat. inkwell widget) (0) | 2024.07.24 |