Archive Lab
이미지와 링크를 모으는 React Native 앱에서 스크롤을 측정하고, 최적화 가설을 검증하는 개발 기록입니다.
목차
지난 글에서 빠른 스크롤을 녹화하다가 사진이 잠깐 회색으로 보이는 장면을 발견했다. 카드 그림자를 뺀 앱에서 본 현상이었는데, 기본 앱과는 스크롤 위치가 달라 제대로 비교하지 못했다.
이번에는 기본 앱부터 다시 녹화했다. 회색 영역이 여기서도 나왔다. 그림자를 없애야만 생기는 현상은 아니었다.
카드는 있는데 사진이 없었다
같은 개인 갤럭시에서 자료 1,000개를 띄우고 빠르게 아래위로 스크롤했다. 사진은 이전과 같이 앱에 넣어 둔 12장을 반복한다. 네트워크로 사진을 내려받는 앱은 아니다.
영상을 나눠 보니 화면 아래로 들어온 카드에 하트 버튼과 둥근 외곽은 이미 있었다. 그런데 사진 자리는 단색이었다. 다음으로 추출한 화면에서는 그 자리에 설산과 해안 절벽 사진이 보였다.

FlatList 문서에도 화면 밖 콘텐츠를 비동기로 채우기 때문에 빠른 스크롤에서 빈 영역이 보일 수 있다는 설명이 있다. 다만 이 장면에서는 카드 일부가 이미 보였다. 목록이 비었다는 말로 묶기보다, 사진이 채워지는 과정을 따로 확인하고 싶었다. FlatList 문서
영상에서 두 장면의 시각 차이는 약 32ms였다. 이것을 사진 로딩 시간이라고 쓰지는 않았다. 녹화가 화면의 모든 프레임을 담는 것도 아니고, 사진이 처음 비기 시작한 순간을 정확히 잡은 것도 아니기 때문이다. 여기서 확인한 것은 회색 사진 영역이 실제로 있었다는 사실이다.

사진마다 기록을 붙였다
저장해 둔 trace에는 이미지 뷰 생성, 사진 디코딩, texture upload 구간이 있었다. 하지만 어느 카드의 작업인지 알 수 없었다. 시간을 비슷하게 맞춰 놓는 것만으로 영상 속 사진과 연결할 수는 없었다.
그래서 진단용 앱에 카드 ID를 붙여 기록했다. 이미지 컴포넌트의 JS 마운트·언마운트, onLoadStart와 onLoad, 행 가시성 콜백을 받은 시점을 함께 모았다. 같은 사진을 여러 카드가 사용하므로 사진 번호만으로는 부족했다. 같은 카드가 다시 만들어지거나 로드 이벤트가 반복되는 경우도 구분했다.
스크롤 중에는 로그를 출력하지 않았다. 메모리에 모아 뒀다가 스크롤이 끝난 뒤 꺼냈다. 중간에 로그가 끊긴 실행도 있어서 시작·끝 표식과 이벤트 개수가 맞는 기록만 사용했다. 계측을 넣은 앱의 수치를 지난 프레임 성능 결과와 섞지는 않았다.
기록을 보니 화면 밖의 이미지 컴포넌트는 먼저 만들어져 있어도, 로드 시작 이벤트는 한참 뒤 첫 스크롤에서 오는 경우가 있었다. JS에서 컴포넌트를 만들었다는 기록만 보고 사진까지 준비됐다고 생각하면 안 됐다.
JS 콜백에서 잰 시간
onLoadStart와 onLoad에서 현재 시각을 찍어 빼면 두 콜백을 받은 간격이 나온다. 실제 이미지 처리 시간과 얼마나 다를지 확인해 봤다.
이번에는 JS가 콜백을 받은 시각과 네이티브 이벤트에 붙은 시각을 따로 보관했다. 사용 중인 RN 0.86.3 Android Fabric의 전달 경로를 확인하고 실제 기기에서도 두 값을 받았다. 공식 onLoad 설명은 이미지 로드 성공 이벤트다. 화면에 사진이 그려진 순간까지 보장하는 콜백은 아니다. Image 문서
첫 번째 정상 수집에서 11번 카드의 첫 요청을 보면 두 계산이 달랐다.
| 시작과 완료 사이 간격 | 관측값 |
|---|---|
| 네이티브 이벤트에 기록된 시각으로 계산 | 17ms |
| JS 콜백에서 받은 시각으로 계산 | 약 2.9ms |
시작 이벤트가 JS에 늦게 도착하고 완료 이벤트가 그 뒤에 가까이 도착하면, 콜백에서 잰 간격은 짧아질 수 있다. 여기서는 두 이벤트를 받는 시점의 차이가 측정값에 들어왔다. 2.9ms만 보고 사진 로딩이 그만큼 걸렸다고 해석할 수 없었다.
17ms도 순수 JPEG 디코딩 시간은 아니다. 네이티브의 시작·완료 이벤트 시각 사이 간격이다. 실제 사진이 포함된 프레임이 화면에 표시되기까지의 시간은 여전히 별도로 봐야 한다.
목록 노출 기록도 비슷했다. 어떤 행은 보인다는 JS 콜백 뒤에 onLoad 콜백을 받았는데, 네이티브에서는 이미 로드 완료 이벤트를 보낸 상태였다. 단순히 두 JS 콜백을 빼서 빈 사진이 보인 시간으로 계산하면 틀린 숫자가 나올 수 있었다.
게다가 이 앱은 두 열 목록이라 가시성 콜백이 행의 위치를 기준으로 한다. 위로 스크롤하면 사진보다 카드 아래의 제목부터 들어올 수도 있다. 행이 보인 시각과 사진 영역이 보인 시각도 구분해야 했다.
페이드 설정은 이미 0이었다
사진이 서서히 나타나는 효과 때문인지도 확인했다. Android Image에는 fadeDuration이 있다. 이 값을 0으로 바꾸면 해결될 것처럼 보였지만, 설치된 RN 소스에서 로컬 리소스 이미지의 기본 페이드 시간은 이미 0이었다.
계측에서도 이번 사진들이 로컬 리소스로 해석되는 것을 확인했다. 이 앱에 fadeDuration={0}을 추가해도 기대했던 작업이 줄어드는 변경은 아니었다. RN 0.86.3 구현
이번에 추가한 코드는 진단용 빌드에서만 켜진다. 일반 앱의 사진 표시 방식은 아직 그대로다.
대신 다음에 확인할 범위는 좁아졌다. 기본 앱에서도 사진이 늦게 채워지는 장면이 있고, JS 콜백만으로는 그 시간을 설명할 수 없었다. 이제 카드별 네이티브 뷰 연결과 첫 그리기까지 이어서 볼 차례다. 사진 준비가 늦은지, 준비된 사진을 화면에 붙이는 과정이 늦은지 확인한 뒤 변경할 부분을 고르려고 한다.
