ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 화면 밖으로 나간 Flutter 상태가 언제 살아남는지 정해야 합니다
    AI Agent/flutter 2026. 7. 4. 21:00
    728x90
    반응형

    출근길 지하철에서 하단 탭바에 엄지를 얹은 손
    탭을 오갈 때마다 맨 위로 튀던 스크롤

    검색 결과를 한참 내리다가 괜찮은 항목 하나를 찾았습니다.
    상세를 확인하려고 옆 탭을 잠깐 열었다가 돌아왔는데, 리스트가 맨 꼭대기로 되돌아가 있었습니다.
    스크롤 위치만 날아간 게 아니라 방금 걸어둔 정렬 조건까지 초기값으로 풀려 있었습니다.
    처음엔 제가 뒤로가기를 잘못 눌렀나 싶어 같은 동작을 세 번쯤 반복했습니다.
    매번 똑같이 맨 위였습니다.

    문제의 화면은 TabBarView 안에 들어 있었습니다.
    탭을 벗어나는 순간 그 안의 위젯이 통째로 버려지고, 돌아올 때 새로 만들어지고 있었던 겁니다.
    저는 상태가 당연히 남아 있을 거라 기대했는데, 정작 남으라고 지시한 사람은 아무도 없었습니다.

    탭 밖으로 나가면 위젯은 기본값으로 버려집니다

    TabBarView나 PageView는 눈에 보이지 않는 자식을 오래 붙들고 있지 않습니다.
    메모리를 아끼려는 합리적인 기본 동작입니다.
    문제는 이 기본값이 "스크롤 위치를 기억해줘" 같은 사용자의 기대와 정반대라는 데 있습니다.
    사용자는 방금 있던 자리를 당연히 기억할 거라 믿고, 프레임워크는 안 보이면 정리하는 게 당연하다고 믿습니다.
    그 둘이 처음으로 부딪힌 지점이 제가 본 화면이었습니다.

    살리는 방법 자체는 어렵지 않습니다.
    AutomaticKeepAliveClientMixin을 붙이고 wantKeepAlive를 true로 돌려주면 됩니다.

    class _SearchTabState extends State<SearchTab>
        with AutomaticKeepAliveClientMixin {
      @override
      bool get wantKeepAlive => true;
    
      @override
      Widget build(BuildContext context) {
        super.build(context); // 이 줄을 빼면 mixin이 조용히 무시됩니다
        return SearchResultList(controller: _scrollController);
      }
    }
    

    사실 여기까지 오기 전에 엉뚱한 데를 한참 팠습니다.
    처음엔 ScrollController를 의심해서, 컨트롤러를 State 바깥에 붙들어두면 위치가 남지 않을까 시도했습니다.
    컨트롤러는 살아 있는데 그 컨트롤러가 붙어 있던 리스트 위젯 자체가 사라지니, 남은 건 주인 잃은 값뿐이었습니다.
    살려야 하는 건 값을 담은 그릇이 아니라 그 그릇을 들고 있던 위젯이라는 걸, 컨트롤러를 붙잡고 삼십 분쯤 헤맨 뒤에야 알았습니다.

    그 뒤에 mixin을 넣고 탭을 오갔더니 스크롤이 그대로 있었습니다.
    그때는 다 끝난 줄 알았습니다.

    그런데 다 살려두니 이번엔 안 죽는 게 문제였습니다

    한 탭이 잘 되니 나머지 탭에도 같은 mixin을 그대로 붙였습니다.
    며칠 뒤에 로그아웃 버튼을 눌렀는데, 로그인 화면으로 넘어가기 직전에 탭 뒤편 목록 맨 위에 방금 로그아웃한 계정의 프로필 이름이 반쯤 잘린 채 그대로 떠 있었습니다.
    스크린샷을 찍어서 다시 봤는데도 그 자리에 있었습니다.

    처음엔 캐시가 안 지워진 줄 알고 로컬 저장소를 통째로 비웠습니다.
    그대로였습니다.
    그다음엔 목록 데이터를 다시 불러오는 provider를 invalidate 했는데, 새 데이터가 위에 덮이긴 해도 한 박자 늦게 깜빡이면서 옛날 이름이 먼저 스쳤습니다.
    hot restart를 하면 깨끗하게 지워지니까 더 헷갈렸습니다.
    앱을 껐다 켜면 멀쩡한데, 로그아웃만으로는 안 죽었습니다.

    한참 헤매다 문득 이상했습니다.
    죽어야 할 화면이 왜 이렇게 끈질기게 붙어 있지.
    그러다 며칠 전에 제가 모든 탭에 "살아남아라"라고 못을 박아둔 게 떠올랐습니다.
    스크롤 하나 살리자고 켠 스위치였는데, 그게 로그아웃해도 안 죽는 화면까지 같이 만들어놓은 거였습니다.
    저는 그동안 "어떻게 살리지"만 붙잡고 있었지, 어떤 화면은 오히려 "제때 죽어야 한다"는 건 한 번도 생각을 안 했습니다.

    스크롤 위치와 로그인 상태는 수명이 다릅니다

    한 화면 안에도 수명이 제각각인 상태가 섞여 있었습니다.
    스크롤 위치는 탭을 오가는 몇 초만 버티면 됩니다.
    검색 필터는 그 세션 동안 살아 있어야 합니다.
    로그인 정보는 앱을 다시 켜도 남아야 하고, 반대로 로그아웃하는 순간엔 즉시 사라져야 합니다.
    이걸 전부 위젯 하나에 wantKeepAlive로 묶으면, 가장 오래 사는 것과 가장 빨리 죽어야 하는 것이 같은 수명을 갖게 됩니다.
    제가 만든 버그가 정확히 그 모양이었습니다.

    그래서 지금은 상태마다 사는 곳을 나눕니다.
    스크롤처럼 화면에 딱 붙은 것만 KeepAlive나 PageStorageKey에 맡기고, 필터나 로그인처럼 화면보다 오래 사는 값은 위젯 바깥의 provider로 끌어올립니다.
    이렇게 나눠두면 로그아웃할 때 provider 하나만 비우면 되고, 탭 위젯이 살아 있든 버려졌든 따로 신경 쓸 일이 없어집니다.
    mixin은 스크롤 하나만 붙들고, 나머지 무거운 결정은 화면 밖에서 처리하는 구조가 됩니다.

    로그아웃 문제가 특히 그랬습니다.
    예전에는 탭을 죽여서 이전 계정의 흔적을 지우려 했는데, 그건 사용자 눈에 보이는 위젯을 붙잡고 데이터 문제를 풀려는 거였습니다.
    지금은 로그인 상태가 provider에 있으니, 로그아웃 한 번에 그 값이 지워지고 목록도 자연스럽게 빈 화면으로 돌아갑니다.
    탭이 살아 있어도 참조할 데이터가 사라졌으니 옛날 계정이 남을 자리가 없습니다.
    위젯의 생사와 데이터의 생사를 떼어놓고 나서야 이 두 버그가 같은 뿌리였다는 게 보였습니다.

    지금도 새 탭 화면을 만들 때는 코드를 짜기 전에 잠깐 멈춥니다.
    이 화면의 스크롤은 얼마나 살아야 하고, 여기 걸린 값은 로그아웃하면 같이 사라져야 하나.
    답이 화면마다 달라서, 아직도 탭을 하나 추가할 때마다 손이 그 질문 앞에서 한 번씩 멎습니다.

    원문 기준

    원문 기준: https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html

    원문 본 날짜: 2026-07-01.

    728x90
    반응형
Designed by Tistory.