블로그 목록

React Native 바텀 시트 제대로 만들기 — @gorhom/bottom-sheet

앱에서 바텀 시트는 생각보다 자주 필요하다. 필터, 상세 옵션, 공유 메뉴... 아래에서 스윽 올라오는 그 시트 말이다. 처음엔 ModalAnimated로 대충 흉내를 냈는데, 드래그로 닫기, 중간 높이에서 멈추기(스냅), 키보드 대응까지 붙이려니 코드가 금방 지저분해졌다.

그러다 @gorhom/bottom-sheet로 갈아탔다. Reanimated 기반이라 애니메이션이 JS 스레드 안 타고 부드럽고, 스냅 포인트나 제스처 처리를 라이브러리가 다 해준다.

설치

Reanimated와 gesture-handler에 의존한다. Expo면 이렇게.

npx expo install @gorhom/bottom-sheet react-native-reanimated react-native-gesture-handler

앱 최상단을 GestureHandlerRootView로 감싸야 제스처가 먹는다. 이거 빼먹으면 시트가 열리긴 하는데 드래그가 안 돼서 한참 헤맨다.

// App.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <RootNavigator />
    </GestureHandlerRootView>
  );
}

기본 사용

BottomSheetModal을 쓰면 화면 어디서든 ref로 열고 닫을 수 있다. snapPoints는 시트가 멈출 높이 목록이다. 퍼센트로 주는 게 편하다.

import { useRef, useMemo, useCallback } from 'react';
import { View, Text, Button } from 'react-native';
import {
  BottomSheetModal,
  BottomSheetView,
  BottomSheetModalProvider,
} from '@gorhom/bottom-sheet';

function FilterScreen() {
  const sheetRef = useRef<BottomSheetModal>(null);
  const snapPoints = useMemo(() => ['30%', '60%'], []);

  const open = useCallback(() => sheetRef.current?.present(), []);

  return (
    <View style={{ flex: 1 }}>
      <Button title="필터 열기" onPress={open} />

      <BottomSheetModal ref={sheetRef} snapPoints={snapPoints}>
        <BottomSheetView style={{ padding: 24 }}>
          <Text style={{ fontSize: 18 }}>정렬 기준</Text>
        </BottomSheetView>
      </BottomSheetModal>
    </View>
  );
}

BottomSheetModal을 쓸 거면 앱 상단을 BottomSheetModalProvider로도 감싸줘야 한다. present()로 열고 dismiss()로 닫는다. 뒤 배경을 어둡게 깔고 싶으면 backdropComponentBottomSheetBackdrop을 넘기면 된다.

스크롤이 필요하면

시트 안에 긴 목록을 넣을 땐 그냥 FlatList를 쓰면 스크롤과 시트 드래그가 서로 싸운다. 라이브러리가 감싼 BottomSheetFlatList, BottomSheetScrollView를 쓰면 위로 스크롤이 끝났을 때 자연스럽게 시트 드래그로 넘어간다.

import { BottomSheetFlatList } from '@gorhom/bottom-sheet';

<BottomSheetFlatList
  data={options}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Row {...item} />}
/>

정리

Modal로 직접 만들 때 제일 골치 아팠던 게 드래그로 닫기와 스냅 처리였는데, 이 라이브러리는 그걸 기본으로 준다. GestureHandlerRootView로 감싸는 것, 그리고 시트 안 스크롤은 전용 컴포넌트를 쓴다는 것 — 이 두 개만 안 빼먹으면 세팅은 10분이면 끝난다.