사이드 프로젝트 개발 도중, 한가지 문제에 직면했다.
현재 프로젝트에서는 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으로 들어가는 header
와 component
가 분리되어 있는데, NavigationContainer
를 담는 파일 하나 안에서 위와 같은 경우의 상태를 모두 선언하고 props
로 넘겨준다면...
당장의 문제 해결은 되겠지만 추후에 프로덕트를 유지보수하는 과정에서 대갈휘가 깨질게 분명했다.
우선 깨끗한 코드를 만들자는 나의 신념과도 거리가 먼 짓이라, 이를 어떻게 해결할까에 대한 고민이 시작되었다.
위에서 언급한 방법이다.
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
를 이용하여 스크린이 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가 힘들다.
참고자료.. 챗 지피티...