일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- 이펙티브 타입스크립트
- sharedvalue
- 정처기 자격
- typeScript
- 속도개선
- js
- 정보처리기사
- TS
- 불변식
- 빌딩 블록
- react natvie
- DDD
- 비동기
- 타입스크립트
- 정처기 준비물
- Expo
- in app purchase
- React Native
- HTML
- nextJS
- 애그리게이트
- IAP
- Aggregate
- BOUNDED CONTEXT
- collapsibletabview
- expo updates
- ui thread
- rniap
- rn
- 코드 푸시
- Today
- Total
nika-blog
React-Native: CollapsibleTabView Reanimated 기반 성능 최적화 본문
이 글은 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 한테 부탁했는데.. 깔끔한 것 같기도하고 어색한 것 같기도 하네요
'React Native' 카테고리의 다른 글
React-Native: CodePush에서 EAS Update로의 마이그레이션 (4) | 2025.07.11 |
---|---|
React Native에서 In-App Purchase 구현하기 (0) | 2025.02.06 |
RNIap subscription promotion, ios introductory offer (인앱 결제 이상한 ios 프로모션 정책) React Native (2) | 2025.01.31 |
global service - in app purchase(iap) currency issue, react-native (0) | 2025.01.11 |