Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
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 31
Archives
Today
Total
관리 메뉴

부귀영화

React Navigation collapsible header 구현기 본문

개발/프로젝트

React Navigation collapsible header 구현기

Jinhoda 2022. 3. 9. 22:53

 

홈화면에서 '더보기'를 클릭하면 음식세트페이지(HomeFoodDetail), 레크세트페이지(HomeRecDetail)로 이동하는데, 디자이너분들의 요구사항은 상단에 뒤로가기, 로고, 기획서를 포함한 헤더 부분이 아래로 스크롤 함에 따라 높이가 줄어두는 것이었다

대략난감

미리 인지하고 있었다면 Header와 본문 Component를 나누지 않고 한 곳에서 처리해서 쉽게 구현했겠지만 이미 React Navigation에서 제공하는 createMaterialTopTabNavigator로 음식 / 레크를 나누는 TabBar를 구현해둔 상태였다. 이렇게 나뉘어진 상황에서 collapsible한 header를 구현하는 것이 어려울 것이라고 생각했던 이유는 스크롤이 발생하는 곳(HomeFoodDetail)과 높이가 줄어드는 애니메이션이 작동하는 곳(TabBar)이 달랐기 때문이다.

대략 이런 식이다

저 y축 변화값을 전달하기 위한 방법으로는

 

1. TabNavigator에서 offset_y 변수를 생성하고 HomeFoodDetail에서 스크롤 이벤트가 발생하면 navigation.setParams로 y값을 업데이트한다. 그리고 TabBar에 props로 정보를 전달해서 state.routes.params.offsetY 값을 받는다. 

 

2. 글로벌 저장소(redux)에 저장하고 TabBar에서 useSelector로 불러온다.

 

 

 

일단 먼저 1번의 방법으로 구현해보았다.

 

// navigation/Homes
const HomeTab = createMaterialTopTabNavigator();

function HomeTabs(navigation) {
  let offsetY_F = 0;
  let offsetY_R = 0;
  return (
    <HomeTab.Navigator
      initialRouteName={navigation.route.params.initialRoute}
      tabBar={props => <TabBar {...props} />}>
      <HomeTab.Screen
        name="HomeFoodDetail"
        component={FoodDetail}
        options={{title: '음식'}}
        initialParams={{offsetY: offsetY_F}}
      />
      <HomeTab.Screen
        name="HomeRecDetail"
        component={RecDetail}
        options={{title: '레크'}}
        initialParams={{offsetY: offsetY_R}}
      />
    </HomeTab.Navigator>
  );
}

Home Navigator를 반환하는 HomeTabs 함수에서 각 화면의 y offset을 나타내는 변수를 만들고 initialParams로 넘겨준다.

// screens/Main/Home/FoodDetail/HomeFoodDetailPresenter
  const navigation = useNavigation();

  const onScroll = e => {
    navigation.setParams({offsetY: e.nativeEvent.contentOffset.y});
  };

스크롤 시 setParams 함수로 y값을 업데이트한다.

// components/Header/TabBar

export default function TabBar({state, descriptors, navigation}) {
  const headerHeight = 58 * 2;
  let index = descriptors[state.routes[0].key].navigation.isFocused() ? 0 : 1;

  const scrollY = useRef(new Animated.Value(0));

  useEffect(() => {
    scrollY.current.setValue(state.routes[index].params.offsetY);
  }, [state]);

  const scrollYClamped = Animated.diffClamp(scrollY.current, 0, headerHeight, {
    useNativeDriver: true,
  });

  const translateY = scrollYClamped.interpolate({
    inputRange: [0, headerHeight],
    outputRange: [0, -(headerHeight / 2)],
  });

  const translateYNumber = useRef();

  translateY.addListener(({value}) => {
    translateYNumber.current = value;
  });

  ...
})

state로 넘어오는 offset Y 값을 받아 animated value로 사용한다.

 

별 문제 없어 보이고, 실제로 동작했지만 큰 문제가 생겼다. 

 

 

 

그것은 바로 매우매우 끊겨서 보인다는 점(...)

 

콘솔로 확인해보니

 

 LOG  0
 LOG  77.81818389892578
 LOG  642.5454711914062
 LOG  664.3636474609375
 LOG  719.272705078125

 

... 아무래도 이 방법은 안될 것 같다.

 

HomeFoodDetailPresenter -> Navigator -> TabBar로 데이터를 오가는데 많은 딜레이가 생기는 것으로 보인다.

 

그럼 다음으로는 redux를 사용하는 방법을 적용해보았다.

// store/actions/scrolls
export const UPDATE = 'UPDATE';

export const updateY = (screen, y) => {
  return {type: UPDATE, screen, y};
};
// store/reducers/scrolls
const initialState = {
  HomeFoodDetail: 0,
  HomeRecDetail: 0,
};

export default (state = initialState, action) => {
  switch (action.type) {
    case 'UPDATE': {
      const screen = action.screen;
      const y = action.y;
      return {...state, [screen]: y};
    }
    default:
      return state;
  }
};

우선  action과 reducer를 정의했다

  // HomeFoodsDetailPresenter
  const dispatch = useDispatch();

  const onScroll = e => {
    dispatch(updateY('HomeRecDetail', e.nativeEvent.contentOffset.y));
  };

각 화면에서는 setParams에서 dispatch로만 바꿔줬다

// components/Header/TabBar
const scrollY = useRef(new Animated.Value(0));
  const y = useSelector(state => state.scrolls[index]);

  useEffect(() => {
    scrollY.current.setValue(y);
  }, [y]);

TabBar에서도 useSelector로 받아오도록 변경했다.

 

그 결과는

 

 

이정도면 성공... 확실히 이전에 비해 뚝뚝 끊기거나 위 아래로 왔다갔다하는 경우가 줄어들었다

 

이걸 구현한건 2달 전이지만 지금에야 수정했다(....)

 

그리고 비슷한 경우가 있었나 검색해봤는데

https://github.com/react-navigation/react-navigation/issues/4786

 

Performance issues and jumpy animations on Android when using setParams to pass Animated Value from one component to another. ·

Please help me out, I've been stuck with this problem for the last 4-5 days. The problem: https://gfycat.com/CelebratedVapidBadger This is my MainTab1.js component which holds the ScrollView wi...

github.com

 

음.. 확실히 collpasible header를 구현하는데에는 적절하지 않았던것 같다.

 

아니면 애초에....

 

 

HomeFoodDetail 안에 Header, TabBar, Content 들어가도록 구조를 바꾸는게 제일 좋겠지만 각 컴포넌트들을 아릅답게 분리해놓았는데 다시 합치기 싫었다(=심술났다) 

 

이상으로 Mount 앱의 Collapsible Header 구현기였다

 

// 04.07 수정 //

 

문제의 원인은 local / global state의 문제 따위가 아니라 단순히 useNativeDriver를 사용하지 않아 발생하는 문제였다.

 

function HomeTabs(navigation) {
  const scrollY = useRef(new Animated.Value(0));

  return (
    <HomeTab.Navigator
      initialRouteName={navigation.route.params.initialRoute}
      tabBar={props => <TabBar scrollY={scrollY} {...props} />}>
      <HomeTab.Screen
        name="HomeFoodDetail"
        component={() => <FoodDetail scrollY={scrollY} />}
        options={{title: '음식'}}
      />
      <HomeTab.Screen
        name="HomeRecDetail"
        component={() => <RecDetail scrollY={scrollY} />}
        options={{title: '레크'}}
      />
    </HomeTab.Navigator>
  );
}

HomeTab에서는 위와 같이 Prop Drilling으로 scrollY를 꽂아주고 

onScroll={Animated.event([
  {
    nativeEvent: {
      contentOffset: {
        y: scrollY.current,
      },
    },
  },
])}

onScoll을 다음과 같이 변경해서 해결할 수 있었다

 

혼자서 문제를 찾고, 해결하는 것은 외롭다😭

'개발 > 프로젝트' 카테고리의 다른 글

Chimha 배포 완료  (0) 2022.03.24
App Store 4.1.0 Design: Copycats  (0) 2022.03.18
Chimha 프로젝트 회고 + 문제 해결 경험..  (0) 2022.03.17
Mount 프로젝트 회고  (2) 2022.03.07