Archive Lab
이미지와 링크를 모으는 React Native 앱에서 스크롤을 측정하고, 최적화 가설을 검증하는 개발 기록입니다.
목차
지난 글에서 11번 카드의 기록을 따라갔다. 이미지 뷰는 먼저 있었지만, 사진 요청은 뷰가 화면에 붙을 때 시작됐다. 첫 그리기에는 비트맵이 없었고 로드가 끝난 뒤 다시 그려졌다.
이번에는 그 사이에 들어갈 사진을 미리 준비해 봤다. 화면이 뜨기를 기다리게 만들면 곤란하니, 목록은 그대로 보여주면서 뒤에서 사진을 준비하도록 했다.
번들에 있어도 준비할 일은 남아 있었다
먼저 RN의 Image.prefetch를 확인했다. 사용 중인 Android 구현은 Fresco의 prefetchToDiskCache를 호출한다. 이번 앱의 사진은 이미 번들에 들어 있다. 필요한 것은 파일을 내려받는 일보다, 그 파일을 풀어서 그릴 수 있는 비트맵으로 준비하는 일이었다. RN 구현
그래서 Android에 작은 모듈을 추가했다. 첫 목록의 자료 12개에서 사진을 골라 중복을 빼고, 한 번에 한 장씩 prefetchToBitmapCache로 준비한다. 자료 1,000개 전체를 돌지는 않는다. 요청이 끝나면 작업에 사용한 참조는 닫고, 사진은 Fresco가 관리하는 캐시에 남긴다.
여기서 실제 이미지 뷰와 같은 요청을 만드는 게 중요했다. JS에서 받은 번들 URI는 assets_photos_photo00 같은 리소스 이름이었다. RN의 ImageSource로 해석해야 실제 뷰와 같은 리소스를 가리킨다. 크기 옵션도 임의로 넣지 않았다. 지금 번들 사진에 적용되는 RN의 AUTO 경로는 resizeOptions=null이고, 다른 크기로 준비하면 캐시 키가 달라질 수 있었다.
준비가 끝났다는 응답만 믿지는 않았다. 각 요청으로 비트맵 캐시를 다시 조회해 사진이 있는지도 확인했다. Fresco의 비트맵 준비·캐시 API
처음 그릴 때 사진이 있었는지 비교했다
진단 앱에서 빠른 왕복 스크롤을 반복했다. 기준 앱 두 번, 미리 준비하는 앱 두 번, 다시 기준 앱 두 번 순서다. 양쪽 모두 같은 시작 절차를 거쳤고, 후보 앱만 준비가 끝날 때까지 따로 기다리지는 않았다.
첫 화면에 나오는 1–4번은 빼고, 스크롤하면서 처음 등장하는 5–12번 카드를 봤다. 같은 카드에서 첫 요청과 첫 그리기가 연결된 기록만 사용했다.
| 실행 순서 | 첫 그리기에 비트맵이 없던 카드 |
|---|---|
| 기준 앱, 두 번 | 각각 8개 / 8개 |
| 미리 준비한 앱, 두 번 | 각각 0개 / 8개 |
| 다시 기준 앱, 두 번 | 각각 8개 / 8개 |
대상 카드의 기록은 여섯 실행 모두 빠짐없이 연결됐다. 미리 준비한 앱은 두 번 모두 사진 12장의 캐시 조회에 성공했다. 준비 작업에 걸린 시간은 111ms와 112ms였다.
11번 카드도 달라졌다. 기준 앱에서는 첫 그리기에 비트맵이 없었지만, 후보 앱에서는 첫 그리기부터 있었다. 로드 시작과 완료 이벤트가 같은 밀리초에 기록되기도 했다. 캐시에 준비해 둔 사진을 가져온 흐름과 맞았다.

이 표는 네이티브 그리기 호출 때의 비트맵 유무다. 화면에 보인 빈 프레임 수나 FPS를 센 것은 아니다. 또 자동화로 화면과 자료 개수를 확인하는 동안 약 9초가 지나 있었다. 앱을 켜자마자 바로 스크롤하는 경우까지 확인했다고 쓰지는 않기로 했다.
기다릴 일을 줄인 만큼 메모리는 먼저 썼다
준비 작업이 끝난 뒤 사진 캐시는 약 17.8MiB였다. 앱 메모리가 그만큼 늘었는지는 별도로 확인해야 해서 진단을 끈 일반 앱으로 다시 비교했다. 기준 앱 네 번과 후보 앱 두 번에서 같은 네 시점의 메모리를 수집했다.
| 시점 | 기준 앱 RSS 중앙값 | 미리 준비한 앱 RSS 중앙값 |
|---|---|---|
| 첫 목록 | 292.2MiB | 307.3MiB |
| 아래로 스크롤한 뒤 | 342.2MiB | 344.3MiB |
| 왕복한 뒤 | 346.0MiB | 344.6MiB |
| 돌아와서 10초 대기 | 343.5MiB | 341.3MiB |
RSS는 사진 캐시뿐 아니라 앱이 사용하는 코드와 공유 페이지 등을 포함한 값이다. Android 메모리 문서
첫 목록에서는 후보 앱이 약 15MiB를 더 썼다. 아래로 내려간 뒤에는 차이가 작아졌다. 기준 앱도 스크롤하면서 사진을 준비하니, 이번 변경은 그 비용을 앞으로 당기는 방식에 가까웠다.
초기 메모리가 늘어나는 건 분명한 비용이다. 목록을 조금만 보고 나가는 사람에게는 아직 보지도 않은 사진을 준비한 셈이다. 실험 전에 정한 추가 RSS 16MiB 안에는 들어왔지만 여유가 크지는 않았다. 마지막 수치가 조금 낮다는 이유로 메모리도 절약했다고 보지는 않았다.
스크롤이 더 느려지지는 않았을까
사진을 먼저 준비하느라 스크롤을 방해하면 곤란하다. 진단과 녹화를 끈 일반 앱으로 기존 스크롤 테스트를 다시 돌렸다. 기준 앱 5회, 후보 앱 10회, 다시 기준 앱 5회. 각 버전 10회씩, 총 21,406프레임을 비교했다.
이번 목적은 사진 준비 시점을 앞당기는 것이다. 실험 전에 프레임 P95와 P99가 5% 넘게 악화되지 않는지를 비용 기준으로 정했다. 블록별 백분위수를 평균내지 않고 각 버전의 원시 프레임을 모아 계산했다.
| 지표 | 기준 앱 | 미리 준비한 앱 | 변화 |
|---|---|---|---|
| CPU 프레임 시간 P95 | 9.903ms | 9.845ms | −0.59% |
| CPU 프레임 시간 P99 | 13.947ms | 14.457ms | +3.66% |
| 프레임 기한 초과 P95 | 5.061ms | 4.872ms | −3.74% |
| 프레임 기한 초과 P99 | 9.531ms | 9.541ms | +0.10% |
전체 비교에서는 정해 둔 범위 안이었다. 다만 처음 두 블록끼리 비교하면 기한 초과 P99가 약 6.8% 높았고, 뒤의 두 블록에서는 약 1.7% 낮았다. 측정 동안 배터리 온도도 31.7°C에서 33.1°C로 올랐다. 이 작은 차이를 두고 스크롤 성능까지 좋아졌다고 말하기는 어렵다.
일반 앱도 각각 한 번씩 따로 녹화했다. 처음 3초의 샘플을 확인하니 기준 앱의 5번 카드는 처음 보일 때 회색이었다가 다음 샘플에서 설산 사진이 나왔다. 후보 앱에서는 그 카드가 처음 보이는 샘플부터 사진이 있었다. 눈으로도 차이를 확인했지만, 한 번의 영상 일부를 본 결과라 모든 빈 사진이 사라졌다고 하지는 않겠다.
지금 앱에는 남겨두기로 했다
사진 준비 효과가 반복됐고, 메모리와 전체 프레임 비교도 실험 전에 정한 범위 안에 들어왔다. 검색·즐겨찾기·상세 화면을 오가는 기능 테스트까지 확인한 뒤, 번들 사진을 쓰는 지금 앱에는 이 변경을 남겨두기로 했다. 기기에도 진단을 끈 후보 앱을 설치했다.
처음 12개 자료의 사진만 한 번 준비한다. 준비가 끝날 때까지 목록을 붙잡아 두지 않고, 실패하더라도 원래 이미지 요청은 계속 동작한다. 이번 적용 범위는 사진이 APK에 들어 있는 Android 빌드다. Metro에서 내려주는 이미지나 외부 URL 사진까지 다룬 코드는 아니다.
첫 화면 메모리 약 15MiB를 써서 아직 화면에 나타나지 않은 사진을 준비했다. 그 비용을 받아들인 선택이다. 다음에는 앱을 켜자마자 스크롤해도 준비가 따라오는지, 준비할 사진 수를 줄이면 어떤 차이가 생기는지 확인해 보고 싶다.
이번 데이터는 사진 12장을 반복해 쓴다. 첫 12개 자료를 준비하면 뒤의 자료들도 같은 사진 캐시를 재사용한다. 고유한 사진이 수백 장인 목록에서도 같은 결과가 날지는 별도 실험이 필요하다.
