회사에서 진행하는 Kotlin 앱을 React Native로 컨버팅 하는 프로젝트 진행 중 기획 단계가 끝나고 어떤 라이브러리를 사용할지 설계 중이었다.
기존 앱의 메인 페이지에는 상단에 드래그 가능한 Top Sheet가 구현되어 있는데, 열심히 찾아보아도 RN에선 Botton Sheet 라이브러리만 있을 뿐 원하는 Top Sheet 라이브러리는 없었다.
# 가장 많이 사용하는 Bottom Sheet 라이브러리
https://ui.gorhom.dev/components/bottom-sheet/
React Native Bottom Sheet | Gorhom UI
A performant interactive bottom sheet with fully configurable options 🚀
ui.gorhom.dev
1. 요구사항
1. top sheet가 보이는 크기는 총 2가지 (ex) 100dp, 200dp)
- 기본 높이 100dp, 확장 시 200dp
2. 하단 부분 화살표 터치 시 확장, 축소
- 드래그뿐만 아니라 하단 화살표를 터치하면 보이는 top sheet 부분이 변해야 함
3. 위/아래로 드래그 가능
- 최소/최대 높이까지 드래그 가능
4. 드래그 도중 놓을 시 드래그 방향의 끝에서 정지
- 위로 쓸어 올리면 축소, 아래로 쓸어내리면 확장
5. 확장/축소 시 보이는 컨텐츠의 opacity 변경
- 컨텐츠의 opacity를 수정하여 자연스럽게 적용
2. 라이브러리 선정
기존 라이브러리의 소스를 직접 수정하여 수직 값을 변경하려 했지만 코드를 보니 너무 많은 부분에서 연결되어 있기에 시간이 오래 걸린다고 판단했다.
그래서 결국 직접 구현하기로 하였고 총 2개의 라이브러리를 사용했다.
1. react-native-gesture-handler
- top sheet를 위/아래로 드래그하기 위해 사용
2. react-native-reanimated
- 위/아래로 드래그 시 애니메이션 효과를 주기 위해 사용
1. react-native-gesture-handler
https://docs.swmansion.com/react-native-gesture-handler/
React Native Gesture Handler | React Native Gesture Handler
Declarative API exposing platform native touch and gesture system to React Native.
docs.swmansion.com
2. react-native-reanimated
https://docs.swmansion.com/react-native-reanimated/
React Native Reanimated
A powerful animation library that makes it easy to create smooth animations and interactions that run in the UI thread.
docs.swmansion.com
3. 구현 방법
첫 번째 top sheet 컴포넌트의 height는 최대 높이로 고정한 뒤 margin top의 값을 수정하여 화면에 보이는 부분을 조절하는 방법, 두 번째 top sheet 컴포넌트의 height가 최소 높이에서 최대 높이로 증가하는 방법
두 방법 모두 보이는 높이를 100dp / 200dp로 가정한다면
1. 첫 번째 방법
- 고정 값 : top sheet height : 200dp
- 변경 값 : top sheet margin top : -100dp ↔ 0dp
2. 두 번째 방법
- 고정 값 : 없음
- 변경 값 : top sheet height : 100dp ↔ 200dp
3-1. margin
top sheet의 높이를 고정하고 margin을 이용한 방법으로 구현했다.
왼쪽부터 버튼 클릭, 스와이프, 드래그 기능을 사용한 것인데, 세 번째 드래그 시 top sheet가 손가락 위치에 따라오지 않고 위아래로 흔들리게 되었다.
// 최대 margin (축소)
const minMarginTop = -(height / 1.5 - height / 6.5);
// 최소 margin (확장)
const maxMarginTop = 0;
const marginTop = useSharedValue(minMarginTop);
const context = useSharedValue({y: 0});
// 변경된 margin 값 style 적용
const animatedStyles = useAnimatedStyle(() => {
return {
marginTop: marginTop.value,
};
});
const gesture = Gesture.Pan()
.onStart(() => {
context.value = {y: marginTop.value};
})
// 최소/최대값 범위에서 margin 업데이트
.onUpdate(e => {
const newValue = e.translationY + context.value.y;
marginTop.value = Math.min(
Math.max(newValue, minMarginTop),
maxMarginTop,
);
})
현재 사용하는 style 단위가 Dimensions을 이용한 뷰포트인데, context.value와 gesture 라이브러리의 translateY 값을 합하여 margin.value를 업데이트하는 방식에서 문제가 발생하는 것 같다.
gesture 라이브러리에서 사용하는 translateY 값은 기본적으로 px에서 dp로 변환된다.
Dimensions에서 가져온 window의 height 비율에 맞춰 value를 업데이트해보려 했지만, 자연스러운 움직임은 되지 않았다.
3-2. height
두 번째는 top sheet의 높이를 직접 변경하는 방법으로 구현했다.
결과적으로 드래그 시에도 손가락 위치에 자연스럽게 따라오게 구현했다.
const minSheetHeight = height / 6.5;
const maxSheetHeight = height / 1.5;
.onUpdate((e) => {
const newValue = e.translationY + context.value.y;
sheetHeight.value = Math.min(
Math.max(newValue, minSheetHeight),
maxSheetHeight,
);
})
하지만 로직이 바뀌거나 다른 단위를 사용한 것은 아니고 단순히 margin 값을 height로 변경한 것이다.
자세히 알 수는 없지만 gesture 라이브러리에서 margin과 height style을 적용할 때 useAnimatedStyle와 reanimated 라이브러리의 useSharedValue를 같이 사용하는 방식에서 차이가 발생하는 것으로 유추할 뿐이다... 😥
React Native Top Sheet 직접 구현 #2
지난번에 gesture handler, reanimated 라이브러리를 사용해 top sheet를 직접 구현해 보았는데, 오늘은 top sheet가 확장/축소 됐을 때 다른 컨텐츠를 보여주고 하단에 자연스럽게 스크롤이 생기게 적용했다
nonmajor-be-developer.tistory.com
https://github.com/yeonhub/react-native-top-sheet_example
GitHub - yeonhub/react-native-top-sheet_example
Contribute to yeonhub/react-native-top-sheet_example development by creating an account on GitHub.
github.com