컨테이너(container)는 바텀시트가 그 안에서 위아래로 움직이는 "무대(영역)"다. 시트 자체가 아니라, 시트를 담고 있으면서 시트가 차지할 수 있는 공간의 범위를 정의하는 바깥 껍데기라고 보면 된다.
바텀시트는 허공에서 슬라이드하는 게 아니라 반드시 어떤 영역 안에서 움직인다. 그 영역이 컨테이너다. 컨테이너의 크기(height)와 화면 가장자리로부터의 여백(offset)이 정해져야 "시트가 어디까지 올라올 수 있는지"를 계산할 수 있다.
바텀시트는 여러 겹으로 쌓인 구조다. 바깥에서 안쪽 순서로 보면 이렇다.
┌─────────────────────────────────┐
│ Container (무대) │ ← 시트가 움직일 수 있는 전체 영역
│ ┌───────────────────────────┐ │
│ │ Backdrop (뒷배경/딤) │ │ ← 뒤를 어둡게 덮는 레이어 (선택)
│ └───────────────────────────┘ │
│ │
│ ┌───────────────────────────┐ │
│ │ Sheet (시트 본체) │ │ ← 실제로 올라오는 패널
│ │ ┌─────────────────────┐ │ │
│ │ │ Handle (손잡이) │ │ │ ← 위쪽 그립 바
│ │ ├─────────────────────┤ │ │
│ │ │ Content (콘텐츠) │ │ │ ← 안에 넣은 내용
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
여기서 핵심은 컨테이너와 시트는 서로 다른 것이라는 점이다. 컨테이너는 고정된 무대이고, 시트는 그 무대 안에서 오르내리는 패널이다. 사람들이 흔히 "바텀시트"라고 부르는 건 보통 시트 본체지만, 위치 계산의 기준이 되는 건 컨테이너다.
개념일 뿐 아니라 라이브러리 내부에 BottomSheetContainer라는 실제 컴포넌트로 존재한다. BottomSheetBackdrop, BottomSheetBackgroundContainer 등과 함께 시트를 구성하는 내부 부품 중 하나다.
이 컨테이너 컴포넌트가 하는 일은 크게 두 가지다.
onLayout 기반 측정)세 가지를 구분하면 개념이 선명해진다.
| 구분 | 역할 | 비유 |
|---|---|---|
| Container | 시트가 움직일 수 있는 전체 영역 | 무대 |
| Sheet | 실제로 오르내리는 패널 | 배우 |
| Content | 시트 안에 넣은 내용 | 배우가 든 소품 |
배우(시트)의 위치는 항상 무대(컨테이너)를 기준으로 잡힌다. snapPoints={['50%']}의 50%는 화면이 아니라 컨테이너 높이의 50%다. 무대가 커지면 같은 "절반 지점"도 실제로는 달라지는 것이다.
BottomSheet의 컨테이너기본 BottomSheet는 컨테이너가 자신이 놓인 부모 뷰 안에 렌더된다. 즉 부모 뷰의 영역이 곧 무대가 된다.
그래서 부모 뷰가 화면 전체를 차지하지 않으면, 시트도 그 좁은 영역 안에서만 움직인다. 부모가 화면의 절반짜리 뷰라면 바텀시트도 그 절반 안에 갇혀 어색하게 보일 수 있다. 일반 BottomSheet를 쓸 때는 대개 flex: 1로 화면을 꽉 채우는 부모 안에 두는 이유가 여기에 있다.
// 컨테이너 = 이 부모 View의 영역
<View style={{ flex: 1 }}>
<BottomSheet index={1} snapPoints={['50%', '90%']}>
<BottomSheetView>{/* content */}</BottomSheetView>
</BottomSheet>
</View>
BottomSheetModal의 컨테이너BottomSheetModal은 컨테이너가 @gorhom/portal로 감싸져 앱의 최상단에 렌더된다. 컴포넌트 트리상 어디에 선언하든, 실제로는 모든 화면 위를 덮는 별도의 무대로 올라간다.
이 덕분에 모달 시트는 부모 뷰의 크기나 위치에 갇히지 않고 항상 화면 전체 위에 얹힌다. 대신 이 포털을 받아줄 BottomSheetModalProvider를 앱 상단에 두어야 한다.
// Provider가 포털 호스트를 제공 → 컨테이너가 최상단에 렌더됨
<BottomSheetModalProvider>
<BottomSheetModal ref={ref} snapPoints={['50%']}>
<BottomSheetView>{/* content */}</BottomSheetView>
</BottomSheetModal>
</BottomSheetModalProvider>
두 방식의 컨테이너 차이를 정리하면 이렇다.
| 컨테이너가 렌더되는 위치 | 무대의 크기 | |
|---|---|---|
BottomSheet | 부모 뷰 안 | 부모 뷰 영역 |
BottomSheetModal | 포털을 통해 앱 최상단 | 화면 전체 |
컨테이너가 중요한 이유는, 시트의 모든 위치가 화면 절대 좌표가 아니라 컨테이너를 기준으로 계산되기 때문이다.
'50%', '90%' 같은 값이 컨테이너 height를 기준으로 픽셀 위치로 바뀐다.offset(top/left/right/bottom)으로 상태바·노치·제스처바를 피한다.이 "컨테이너의 크기와 여백" 정보가 바로 containerLayoutState(= { height, offset })다. 값을 직접 넘기지 않으면 컨테이너 컴포넌트가 onLayout으로 스스로 측정하는데, 이때 리렌더가 한 번 더 발생한다. 컨테이너 크기를 미리 알고 있다면 값을 주입해 이 측정 과정을 건너뛸 수 있다.
SharedValue로 관리되어 UI 스레드에서 소비된다. New Architecture의 동기 레이아웃 흐름과 잘 맞는 구조다.react-navigation의 네이티브 스택 모달 등과 겹칠 때 스택 컨텍스트(z-순서) 문제가 생길 수 있다. Provider와 GestureHandlerRootView의 배치 위치를 함께 점검하는 것이 좋다.offset은 react-native-safe-area-context의 인셋과 연결하면, 노치·제스처 바 영역까지 정확히 반영된 무대를 얻을 수 있다.BottomSheetContainer라는 실제 컴포넌트로 존재한다.BottomSheet는 부모 뷰 안에, BottomSheetModal은 포털을 통해 앱 최상단에 컨테이너를 렌더한다.height)와 여백(offset)이 곧 containerLayoutState이며, 시트의 모든 snap 위치가 이 값을 기준으로 계산된다.