
자사 서비스 개발을 하다 보니 Sticky Header UI를 구현할 일이 생각보다 많았다.
React Native에서는 성능 이슈가 비교적 쉽게 발생하는 만큼
리스트 렌더링에는 FlashList를 사용하며 최대한 최적화를 고려하고 있다.
이번 글에서는 FlashList + Sticky Header를 구현하면서 특히 애를 먹었던 부분들과 그 과정에서 정리한 포인트들을 공유해보려고 한다.
FlashList에서 제공하는 stickyHeaderIndices 옵션을 활용해 Sticky Header를 구현해보았다.
하지만 예상과는 다른 문제가 발생했다.
stickyHeaderIndices는 renderItem으로 렌더링되는 항목에만 적용되며,
ListHeaderComponent에는 적용할 수 없었다.
그 결과 리스트의 첫 번째 상품 아이템이 고정되는 현상이 발생했고,
의도했던 Sticky Header와는 전혀 다른 어색한 UI가 만들어졌다.
그래서 다음으로는:
ListHeaderComponent를 사용하지 않고 Header 자체를renderItem안으로 넣어보자
라는 방향으로 구현을 변경했다.
이를 위해 리스트 데이터에 타입을 부여해 각 아이템 역할을 구분하도록 구성했다.
type: 'header' → Sticky Header 영역type: 'item' → 일반 상품 아이템이렇게 하면 Header 역시 리스트 아이템처럼 취급되기 때문에 stickyHeaderIndices를 적용할 수 있을 것이라 생각했다.
하지만 이 방식 역시 FlashList의 구조와 완전히 잘 맞는 방식은 아니었다.
FlashList 입장에서는 Header 또한 일반 아이템과 동일하게 처리되기 때문에:
결과적으로 FlashList의 가상화 장점을 일부 희생하는 구조에 가까웠다.
또한 성능 관점에서도 아쉬움이 있었다.
FlashList는 기본적으로 아이템 가상화에 최적화된 리스트인데,
stickyHeaderIndices는 내부적으로 레이아웃 계산과 위치 보정 과정을 수행하기 때문에 스크롤 중 추가 비용이 발생할 수 있었다.
결국 FlashList에서 제공하는 스크롤 이벤트를 활용해 현재 스크롤 위치를 직접 추적하는 방식으로 구현했다.
스크롤 중 전달받는 현재 Y축 값을 기준으로 Header의 위치를 비교하고,
특정 시점부터 Header를 show / hide 하도록 구성했다.
실제 구현에서는 Header의 opacity 값을 조정해
스크롤 위치에 따라 자연스럽게 나타나고 사라지는 형태로 처리했다.
이를 통해 Sticky Header를 실제로 고정시키지 않더라도,
사용자 입장에서는 자연스러운 Sticky UI처럼 느껴지도록 구현할 수 있었다.
무엇보다 레이아웃 재계산을 최소화하면서 JS → UI 업데이트 비용도 줄일 수 있었고,
결과적으로 성능과 UX 모두 만족할 수 있는 방향으로 마무리할 수 있었다.