React-Navigation에서 header와 component의 상태를 공유하는 방법

Derhon·2023년 4월 2일
1
post-thumbnail

사이드 프로젝트 개발 도중, 한가지 문제에 직면했다.
현재 프로젝트에서는 React-Native-Navigation을 통해 스크린 스택을 관리하고 있는데, 기본 props인 header에 커스텀 헤더를 적용하며 내부 컴포넌트와 상태를 공유해야하는 문제였다.

말이 굉장히 복잡한데, 그림으로 보면 한 번에 이해가 된다.


아직 출시 전이라 세부 콘텐츠는 가렸다.

...
		<RootStack.Screen
          name="FolderDetailCollapse"
          component={FolderDetailCollapse}
          options={{
            headerShown: true,
            header: (props) => {
              return (
                <IconBar
                  curNum={1}
                  maxNum={15}
                  onBack={() => props.navigation.pop()}
                  icon={Icon.fold}
                  onPress={() =>
                    props.navigation.navigate("FolderDetailGlance", { id: 0 })
                  }
                  onDots={() => null}
                  bookmarked={true}
                  onBookmark={() => null}
                  id={0}
                />
              );
            },
          }}
        />
      </RootStack.Navigator>
...

이런 경우에 스택 스크린의 prop으로 들어가는 headercomponent가 분리되어 있는데, NavigationContainer를 담는 파일 하나 안에서 위와 같은 경우의 상태를 모두 선언하고 props로 넘겨준다면...
당장의 문제 해결은 되겠지만 추후에 프로덕트를 유지보수하는 과정에서 대갈휘가 깨질게 분명했다.
우선 깨끗한 코드를 만들자는 나의 신념과도 거리가 먼 짓이라, 이를 어떻게 해결할까에 대한 고민이 시작되었다.

❌ Wrapper에서 상태 선언

위에서 언급한 방법이다.

NavigationContainer를 담는 파일에서 관리해야하는 상태를 모두 선언하고 커스텀 헤더와 내부 콘텐트에 상태를 drilling하는 방법이다.

물론 가장 직관적이고 접근하기도 쉬운 방법이지만, 이미 관리해야하는 스크린이 10개 이상으로 넘어간 시점에서 모든 상태를 하나의 파일에서 관리하고 spread하는 방식은 옳지 못하다 판단했다.

기각!

🤔 전역 상태 관리 활용

본 프로젝트에서는 상태를 recoil로 관리하고 있다.
헤더에서 공유하는 상태를 atom으로 만든 후, 이를 관리하는 방법이 있을 것이다.

상태를 prop으로 던져주는 위의 방식보다는 낫겠지만, 사용자가 해당 스크린에서 벗어날 때마다 상태를 초기화하는 과정을 하나하나 가져오는 것이 과연 가능할까라는 생각이 들었다.

어쩌면 나는 늑대를 피하려고 호랑이한테 가는게 아닐까... 라는 생각이 들었다.

✅ 전역 상태 선언

우선 기존에 params로 전달받던 데이터 중 내부 컴포넌트와 공유하는 상태를 전역으로 선언했다.

import { atom } from "recoil";

interface HeaderProps {
  id: number;
  curNum: number;
  maxNum: number;
  bookmarked: boolean;
  modal: boolean;
}

const DEFAULT_VALUE: HeaderProps = {
  id: 0,
  curNum: 0,
  maxNum: 0,
  bookmarked: false,
  modal: false,
};

export const iconHeaderState = atom<HeaderProps>({
  key: "headerState",
  default: DEFAULT_VALUE,
});

상태 값은 자유롭게 설정한다.

import { StackScreenProps } from "@react-navigation/stack";
import { RootStackParamList } from "@interfaces/RootStackParamList";
import { useRecoilState } from "recoil";
import { iconHeaderState } from "@recoil/iconHeaderState";
import { useEffect } from "react";

export type Props = StackScreenProps<
  RootStackParamList,
  "FolderDetailCollapse"
>;

/**
 * 폴더 상세페이지 -> 하나씩 보기
 */
const FolderDetailCollapse = ({ navigation, route }: Props) => {
  /**데이터 받아오기 */
  const FOLDER_ID = route.params.id;
  const [headerState, setHeaderState] = useRecoilState(iconHeaderState);

  useEffect(() => {
    /**헤더 값 초기화 */
    setHeaderState({
      id: FOLDER_ID,
      curNum: 1,
      maxNum: 15,
      bookmarked: true,
      modal: false,
    });
  }, []);

  return <></>;
};

export default FolderDetailCollapse;

아직 API 연동을 하지 않았기에, 최초 렌더링시 초기 값을 더미로 설정하였다.
내부 컴포넌트가 렌더링이 일어날 때 헤더에 연동된 전역 상태가 모두 최신화된다.

사실 여기까지만 해도, 다른 컴포넌트에서 해당 상태에 접근할 때 초기화 되기 때문에 문제가 되지 않는다.

✅ 커스텀 훅

하지만 날 괴롭히는 귀차니즘

ract-navigation에서 스크린이 focus되고 blur 되는 이벤트를 감지할 수 있는 hook이 존재한다.

mport { useFocusEffect } from '@react-navigation/native';

function Profile({ userId }) {
  const [user, setUser] = React.useState(null);

  useFocusEffect(
    React.useCallback(() => {
      const unsubscribe = API.subscribe(userId, user => setUser(user));

      return () => unsubscribe();
    }, [userId])
  );

  return <ProfileContent user={user} />;
}

useFocusEffect 공식 문서

위의 useFocusEffect를 이용하여 스크린이 blur될 때 데이터를 초기화하고, focus될 때 업데이트하는 커스텀 훅을 제작하였다.

import { useRecoilState } from "recoil";
import { iconHeaderState, DEFAULT_VALUE } from "@recoil/iconHeaderState";
import { useCallback } from "react";
import { useFocusEffect } from "@react-navigation/native";

export function useHeader(id: number) {
  const [headerState, setHeaderState] = useRecoilState(iconHeaderState);

  useFocusEffect(
    useCallback(() => {
      /**헤더 값 초기화
       * api연동함수 추가
       */
      setHeaderState({
        id: id,
        curNum: 1,
        maxNum: 15,
        bookmarked: true,
        modal: false,
      });
      return () => {
        setHeaderState(DEFAULT_VALUE);
      };
    }, [])
  );

  return headerState;
}

API 연동이 되지 않은 상태지만, 저기서 axios를 활용한 비동기 함수 하나만 추가하면 된다.

이렇게 반복적인 선언 없이 헤더에서 상태를 공유할 수 있는 훅이 완성되었다!

👏 마치며

리액트 네이티브는 참 매력적이다.
라이브러리에 의존성이 너무 심한 것 같다 싶다가도... 코틀린 개발 때려치우고 웹으로 넘어온 시점에서... 이렇게 편하게 앱 개발할 수 있는 플랫폼이 또 있을까 싶다.
그나마 PWA정도 일듯..? 플러터도 선언형 UI가 힘들다.

참고자료.. 챗 지피티...

profile
🧑‍🚀 이사했어요 ⮕ https://99uulog.tistory.com/

0개의 댓글