nika-blog

React-Native: CollapsibleTabView Reanimated 기반 성능 최적화 본문

React Native

React-Native: CollapsibleTabView Reanimated 기반 성능 최적화

nika0 2025. 5. 6. 22:52

이 글은 React Native 기반 앱에서 스크롤, 제스처, UI 업데이트를 최적화하기 위해 CollapsibleTabView를 어떻게 개선했는지를 설명합니다. 특히 Reanimated를 활용한 UI Thread 기반 렌더링 구조와 그 구현 방식을 자세히 다룹니다.


🧭 왜 이 구조가 필요했을까?

Native 앱에서 성능이 중요한 이유

Native 환경에서는 항상 UI Thread 관리가 핵심입니다.

  • UI Thread가 막히면 ANR 또는 App Freeze 발생
  • Android: AsyncTask → Coroutines, iOS: GCD → Swift Concurrency로 발전

그런데… React Native는 다르다?

React Native는 대부분의 UI 처리를 Native 엔진에 위임합니다.

  • 그래서 일반적으로는 JS Thread 위주로 성능을 고민하게 됩니다.
  • 하지만, UI 반응이 즉각적이어야 하는 상호작용(스크롤, 제스처)에서는 구조적 한계가 존재합니다.

💥 문제: UI Thread에 직접 접근할 수 없다

JS Thread에서 제스처나 스크롤 위치를 계산하고
→ 이 값을 넘겨서 UI Thread가 반영하는 구조는 레이턴시가 생깁니다.

"내가 스크롤을 했는데 화면 반응이 한 박자 느린 것 같은…"
→ 바로 이 레이턴시가 사용자 경험을 망칩니다.


✅ 해결책: Reanimated + Worklet

Reanimated는 JS 코드를 UI Thread에서 실행할 수 있는 worklet 기능을 제공합니다.

이 구조를 이용하면…

  • 스크롤/제스처 계산을 UI Thread에서 처리
  • scrollTo, withDecay, SharedValue 등을 통해
    스크롤 위치를 직접 조정하고 상태를 동기화할 수 있습니다.

 

💡 구조적 핵심 요약 – 왜 이 설계가 중요한가?

이 구조에서 가장 중요한 철학은 “모든 스크롤 상태를 하나의 값에 집중시켜 관리한다”는 점입니다.
그 중심에는 scrollY라는 SharedValue가 있습니다.

React Native에서는 JS Thread와 UI Thread가 따로 돌아가며, 일반적으로 두 쓰레드 사이에서 데이터를 주고받을 때 딜레이나 레이스 컨디션 같은 문제가 생길 수 있습니다.
이를 해결하기 위해 Reanimated는 SharedValue라는 특별한 구조를 제공합니다. 이 값은 JS Thread와 UI Thread 양쪽에서 모두 접근 가능하고, 실시간으로 안전하게 공유할 수 있는 데이터 통로입니다.

따라서 이 구조에서는 모든 UI 위치 계산을 scrollY 하나에 집중시킵니다.

  • 사용자가 리스트를 스크롤하든
  • 헤더 영역을 팬 제스처로 밀든
  • 혹은 탭 간 스크롤 위치를 동기화하든

결국은 scrollY 값이 변하고, 그 값에 따라 UI가 갱신되도록 설계되어 있습니다.

또한 Reanimated의 scrollTo, withDecay, runOnJS 같은 고급 기능을 활용하여,

  • UI Thread에서 직접 스크롤 위치를 계산하고 반영하고
  • 관성 스크롤 같은 동작도 자연스럽게 처리하며
  • 특정 상태가 바뀌었을 때는 JS 콜백도 안전하게 호출할 수 있게 됩니다.

이러한 구조 덕분에,

여러 탭에서 각각 다른 스크롤 이벤트가 발생하더라도,
모든 흐름은 결국 scrollY와 syncScrollOffset이라는 두 개의 중심 값으로 수렴됩니다.

이를 통해 코드 구조는 단순해지고, 예외 처리도 줄어들며, UI 일관성과 성능을 모두 잡을 수 있게 되는 것입니다.

 

🧱 CollapsibleTabView 구조 요약 (UI & 상태 관리 흐름 다이어그램)

[User Interaction]
   ├── 리스트 영역 스크롤
   │     └── [onScroll]
   │            └── [scrollY (SharedValue)] ← 상태 갱신
   │                   └── [Header, TabBar 위치] ← 애니메이션 적용
   │
   └── 헤더/탭바 PanGesture
         └── [panGestureScroller (SharedValue)] ← 제스처 기반 위치 계산
                └── [scrollTo] ← 실제 리스트에 반영 (UI Thread)

┌────────────┐
│ usePanGesture │
└────────────┘
   └── 관성 스크롤 withDecay
   └── stopMomentumScrolling

┌────────────┐
│ useCollapsibleTab │
└────────────┘
   ├── [scrollTo] ← panGesture 또는 syncScrollOffset에서 호출
   ├── [onScrollHandler] ← scrollY & tabScrollY 갱신
   └── [scrollToTop] ← 탭 재선택 시 처리

┌────────────┐
│ useSyncScroll │
└────────────┘
   ├── [syncScroll] ← onScroll / PanGesture에서 호출
   └── [syncScrollOffset (SharedValue)]
           └── 다른 탭에서 감지 → scrollTo

┌────────────┐
│ CollapsibleTabView UI │
└────────────┘
   ├── [AnimatedHeader] ← scrollY 기반 translateY 적용
   ├── [AnimatedTabBar] ← scrollY + headerTranslateY
   └── [HorizontalSwipeable] ← index 변경 시 onSwipe

 


🔁 주요 SharedValue 및 컨텍스트 구조

interface CollapsibleTabContextValue {
  action: {
    stopMomentumScrolling: () => void, // 관성 스크롤을 강제로 중단하는 함수
    syncScroll: (tabIndex: number, endY: number) => void, // 탭 간 스크롤 위치 동기화
  },
  attributes: {
    headerHeight: number,     // 헤더 높이 (스크롤 위치 제한 등에 사용)
    selectedIndex: number,    // 현재 선택된 탭 인덱스
    tabBarHeight: number,     // 탭바 높이 (헤더 접힘 여부 계산 등)
  },
  panGestureScroller: SharedValue<number>, // 팬 제스처로 계산된 스크롤 위치
  setScrollY: (y: number) => void,         // 현재 탭의 scrollY 값 업데이트 함수 (worklet에서 실행 필요)
  syncScrollOffset: SharedValue<{ tabIndex: number, y: number }>, // 탭 간 스크롤 동기화를 위한 값
}

🧠 핵심 로직 요약 (의사코드 기반 설명)

1. 리스트 스크롤 → scrollY 업데이트 → UI 반영

- 사용자가 리스트를 스크롤하면 onScroll 이벤트가 발생하고, 이 값을 scrollY에 저장합니다. 이 scrollY는 Reanimated의 useAnimatedStyle 등에서 직접 사용되어, 헤더 위치나 애니메이션을 실시간으로 제어하는 기준값이 됩니다.

// useCollapsibleTab
onScroll = (event) => {
  if (isSelectedTab) {
    scrollY.value = event.contentOffset.y; // SharedValue로 UI Thread에 실시간 반영
  }
}

2. 헤더 영역 제스처 → scrollTo로 강제 스크롤

- 헤더 영역을 Pan 하면 리스트 전체가 같이 움직이는 느낌을 주기 위해, 팬 이동 값을 기반으로 리스트 컴포넌트를 scrollTo로 강제 스크롤합니다. 이 과정은 UI Thread에서 수행되어 매우 부드럽습니다.

// usePanGesture
onUpdate = (e) => {
  panGestureScroller.value = startY.value - e.translationY;
}

// useDerivedValue 내부에서
if (isSelectedTab && panGestureScroller.value >= 0) {
  scrollTo(ref, 0, panGestureScroller.value);
}

3. 탭 간 동기화 → syncScroll 호출 → 다른 탭도 scrollTo

- 사용자가 탭을 전환했을 때, 이전 탭에서 사용하던 스크롤 위치를 현재 탭에 반영해 주기 위한 처리입니다. syncScroll() 함수는 syncScrollOffset 값을 업데이트하고, 다른 탭에서는 이 값을 감지해 자신에게 scrollTo()를 실행합니다.

// useSyncScroll
syncScroll = (tabIndex, y) => {
  syncScrollOffset.value = { tabIndex, y: Math.min(y, headerHeight) };
}

// useCollapsibleTab (다른 탭에서 감지)
if (syncScrollOffset.value.tabIndex !== tabIndex) {
  scrollTo(ref, 0, syncScrollOffset.value.y);
}

 


💡 최적화 팁

 

항목 내용
SharedValue JS ↔ UI 간 안전한 값 전달
Primitive 추천 객체 대신 숫자, 문자열 사용 시 업데이트 감지 안정성 ↑
scrollTo 사용 JS에서 직접 scrollY 조작 ❌ → scrollTo로 트리거
withDecay Android에서 부드러운 관성 스크롤 구현
runOnJS 상태 변경 알림 (ex. onCollapsibleStateChange)
 

✨ 적용 결과

  • 스크롤과 헤더 애니메이션의 지연 현상이 거의 사라짐
  • 탭 전환 시 자연스럽게 이전 위치로 스크롤
  • iOS/Android 모두에서 60FPS 수준 유지

🧷 결론

React Native에서 고성능 스크롤 인터랙션을 구현하기 위해
UI Thread를 간접 제어하는 구조는 필수입니다.
그 핵심 도구가 바로 Reanimated + SharedValue + scrollTo입니다.

CollapsibleTabView는 이 구조를 잘 구현한 대표 사례로,
성능 개선이 필요한 모든 "스크롤 + 탭 + 제스처" UI에 적용 가능한 강력한 패턴입니다.

 

🆚 기존 라이브러리와의 차별점

제가 구현한 CollapsibleTabView는 react-native-collapsible-tab-view와 동일한 목적을 가지지만, 실제 적용 과정에서 느낀 구조적 한계를 보완하고, 현실적인 문제 해결에 중점을 두고 설계되었습니다.

❌ 기존 라이브러리의 실질적인 한계

  • 스크롤과 터치 충돌: 헤더에 터치 가능한 요소(버튼, 필터 등)가 있을 경우, 터치와 스크롤이 충돌하여 pointerEvents를 각 요소에 직접 설정해야 함
  • 복잡한 헤더 구조 대응 불가: 헤더가 레이어드되고, 인터랙션이 복잡해질수록 이벤트 관리가 어려워지고, 유지보수가 점점 힘들어짐
  • 탭 간 스크롤 동기화 로직 불투명: 내부 동작은 있지만 커스터마이징이나 디버깅이 어려움
  • 확장성 부족: 헤더 자동 숨김, 스크롤 감도 조절, 탭바 위치 커스터마이징 등은 불가능하거나 제한적

✅ 우리가 만든 구조의 개선점

  • PanGesture 기반 통합 이벤트 처리: 터치와 스크롤을 동시에 하나의 제스처 그룹으로 관리. pointerEvents 설정 없이 헤더 내 모든 UI 요소에서 터치/스크롤 모두 동작
  • 명시적 상태 흐름 관리: scrollY, syncScrollOffset 등 핵심 상태를 SharedValue로 통일하여 디버깅 및 확장 용이
  • 모듈화된 구조: usePanGesture, useSyncScroll, useAutoHideHeader 등 기능 단위로 분리되어 재사용성과 유지보수성 우수
  • 고성능 UI 애니메이션: Reanimated 기반으로 네이티브 수준의 부드러운 전환과 FPS 확보

📦 잡담

현재구조는 실전 프로젝트에서 충분히 안정성과 유연성을 검증했는데요, 시간이 된다면 라이브러리로 만들고 싶습니다.. ㅎㅎ

내용만 주고 정리는 gpt 한테 부탁했는데.. 깔끔한 것 같기도하고 어색한 것 같기도 하네요