# 현실 세계에서 제대로 작동하는 Flutter 클라이언트: 복잡한 시나리오를 위한 세 가지 솔루션
이론상으로는 Flutter 앱의 아키텍처가 완벽해 보입니다: 상태 관리에 Bloc, API에 Dio, 네비게이션에 go_router. 하지만 실제 프로젝트에서는 교과서 예제 너머의 문제들이 발생합니다—사용자 전환부터 백그라운드 작업까지. 행복한 경로(happy path)에서 벗어나는 첫 번째 상황에서 무너지지 않는 클라이언트를 어떻게 구축할까요?
다양한 복잡한 기능을 가진 완전한 언어 학습 앱을 생각해 보세요: 다중 제공자 인증, 익명 로그인, 번역, 음성 인식, 콘텐츠 생성, 오프라인 모드, WebSocket 연결 등. 표준 패키지(flutter_bloc, dio, get_it)는 개별 작업을 해결하지만, 제품 로직이 단순 예제를 넘어서는 시나리오는 커버하지 못합니다. 주요 문제는 다음과 같습니다:
- 익명 사용자를 영구 계정에 연결하면서 데이터 손실 없이 처리하기
- 명목상 사용 가능하지만 불안정한 네트워크에서 작업하기
- UI를 차단하지 않고 수십 초 걸리는 장기 작업 처리하기
- 로그아웃/로그인 시 적절한 ID 전환
- 사용자 로그아웃 후 보장된 상태 정리
이런 경우들은 새로운 패키지만으로는 부족하고, 근본적인 아키텍처 솔루션이 필요합니다. 견고한 클라이언트를 만들어내는 데 도움이 된 세 가지 접근 방식을 소개합니다.
ID와 앱 액세스의 분리
표준 접근법—Firebase Auth를 유일한 진실의 원천(single source of truth)으로 사용. 하지만 복잡한 비즈니스 로직에서는 문제가 발생합니다. 예를 들어, 익명 로그인에서 Google 등록으로 전환하면 데이터가 손실되고, 서버 액세스 규칙이 Firebase에 강하게 결합됩니다.
우리는 책임을 분리했습니다:
- Firebase가 ID를 처리 (이메일/비밀번호, Google, Apple, 익명 로그인)
- 백엔드가 ID 확인 후 자체 JWT 토큰 생성
워크플로:
- 사용자가 Firebase를 통해 로그인
- 클라이언트가 Firebase ID 토큰 획득
- 이를 백엔드로 전송
- 백엔드가 앱 JWT 반환
- 이후 모든 요청(REST와 WebSocket)이 이 JWT 사용
이렇게 하면 세 가지 핵심 문제를 해결합니다:
- 제공자 독립성: Firebase를 다른 ID 제공자로 전환해도 백엔드 로직에 영향 없음
- 전송 통합: 하나의 토큰으로 HTTP, WebSocket, 권한 관리 모두 처리
- 부드러운 계정 연결: 익명 사용자가 영구 계정으로 업그레이드할 때 데이터 유지
여기서 치명적인 실수—Firebase 토큰을 API 액세스에 직접 사용하려는 것. 백엔드는 자체 권한과 세션 수명 주기를 관리해야 합니다. 2단계 인증은 복잡성을 더하지만, 유연성과 실전 적용성에서 보상받습니다.
수동 정리 대신 전체 컨텍스트 재설정
로그아웃 후 상태를 논리적으로 정리하려는 시도(TokenCubit, UserCubit, 캐시를 0으로 초기화)는 필연적으로 누출을 초래합니다. 특히 이런 시나리오에서:
- 사용자 전환 (로그아웃 → 다른 계정 로그인)
- 익명 프로필을 등록된 계정에 연결
- 만료된 토큰 후 세션 복구
- 깊게 중첩된 화면에서 돌아오기
부분 청소 대신 전체 제공자 컨텍스트를 재생성합니다. 기술적 구현:
- 루트 위젯 AppInitializer에서 고유 키 생성
- 중요한 인증 상태 변경(로그아웃, 사용자 전환) 시 키 변경
- MultiBlocProvider를 포함한 하위 트리가 처음부터 재빌드
- 모든 Cubit이 GetIt의 깨끗한 상태로 초기화
final GlobalKey<_AppInitializerState> appContextKey =
GlobalKey<_AppInitializerState>();
class _AppInitializerState extends State<AppInitializer> {
Key _appKey = UniqueKey();
void resetApp() {
setState(() {
_appKey = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
key: _appKey,
providers: ProvidersManager.getAllProviders(),
child: widget.child,
);
}
}
이 방법은 우아하지 않지만 예측 가능합니다. 재설정 후 앱은 새로운 세션 인스턴스처럼 동작합니다. 중요한 점:
- 장기 생존 서비스(예: WebSocket 연결)를 화면 상태와 명확히 분리
- DI를 구성해 싱글톤이 재생성에 영향받지 않도록
- 간단한 앱(5개 미만 화면)에는 사용하지 말기
복잡한 프로젝트에서는 끝없는 수동 정리보다 전체 컨텍스트 재설정이 더 저렴하고 신뢰할 수 있습니다.
장기 작업을 위한 하이브리드 HTTP + WebSocket
표준 패턴 final response = await dio.post(...)는 텍스트 분석, 콘텐츠 생성, 인식처럼 수십 초 걸리는 작업에서 깨집니다. 문제점:
- HTTP 요청 타임아웃
- 앱이 백그라운드일 때 불안정
- 긴 대기 중 UX 저하
- 오류 진단의 복잡성
솔루션—하이브리드 모델로 전환:
- 클라이언트가
X-Async-Background: true헤더와 함께 요청 전송 - 서버가 최종 결과 대신
task_id반환 - 클라이언트가 WebSocket으로 완료 이벤트 구독
- 알림 후 별도 REST 요청으로 결과 가져오기
주요 장점:
- 진화 가능성: 빠른 엔드포인트가 백그라운드로 바뀌어도 클라이언트 재작성 불필요
- 신뢰성: WebSocket이 완료 신호, REST가 결과 전달 처리
- 진단: 추적, 재시도, 오류 처리의 별도 지점
모바일 클라이언트에서는 다음을 반드시 구현:
- 자동 재연결
- 하트비트 메커니즘
- 메시지 큐
- 앱 생명 주기 이벤트 처리
- 상태, 데이터, 오류를 위한 별도 스트림
이렇게 WebSocket을 단순 "알림 채널"에서 핵심 인프라 레이어로 승격시킵니다. X-Async-Background 헤더는 클라이언트 핵이 아니라 준비된 백엔드 솔루션의 인터페이스 역할을 합니다.
핵심 요점: 중요한 takeaways
- ID와 액세스 분리: Firebase/Apple/Google은 ID 확인하지만, 리소스 액세스 권한은 관리하지 않음
- 상태 정리 대신 컨텍스트 재설정: 사용자 전환 시 제공자 트리를 재생성
- 진화를 고려한 설계: 장기 작업을 HTTP(시작) → WebSocket(신호) → HTTP(결과)로 구성
- 행복 경로 너머 테스트: 익명 로그인 → 등록, 고핑 네트워크, 깊은 네비 스택에서의 사용자 전환
- 인프라 레이어 격리: WebSocket을 시스템 구성 요소로, 예외가 아닌 것으로
이 솔루션들은 이론적 선호가 아니라 실제 실패에서 나왔습니다. Flutter는 도구를 제공하지만, 앱의 탄력성은 컴포넌트 경계를 어떻게 처리하느냐에 달려 있습니다. 문제가 발생하는 시나리오에 집중하는 것이 프로덕션 준비 클라이언트를 만드는 유일한 길입니다.
— Editorial Team
아직 댓글이 없습니다.