Launch Lab
이벤트에 맞춰 바뀌는 앱의 첫 화면을 만들며, 이미지 준비와 실패 처리의 기준을 정하는 React Native 개발 기록입니다.
목차
지난 글에서는 앱이 실행될 때 새 이벤트 이미지를 받는 방식과, 저장한 이미지를 먼저 보여주는 방식을 비교했다. 캐시 방식은 이번 실행의 그림을 유지하면서 다음 실행에 쓸 이미지를 준비했다.
정상 응답만 보면 흐름은 단순했다. 새 이미지가 있으면 저장하고, 다음에 열 때 보여주면 된다. 하지만 이벤트 서버가 응답하지 않거나 이미지 파일만 내려오지 않는다면 어디까지 기다려야 할까. 지난 이벤트가 저장돼 있다고 해서 무조건 다시 보여줄 수도 없었다.
실험 서버에서 응답을 늦추거나 실패시키고, 저장한 이벤트의 기간도 끝나게 해 봤다. 이번에도 Android 15가 설치된 Flip3의 릴리스 앱을 사용했다.
새 이미지를 못 받았을 때
실패했을 때 고를 화면을 먼저 정했다. 파일이 남아 있고 표시 기간도 지나지 않았다면 그 이미지를 쓴다. 쓸 수 있는 이미지가 없으면 기본 화면을 보여준 뒤 홈으로 넘긴다.
첫 설치라 캐시가 없는 경우도 이 규칙으로 처리했다. 이벤트 이미지 하나를 받지 못했다는 이유로 앱 진입에 재시도 버튼을 누르게 하고 싶지는 않았다. 여기서 다루는 것은 이벤트 이미지다. 로그인이나 주문처럼 반드시 성공해야 하는 요청까지 같은 방식으로 건너뛰겠다는 의미는 아니다.
기기의 Wi-Fi나 셀룰러 설정을 바꾸는 대신 실험 서버가 HTTP 503을 반환하도록 했다. 이벤트 정보를 받는 요청과 실제 이미지 파일을 받는 요청은 각각 실패시켰다.
| 만든 조건 | 앱이 고른 화면 |
|---|---|
| 이벤트 정보 요청 실패, 유효한 B 캐시 있음 | 저장한 B |
| 이벤트 정보 요청 실패, 캐시 없음 | 기본 화면 |
| 이벤트 정보는 받았지만 이미지 요청 실패 | 기본 화면 |
| 저장한 A가 만료된 상태에서 요청 실패 | 기본 화면 |
이미지 요청을 실패시킨 실행에서는 먼저 이벤트 정보 응답을 받았다. 주소를 아는 것과 표시할 파일을 확보한 것은 달랐다. 이미지 다운로드가 실패하자 새 이벤트를 저장하지 않고 기본 화면으로 이어졌다.
현재 코드는 파일을 내려받은 뒤 로컬 파일에서 이미지 크기를 읽을 수 있는지 확인하고, 그다음 이벤트 정보를 저장한다. Image.getSize가 성공했다고 화면에 완전히 표시된 것은 아니어서 실제 뷰의 onLoad와 onError도 따로 처리했다. 다만 이번 기기 실험에서 직접 만든 이미지 실패는 HTTP 503이다. 손상된 파일을 넣어 본 결과로 설명하지는 않는다.
응답을 4초 늦췄다
곧바로 실패하는 요청은 빠르게 기본 화면이나 캐시로 넘어간다. 더 신경 쓰였던 쪽은 응답이 오지 않은 채 기다리는 경우였다.
실행 중 새 이미지를 받는 방식으로 전환하고, 이벤트 정보 응답을 4초 늦췄다. 기기에는 유효한 B가 저장돼 있었다. 새 이미지 준비를 기다리는 제한 시간은 1.5초로 뒀다. 이 값도 이번 비교를 위한 설정이다.

로그에는 준비 시작이 14ms, 제한 시간에 따른 종료가 1524ms로 남았다. 1525ms에 B 캐시를 선택했고, 1572ms에 B의 onLoad가 호출됐다. 홈 layout은 2633ms였다. 모두 JS 관찰 코드의 시작점부터 잰 시간이다.
1.5초 제한을 뒀다고 홈까지 1.5초 안에 도착하는 건 아니었다. 제한은 새 이미지를 준비하는 구간에만 걸려 있다. 그 뒤 캐시 이미지를 로드하고, 비교 조건으로 둔 1초 표시 시간을 거친다. 기본 화면이나 이벤트를 얼마나 오래 붙잡을지는 별도의 선택이었다.
이번 실행에서는 서버의 4초 응답을 끝까지 기다리지 않고 캐시를 선택한 것을 확인했다. 실제 통신 단절이나 느린 셀룰러망을 시험한 것은 아니다. 지연과 오류를 통제할 수 있는 로컬 서버로 먼저 흐름을 확인했다.
기다리기를 끝낸 뒤 도착하는 응답
화면만 캐시로 넘기고 다운로드 결과는 계속 저장하게 두면 어떨까. 준비 제한이 지난 뒤 완료된 요청이 다음 실행의 캐시를 바꿀 수 있다. 이번에는 제한이 지난 뒤 돌아온 다운로드 결과를 저장 경로로 넘기지 않기로 했다.
제한 시간에는 AbortController를 취소한다. 그리고 다운로드가 끝나 돌아왔을 때도 취소 상태를 다시 확인한다. 아래는 실제 요청 처리 부분이다.
취소를 요청하고, 늦게 도착한 결과도 걸러내도록 했다. 이 경계는 단위 테스트로 따로 확인했다. 다운로드 Promise를 보류해 둔 채 제한 시간을 넘기고, A 캐시가 선택된 뒤에 B로 완료시켰다. 그 뒤에도 선택은 A였고 저장 호출은 없었다.
실기기의 4초 지연 실험에서는 제한 시간에 캐시로 빠져나오는 흐름을 확인했다. 취소를 무시한 응답을 나중에 강제로 완료하는 경우는 위 단위 테스트에서 만든 조건이다. 기기에서 모든 경합을 확인한 것은 아니다. 앱 종료나 저장 도중 중단되는 경우도 아직 남아 있다.
파일이 남아 있어도 이벤트는 끝날 수 있었다
캐시가 있다는 사실만으로는 충분하지 않았다. 이벤트 A의 유효 기간을 짧게 두고 정상적으로 받아 저장한 다음, 기간이 지나기를 기다렸다. 이후 서버가 오류를 반환하게 만들고 캐시 방식으로 앱을 열었다.
저장소에서는 A를 읽었다. 그런데 이번 실행의 선택은 기본 화면이었다. 파일이 남아 있어도 기간이 끝난 이벤트는 다시 보여주지 않았다.
기간을 판단하는 함수는 현재 아래와 같다. 시작 시각은 포함하고 종료 시각부터는 제외한다.
현재는 기기 시각으로 비교한다. 기기의 시간이 틀렸을 때도 서버 기준으로 정확히 끝나는 구조는 아니다. 또 아직 기간이 남은 이벤트를 서버에서 급히 내리더라도, 응답을 받지 못한 기기는 그 사실을 알 수 없다. 캐시를 먼저 보여주는 선택에는 이런 제약도 남는다.
홈 layout 이후도 한 번 더 확인했다
실패 조건마다 홈 layout 로그까지 이어지는 것을 봤다. 그래도 layout 로그만으로 버튼 입력이 된다고 판단하지는 않았다. Maestro로 홈 버튼을 누르고 입력 완료 문구가 뜨는지 별도로 확인했다.
세로 화면에서는 통과했지만 가로 화면에서는 한 번 실패했다. 기록을 보니 테스트가 화면 끝에 1px만 보이는 버튼을 누르고 있었다. 버튼이 충분히 보일 때까지 스크롤한 뒤 누르도록 테스트를 바꿨고, 가로에서도 입력 완료 문구를 확인했다. 이미지 실패 처리가 아니라 테스트의 탭 조건에서 생긴 문제였다.
이번에 확인한 흐름에서는 새 이미지를 못 받아도 앱을 열 수 있었다. 유효한 캐시가 있으면 쓰고, 없거나 만료됐으면 기본 화면으로 이어간다. 끝까지 기다리지 않을 시점도 정했다.
다만 기본 화면까지 1초 동안 보여준 것은 실험 조건이었다. 새 이벤트를 못 받았고 홈은 이미 준비됐다면 그 1초를 남겨둘 이유가 있을까. 이 선택은 아직 남아 있다.
그전에 비교할 방법이 하나 더 있었다. 다음 글에서는 파일을 직접 저장하지 않고 서버의 이미지 URL을 Image에 바로 넘겨 본다.
