앱 시작 씬 하나가 설정 읽기, SDK 초기화, 자동 로그인, 자산 로드, 메인 화면 준비를 모두 담당했다. 어떤 작업이 실패해도 같은 로딩 화면에서 멈췄고 다시 시도하면 이미 만들어진 전역 객체가 중복됐다. 로딩 시간을 줄이는 것보다 먼저 책임과 수명을 나눌 필요가 있었다.
씬을 파일 단위로 쪼개는 것만으로는 해결되지 않았다. 각 단계가 무엇을 보장하고 다음 단계에 무엇을 넘기는지 정했다.
부트 단계는 최소 환경만 준비했다
첫 씬은 로그, 공개 설정, 언어, 오류 화면처럼 이후 모든 단계가 의존하는 작은 기반만 만들었다.
Boot Scene : 설정·로그·기본 자산
Login Scene : SDK·세션·계정 선택
Main Scene : 업무 데이터·기능 화면
메인 기능의 무거운 자산은 인증 결과를 알기 전에 불러오지 않았다.
씬 사이에 전역 객체를 남발하지 않았다
DontDestroyOnLoad 객체가 늘어나면 이전 씬의 상태가 숨어 남았다. 앱 수명 서비스만 명확한 컨테이너에서 유지하고 화면 전용 객체는 씬 종료와 함께 정리했다.
public record LoginResult(Session Session, UserContext User);
await sceneFlow.MoveToMainAsync(loginResult);
다음 씬이 필요한 값을 전환 결과로 받게 해 임의의 싱글턴 조회를 줄였다.
실패와 되돌아갈 위치를 정했다
설정 로드 실패는 부트 오류 화면, 인증 만료는 로그인, 메인 데이터 실패는 재시도 가능한 메인 로딩으로 보냈다. 모든 오류가 앱 재시작을 요구하지 않았다.
로그아웃하면 Main 자원과 사용자 범위 서비스를 정리한 뒤 Login으로 돌아갔다. Boot까지 다시 실행할 필요가 있는 변경은 별도로 구분했다.
전환 중 앱이 백그라운드로 가거나 씬 로드가 취소되는 경우도 처리했다. 새 씬이 준비되기 전에 이전 씬을 먼저 없애면 오류 화면조차 표시할 수 없었다. 준비 완료를 확인한 뒤 활성 씬을 바꾸고 실패하면 이전 안전 화면을 유지했다.
진행률은 각 작업의 개수만 단순 합산하지 않고 사용자가 기다리는 핵심 단계에 맞춰 표시했다. 99%에서 오래 멈추는 가짜 진행률보다 현재 준비 중인 단계를 설명하는 편이 나았다.
화면 분리는 생명주기 분리였다
씬을 나누면 코드가 자동으로 깔끔해질 거라 생각하기 쉽다. 실제 효과는 각 단계의 입력·출력·정리 조건을 정했을 때 나왔다.
이 작업 뒤 로딩 문제를 스피너와 진행률 문제로만 보지 않는다. 무엇이 어느 단계에 살아 있어야 하는지, 실패하면 어디까지 되돌릴지를 설계한다. 로딩은 앱 구조가 사용자에게 드러나는 과정이었다.