-
Flutter 라우팅은 화면 이동이 아니라 상태와 권한의 계약입니다AI Agent/flutter 2026. 7. 5. 21:00728x90반응형

알림을 눌렀는데 빈 화면이 뜬다는 문의 푸시 알림을 누르고 들어온 사용자가 로그인도 안 한 채로 상세 화면 앞에 서 있었습니다.
정확히는 상세 화면의 빈 껍데기였습니다.
제목도 없고, 내용도 없고, 로딩 스피너만 하염없이 돌았습니다.
로그인 가드가 분명히 있는데, 이 사용자는 그 문을 통째로 지나쳐 안쪽에 도착해 있었습니다.이 화면을 제가 직접 본 것도 아니었습니다.
"알림 눌렀는데 아무것도 안 떠요"라는 문의에 짧은 화면 녹화 하나가 붙어 왔고, 스피너가 한참을 도는 동안 사용자는 결국 앱을 닫았습니다.
재현하려고 제 폰에서 같은 알림을 눌러봤는데, 저는 이미 로그인이 되어 있어서 멀쩡히 상세가 떴습니다.
로그인을 지우고 앱을 완전히 종료한 뒤에야 그 빈 껍데기가 제 눈앞에도 나타났습니다.알림에는 딥링크가 걸려 있었습니다.
앱이 꺼진 상태에서 딥링크로 진입하면, 평소 로그인 화면을 거쳐 들어오던 순서가 통째로 건너뛰어졌습니다.
저는 라우팅을 화면과 화면 사이를 잇는 길이라고만 생각했는데, 이 버그는 그게 아니라 "누가 어디에 들어올 자격이 있는가"를 정하는 계약에 가깝다는 걸 보여줬습니다.딥링크는 가드를 건너뛰고 들어옵니다
일반적인 화면 이동이라면 사용자는 로그인 화면을 거쳐, 홈을 거쳐, 상세로 옵니다.
그 길목마다 로그인 여부를 확인할 기회가 있습니다.
그런데 딥링크는 그 길목을 전부 무시하고 목적지에 바로 꽂힙니다.
중간에 있던 확인 지점들이 애초에 실행되질 않는 겁니다.그래서 go_router에는 redirect가 있습니다.
어느 경로로 들어오든 화면을 그리기 전에 한 번 불려서, 여기 들어와도 되는지 판단하고 아니면 다른 곳으로 돌려보내는 관문입니다.
저도 이걸 넣어뒀습니다.
넣어뒀는데도 뚫렸습니다.제보를 받고 처음 한 건 딥링크 파싱 의심이었습니다.
알림에 실린 URL이 이상하게 들어오나 싶어 링크를 로그로 찍고 파라미터를 하나씩 뜯어봤습니다.
링크는 멀쩡했고, 정확히 제가 의도한 경로가 그대로 들어오고 있었습니다.
한 시간 가까이 엉뚱한 데를 판 셈입니다.그다음 redirect 안에 로그를 심었습니다.
거기서 두 가지가 한꺼번에 보였습니다.
redirect는 분명히 호출되고 있었고, 그런데 그 순간 찍힌 isSignedIn 값이 false였습니다.
이상한 건 저장소에는 로그인 토큰이 멀쩡히 남아 있었다는 점입니다.
토큰은 있는데 로그인 상태는 false.
관문이 없는 게 아니라, 관문이 아직 눈을 못 뜬 사이에 사용자가 지나간 거였습니다.redirect는 상태가 준비되기 전에 한 번 돕니다
토큰을 저장소에서 읽어오는 건 비동기라, 앱이 켜지자마자 도는 redirect보다 한 박자 늦게 끝납니다.
그 한 박자 사이에 제 코드는 "아직 모르는" 상태와 "로그인 안 된" 상태를 똑같이 취급하고 있었습니다.
모른다고 답해야 할 자리에서 아니라고 답한 셈입니다.게다가 뒤늦게 토큰 로드가 끝나 로그인 상태가 확정돼도, 라우터는 그 변화를 몰라서 redirect를 다시 돌지 않았습니다.
한 번 잘못 통과시킨 판단이 그대로 굳었습니다.
빈 껍데기 화면은 그렇게 남은 거였습니다.라우터가 auth 상태를 구독하게 만듭니다
고치는 방향은 두 가지였습니다.
아직 상태를 모르는 동안에는 판단을 미루게 하고, 상태가 바뀌면 라우터가 그걸 알아채고 redirect를 다시 돌게 하는 것.
go_router에서는 refreshListenable로 상태를 물려주면 됩니다.final router = GoRouter( refreshListenable: authState, // 상태가 바뀌면 redirect를 다시 태운다 redirect: (context, state) { if (authState.isLoading) return null; // 아직 모른다: 판단을 미룬다 final signedIn = authState.isSignedIn; final atLogin = state.matchedLocation == '/login'; if (!signedIn && !atLogin) return '/login?from=${state.uri}'; if (signedIn && atLogin) return '/home'; return null; }, );핵심은 isLoading일 때 null을 돌려주는 한 줄이었습니다.
모를 땐 아무 데도 보내지 않고 잠깐 기다리게 두는 겁니다.
토큰 로드가 끝나면 authState가 알림을 보내고, refreshListenable이 그걸 받아 redirect를 다시 돌립니다.
그제야 로그인 안 한 사용자는 제대로 로그인 화면으로 튕겨 나갔고, from 파라미터에 원래 가려던 딥링크 목적지를 실어 보내 로그인 뒤엔 그 자리로 돌아오게 했습니다.다만 isLoading 동안 null을 돌려주면 그 짧은 순간 목적지 화면이 반짝 비칠 수 있어서, 로그인 상태가 확정되기 전까지는 중립 화면 하나를 걸어 그 깜빡임을 가렸습니다.
지금은 새 화면을 라우터에 등록할 때, 이 경로가 딥링크로 곧장 열렸을 때 무슨 일이 벌어지는지를 먼저 그려봅니다.
로그인 안 한 사람이 여기 꽂히면, 로드 중에 꽂히면, 권한 없는 사람이 꽂히면.
그 세 장면을 머릿속에서 통과시켜보지 않으면, 저는 또 어딘가에서 하염없이 돌아가는 스피너 앞에 선 사용자를 마주하게 됩니다.원문 기준
원문 기준: https://docs.flutter.dev/ui/navigation
원문 본 날짜: 2026-07-01.
728x90반응형'AI Agent > flutter' 카테고리의 다른 글
Riverpod 3에서 await 중 ref가 죽는 순간 (0) 2026.07.06 로컬 상태는 서버 상태의 대체물이 아니라 임시 복사본입니다 (0) 2026.07.06 SafeArea는 항상 안전하지 않습니다 (0) 2026.07.05 Positioned 애니메이션이 프레임을 태우는 이유 (0) 2026.07.05 화면 밖으로 나간 Flutter 상태가 언제 살아남는지 정해야 합니다 (0) 2026.07.04