Launch Lab
이벤트에 맞춰 바뀌는 앱의 첫 화면을 만들며, 이미지 준비와 실패 처리의 기준을 정하는 React Native 개발 기록입니다.
목차
카페 앱을 주로 만들다 보니 시즌이나 행사에 따라 바뀌는 첫 화면에도 관심이 갔다. 앱을 켰는데 어제와 다른 이벤트 이미지가 나오는 식이다. 이미지 주소를 서버에서 받으면 바꾸는 일 자체는 어렵지 않을 것 같았다.
그런데 첫 화면에 쓸 이미지라면 언제 받아야 할까. 앱을 켠 뒤에 받으면 새 이벤트를 바로 보여줄 수 있지만, 이미지를 받는 동안 사용자는 기다려야 한다. 미리 받아 둔 이미지를 쓰면 처음 설치한 사람에게는 무엇을 보여줘야 할지도 정해야 했다.
작은 RN 앱을 하나 만들고 두 방식을 같은 기기에서 비교해 보기로 했다. 이름은 Launch Lab으로 붙였다.
바꿀 화면부터 정했다
이번에는 네이티브 시작 화면의 로고를 고정하고, 그 뒤에 RN으로 그리는 이벤트 화면을 뒀다. 서버에서 받은 이미지가 바뀌는 곳은 이 두 번째 화면이다. 다른 서비스가 내부적으로 어떻게 구현했는지를 확인한 것은 아니다.
Expo의 SplashScreen 설정에서 이미지나 배경 같은 설정은 새 앱 바이너리를 빌드해야 반영되는 영역이다. 이번에 바꾸려는 이벤트 이미지는 앱이 실행된 뒤 받아서 표시하도록 분리했다. 같은 문서에서 시작 화면은 릴리스 빌드로 확인할 것을 권장하므로 Expo Go 대신 APK를 만들었다.
앱에는 이벤트 화면과 홈 화면만 넣었다. 서버는 이벤트 ID, 버전, 이미지 주소, 시작·종료 시각을 내려준다. 가을 커피 이미지 A와 저녁 커피 이미지 B 두 장을 만들어서 서버가 내려주는 이벤트를 바꿀 수 있게 했다. 관리자 화면까지 만들 필요는 없었다.
이미지 파일은 로컬에 저장하고, 어떤 이벤트의 파일인지 알려주는 정보는 AsyncStorage에 뒀다. 확인에 쓴 기기는 Android 15와 One UI 7이 설치된 갤럭시 Z Flip3다. USB로 Mac의 실험 서버에 연결했으며, iOS는 아직 확인하지 않았다.
같은 이미지를 다른 시점에 준비했다
첫 번째 방식은 앱을 켤 때마다 서버에서 새 이미지를 준비한 뒤 표시한다. 코드에서는 network-first라고 이름 붙였다.
두 번째인 cached-first는 실행 초기에 저장된 이미지부터 고른다. 새 이벤트 조회와 다운로드도 시작하지만, 그 결과를 기다리며 화면을 붙잡지는 않는다. 갱신에 성공한 이미지는 다음 실행에서 쓴다.
이번 실행에 보여줄 이미지를 고르는 부분은 아래와 같다. 실제 beginLaunch 함수에서 발췌했다. cachedDecision에는 조회를 시작하기 전에 고른 캐시 또는 기본 화면이 들어 있다.
캐시 방식에서도 조회는 한다. 차이는 새 결과를 지금 보여줄 이미지 선택에 사용할지다. 다운로드가 끝났다는 이유로 이미 표시 중인 A를 B로 바꾸면 한 번의 시작 화면에서 이벤트가 교체될 수 있어서, 선택 결과를 현재 실행 동안 유지했다.
비교할 때는 이미지의 onLoad 이후 1초를 기다리고 홈으로 넘겼다. 기본 화면도 1초를 표시했다. 두 방식의 이미지 표시 시간을 맞추려고 둔 값이지, 이벤트를 꼭 1초 보여줘야 한다는 제품 요구는 아니다. 건너뛰기 버튼도 넣었지만 이 비교에서는 누르지 않았다.
서버를 B로 바꾸고 다시 열었다
캐시를 지운 첫 실행에서는 기본 화면이 나왔다. 그 실행에서 A를 내려받아 저장한 뒤 앱을 완전히 종료하고 다시 열자 A가 표시됐다.
그다음 서버의 이벤트를 B로 바꿨다. 앱에는 아직 A가 저장돼 있었다. 캐시 방식으로 열었더니 이번에도 A가 나왔고, 그 사이 B를 내려받았다. 실행 기록을 시간순으로 보면 이렇다.
| JS 관찰 시작 후 | 일어난 일 |
|---|---|
| 15ms | 이번 실행의 이미지로 A 선택 |
| 71ms | A 이미지의 onLoad 호출 |
| 998ms | 새로 받은 B 저장 완료 |
| 1127ms | A를 표시한 실행에서 홈 layout 확인 |
B 저장이 끝났어도 이번 실행의 선택은 A였다. 한 번 더 종료하고 열었을 때 B가 표시됐다. 서버에서 이벤트를 바꾸는 시점과 사용자가 새 이미지를 보는 시점이 한 번의 실행만큼 벌어졌다.
운영할 때도 이 차이를 설명해야겠다고 생각했다. 캐시 방식을 택하면 이벤트를 바꾼 직후에도 이전 이미지를 보는 사람이 있을 수 있다. 대신 그 실행에서는 새 이미지를 내려받을 때까지 기다리지 않는다.
기다리는 시간은 어디로 갔을까
이미지 B와 유효한 B 캐시가 있는 상태에서도 두 방식을 비교했다. 서버의 이벤트 정보 응답에는 700ms, 이미지 응답에는 200ms 지연을 넣었다. 빠른 로컬 연결에서도 기다리는 구간이 분명히 드러나도록 한 조건이다.
| 방식 | 이미지 선택 | 홈 layout |
|---|---|---|
| 저장한 이미지 먼저 | 15ms | 1137ms |
| 실행 중 새 이미지 준비 | 1005ms | 2109ms |
시간의 기준은 앱 안에 둔 JS 관찰 코드의 시작점이다. 앱 아이콘을 누른 순간부터 잰 시간이 아니고, home-layout도 화면이 입력을 받을 수 있다는 뜻은 아니다. 각 조건을 한 번씩 관찰한 값이라 성능 개선율로 계산하지 않았다. 기기 방향이나 온도를 고정한 벤치마크도 아니다.
넣어 둔 네트워크 지연을 어느 쪽에서 기다리는지 보고 싶었다. 실행 중 다운로드 방식은 새 파일이 준비될 때까지 이미지 선택을 미뤘다. 캐시 방식은 이전 파일로 화면을 진행했고, 새 파일을 준비하는 일은 함께 진행됐다.
최신 이미지가 반드시 이번 실행에 나와야 한다면 첫 번째 방식이 맞을 수 있다. 이번 실험에서는 홈 진입을 이미지 다운로드에 묶지 않는 쪽을 기본 후보로 두기로 했다. 첫 설치에서는 기본 화면이 나와도 된다는 조건이 따라붙는다.
기기에서 보니 문구가 잘렸다
이미지 선택을 확인하고 실제 화면도 캡처했다. Flip3처럼 세로로 긴 화면에 이미지를 cover로 채우니 좌우 문구가 잘렸다. 두 장 모두 같은 크기의 이미지였지만, 화면을 빈틈없이 채우는 것과 문구를 전부 보여주는 것은 달랐다.


우선 contain으로 바꿨다. 위아래 여백은 생겼지만 준비한 이미지 전체가 보였다. 실제 서비스라면 이미지 제작 규격이나 문구의 안전 영역도 함께 정할 문제다. 이번에는 이미지가 선택되고 표시되는 흐름을 확인하는 데 필요한 만큼만 수정했다.
정상 응답에서는 두 방식의 차이가 드러났다. 다음으로는 새 이미지를 못 받는 경우를 확인했다. 저장한 그림이 있다는 이유만으로 계속 보여줘도 되는지, 응답을 언제까지 기다릴지가 남았다. 다음 글에서는 이미지 요청을 일부러 실패시켜 봤다.
