Archive Lab
이미지와 링크를 모으는 React Native 앱에서 스크롤을 측정하고, 최적화 가설을 검증하는 개발 기록입니다.
목차
2편에서는 목록을 늘리며 스크롤의 기준 수치를 잡았다. 이제 느린 프레임 안에서 어떤 일이 벌어졌는지 볼 차례였다.
이번에는 FlatList 설정 두 가지를 시험했다. 화면 밖 뷰를 떼는 기능을 끄는 것, 그리고 미리 렌더링해 두는 범위를 줄이는 것. 둘 다 스크롤을 개선할 수 있을 거라는 근거가 있었지만, 결과를 보고 모두 되돌렸다.
먼저 눈에 들어온 건 clipping 계산이었다
기준 측정의 trace를 열었을 때 ReactScrollView.updateClippingRect가 눈에 띄었다. 특히 오래 걸린 프레임에서 이 구간이 길게 나타나는 경우가 있었다.
이름만 보면 화면을 잘라 내는 것 같지만, 여기서 하는 일은 화면 밖의 네이티브 뷰를 화면 계층에서 떼고 필요한 뷰를 다시 붙이는 쪽에 가깝다. FlatList의 removeClippedSubviews가 이 동작을 제어하고, Android에서는 기본값이 true다. 뷰 객체 자체를 없애는 기능은 아니다. React Native 문서
물론 화면 밖 뷰까지 계속 붙여 두면 다른 비용이 생길 수 있다. 그래도 지금 trace에서 반복해서 보이는 계산을 없앴을 때, 얻는 쪽이 더 클 가능성은 확인해 볼 만했다.
Android에서 이 값만 false로 바꿨다. 사진이나 카드 스타일, 한 번에 렌더링하는 범위는 그대로 뒀다. JS 가상화를 끄거나 1,000개를 한꺼번에 렌더링하는 변경은 아니었다.
기준값, 변경값, 다시 기준값
두 실험 모두 같은 개인 Android 기기에서 1,000개 메타데이터를 사용했다. 사진은 로컬 JPEG 12장을 반복했다. 앱을 매회 다시 시작하고, 아래로 8번·위로 8번 같은 스크롤을 실행했다. 개발 서버를 거치지 않는 benchmark 빌드였다.
비교 전에 APK도 확인했다. 각 실험의 기준 앱과 변경 앱은 JS bundle만 다르고, 나머지 1,102개 항목은 같았다. 설정 한 줄을 비교하려는데 다른 빌드 차이가 섞이면 해석하기 어려워서다.
처음부터 많은 횟수를 돌리지는 않았다. 기본값 A를 1회, 변경값 B를 2회 측정하고, 유망할 때 반복을 늘리기로 했다. 결과가 나빠지면 A로 돌아가 2회 더 확인한다.
유지할 기준도 미리 정했다. 본 비교에서 frameOverrunMs P95가 10% 이상 줄고, 더 느린 쪽인 P99와 화면·기능에서도 문제가 없어야 했다. P95는 프레임별 값을 작은 순서로 놓았을 때 95% 지점의 값이다. 여기서는 프레임 기한을 얼마나 넘겼는지를 보므로 낮을수록 좋다. FPS나 터치 반응 시간을 직접 측정한 값은 아니다.
| clipping 실험 | 반복 | frameOverrunMs P95 |
|---|---|---|
| A: Android 기본값 | 1회 | 4.59ms |
| B: clipping 끔 | 2회 | 6.42ms |
| A: 기본값으로 복귀 | 2회 | 4.57ms |
5회에서 5,323프레임을 모았다. 표의 값은 각 단계에서 수집한 프레임들을 합쳐 계산했다. 회차별 P95를 평균 낸 값은 아니다.
기대와 반대였다. 바꾼 쪽의 P95가 더 높았고, 기본값으로 되돌리니 처음과 비슷해졌다. B의 두 회차를 따로 봐도 6.38ms, 6.49ms였다.
없애려던 계산은 실제로 없어졌다
흥미로운 건 가설의 앞부분은 맞았다는 점이다. B의 trace에서는 clipping 계산 구간이 사라졌고, 프레임 처리 구간에서 메인 스레드가 실제로 실행 중이던 시간도 줄었다.
그런데 프레임 전체는 늦어졌다. RenderThread 쪽을 함께 보니 다른 변화가 있었다.
| trace 프레임 처리 구간당 평균 | A | B 1회차 | B 2회차 |
|---|---|---|---|
| 메인 스레드 Running | 2.030ms | 1.762ms | 1.757ms |
| RenderThread Running | 3.960ms | 5.604ms | 5.567ms |
RenderThread prepareTree | 0.174ms | 0.822ms | 0.819ms |
메인 스레드 postAndWait | 0.856ms | 2.081ms | 2.055ms |
메인 스레드의 계산 일부를 덜어 낸 대신, 렌더링 트리를 준비하는 작업과 그 뒤의 기다림이 늘었다. 이 항목들은 서로 겹치는 구간도 있어서 전부 더해 프레임 시간으로 계산하지 않았다.
별도 화면 점검에서도 차이가 보였다. 스크롤했다가 같은 첫 화면으로 돌아와 멈춘 상태에서, 화면 계층의 뷰 수는 A가 80개, B가 584개였다. 이 숫자는 View 객체의 총개수도, 카드 개수도 아니다. 실제로 전체 View 객체 수는 두 경우에 같았다.
화면 밖 뷰를 붙여 둔 쪽에서 트리 처리 비용이 커졌다는 설명과 맞아떨어지는 관찰이었다. 다만 멈춘 시점의 뷰 수만으로 스크롤 중 모든 프레임의 원인을 설명할 수는 없다. 기본값으로 복귀한 trace에서 prepareTree와 postAndWait도 이전 수준으로 돌아온 점까지 함께 봤다.
없애려던 구간을 없앴다는 사실만으로 이 설정을 남길 이유는 없었다. 첫 변경은 여기서 원복했다.
그럼 렌더링 범위를 줄이면 어떨까
다음 질문은 자연스럽게 이어졌다. 화면 밖 뷰를 떼는 기능은 살려 두고, 애초에 유지하는 렌더링 범위를 줄이면 어떨까?
이번에는 Android의 windowSize만 기본값 21에서 7로 줄였다. 앞서 바꿨던 clipping은 이미 기본값으로 돌아온 상태였다.
windowSize의 단위는 항목 개수가 아니라 화면 높이다. 7이면 대략 현재 화면과 위아래 세 화면에 해당하는 범위를 목표로 한다. 작게 잡으면 동시에 유지하는 항목을 줄일 수 있지만, 빠르게 스크롤할 때 빈 영역을 만날 가능성도 커진다. React Native 문서
숫자를 3분의 1로 줄였다고 실제 렌더링 개수도 정확히 3분의 1이 되는 것은 아니다. 초기 영역을 보존하는 동작과 목록 경계, 비동기 렌더링 배치가 함께 작용한다. 이 앱은 두 열 목록이어서 내부 행과 카드 개수도 구분해야 했다.
이번 비교용 A와 B를 새로 빌드하고, 다시 A 1회 → B 2회 → A 2회 순서로 측정했다. 앞선 실험과 APK가 다른 별도 비교라 두 실험의 프레임을 하나로 합치지는 않았다.
| windowSize 실험 | 반복 | frameOverrunMs P95 |
|---|---|---|
| A: 기본값 21 | 1회 | 4.371ms |
| B: 7로 축소 | 2회 | 5.082ms |
| A: 기본값으로 복귀 | 2회 | 4.595ms |
이번에는 총 5회, 5,339프레임이었다. 이쪽도 개선 근거를 얻지 못했다.
trace에서는 clipping 계산의 평균 시간이 조금 줄었다. 하지만 메인 스레드와 RenderThread 전체의 평균 실행 시간은 거의 달라지지 않았다. 한 B 회차에서는 mount와 buffer 대기가 늘었지만, 다른 회차에서는 비슷한 변화가 반복되지 않았다. 그래서 “범위를 줄여 재마운트가 늘었고, 그것 때문에 느려졌다”고 원인을 확정하지 않았다.
복귀 A도 최초 A와 완전히 같지는 않았다. 배터리 온도는 측정 과정에서 조금 올랐고 순서도 고정돼 있었다. 이 짧은 비교로 작은 window가 일반적으로 더 느리다고 말할 수는 없다. 다만 지금 이 앱의 스크롤을 개선하려고 설정을 유지할 근거는 없었다. 두 번째 변경도 되돌렸다.

이번에 멈춘 곳
두 후보 모두 짧은 탐색에서 채택 기준을 통과하지 못했다. 유망한 후보에만 진행하려던 20회 비교는 하지 않았다. 수천 프레임이 있어도 같은 실행 안에서 이어진 값들이므로, 그만큼 독립적인 실험을 했다는 뜻은 아니다.
windowSize 후보의 메모리 비교와 빠른 스크롤 영상 점검도 남아 있다. 따라서 메모리가 얼마나 줄었는지, 빈 영역이 생겼는지는 이번 글에서 답하지 못한다. 프레임 개선이 없었던 설정을 메모리에 좋을 것 같다는 이유로 남겨 두지도 않았다.
현재 앱은 두 설정 모두 기본값이다. 원복 후 테스트 34개와 타입 검사는 통과했다. 이 글을 쓰려고 추가 측정을 돌리지는 않았고, 앞서 얻은 결과까지 정리했다.
다음에는 느린 프레임에서 반복되는 작업을 조금 더 좁혀 보려고 한다. 카드 그리기의 어떤 부분이 시간을 쓰는지, JS 쪽 작업도 함께 관여하는지 아직 확인할 게 있다. 다음 옵션을 고르기 전에 그 근거부터 찾을 생각이다.
