react native navigation을 사용하다보면 생각보다 type에 엄격하다는 사실을 알 수 있다. 대표적으로 generic 없이 그냥 useNaivgation을 사용하면 screenName의 타입을 never로 잡아버려서 엉뚱한 타입에러가 발생하는 케이스를 들 수 있다.
const navigation = useNavigation();
useEffect(() => {
navigation.navigate(SOME_SCREEN_NAME);
// Argument of type 'string' is not assignable to parameter of type 'never'.
}, []);
이런 타입 에러를 해결하는 정석적인 방법은 useNavigation에 제네릭으로 navigation type을 넘겨주는 것이다.
export type StackParamList = {
Home: undefined;
SomeScreen: {id:string}
};
const navigation = useNavigation<
NativeStackNavigationProp<StackParamList>
>();
하지만 매번 제네릭으로 타입을 넘겨주는게 너무 귀찮았던 나는 type casting으로 빨간 줄을 없애버리곤 했다.
navigation.navigate(SOME_SCREEN_NAME as never)
처음에는 꽤나 괜찮았다. 쌓여가는 as never가 거슬렸지만 그래도 애써 못본 척 넘어갔다. 하지만 스크린이 늘어감에 따라 ts의 최대 장점인 자동완성을 사용할 수 없다는 점이 점점 크게 다가오는 시점이 왔다. 스크린은 늘어나고 스크린의 params가 다양화될 수록 이대로는 안되겠다는 생각이 들었고, 이참에 크게 한 번 귀찮음을 감수해보자고 마음먹었다.
다짐은 중첩 네비게이션 구조로 되어 있기 때문에 root stack 하위에 여러 네비게이터들이 속해있다. 중첩 네비게이션 구조에서 다른 네비게이터의 스크린으로 정확하게 접근하기 위해서는 다음과 같이 navigate해야 한다.
navigation.navigate(SOME_STACK,{screen:SOME_SCREEN,params:{id:'1'}})
이런 형태의 params(navigate 함수의 두번 째 인자)를 구현하기 위해 NavigationPropType이라는 이름으로 타입을 만들어줬다.
export type ValueOf<T> = T[keyof T];
export type NavigationPropType<T> = {
screen?: keyof T;
params?: ValueOf<T>;
};
갑자기 뜬금없이 NavigationPropType을 만들어준 이유는 navigation에 TS를 적용하기 위해서는 ParamList type을 정의해줘야하기 때문이다. NavigationPropType을 활용하면 아래 예시 코드처럼 중첩 네비게이션 구조에서 params type을 정의할 때 매우 편리하다.
export type RootStackParamList = {
MainTab: undefined;
SignStack?: NavigationPropType<SignStackParamList>;
GymDetail: {gymId: string;};
MyPoint: undefined;
MyCoupon: undefined;
MyFavorite: undefined;
MyMembership?: {membershipId?: string};
...
};
const Stack = createNativeStackNavigator<RootStackParamList>();
가장 먼저 Navigator마다 각각 어떤 스크린이 있고, 어떤 params를 받는지 정의된 ParamList 타입을 선언해야 한다. 작은 꿀팁을 주자면, 이렇게 선언한 ParamList 타입을 createNativeStackNavigator에 제네릭으로 넘겨주면 타입스크립트가 ParamList 대로 네비게이터를 정의했는지 검사해주기 때문에 에러를 방지할 수 있다.
useNavigation을 사용할 때 컴포넌트 내부에서 매번 타입을 제네릭으로 넘겨주는 과정을 생략하고자, useDgNavigation이라는 custom hook을 만들었다.
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
const useDgNavigation = () =>{
const navigation = useNavigation<
NativeStackNavigationProp<SharedStackParamList> &
NativeStackNavigationProp<RootStackParamList> &
NativeStackNavigationProp<SignStackParamList> &
NativeStackNavigationProp<SearchStackParamList> &
NativeStackNavigationProp<CartStackParamList> &
NativeStackNavigationProp<ChatStackParamList> &
NativeStackNavigationProp<MapStackParamList> &
NativeStackNavigationProp<ModalPresentationParamList> &
BottomTabNavigationProp<MainTabParamList>
>();
return {navigation}
}
이제 실제 컴포넌트에서는 useDgNavigation으로 navigation을 가져와 사용하기만 하면 되기 때문에 내 귀차니즘은 말끔히 해결됐다.
const {navigation} = useDgNavigation()
navigation.navigate(SOME_SCREEN)
여담으로 사실 실제 다짐 코드에는 useDgNavigation안에 goBackRootNavigator라는 함수가 하나 더 존재한다.
const useDgNavigation = () =>{
const {rootStackNavigation} = useRootStackNavigation();
...
const goBackRootNavigator = useCallback(() => {
rootStackNavigation
? rootStackNavigation?.goBack()
: navigation.navigate(SCREEN_NAMES.MAIN_TAB);
}, [rootStackNavigation, navigation]);
return {navigation, goBackRootNavigator}
}
중첩 네비게이션 구조로 짜여진 경우 하위 navigator에서 useNavigation으로 가져오는 navigation은 가장 가까운 navigation을 가져오게 된다.
const SignStackNavigator = () =>{
const navigation = useNavigation() // RootStack의 navigation을 가져온다.
}
const SignIn = () =>{
const navigation = useNavigation() // SignStack의 navigation을 가져온다.
}
하지만 경우에 따라 중첩 네비게이션에 속한 스크린에서 root stack navigation을 핸들링해야하는 소요가 생길 수 있으므로, rootStackNavigation 객체를 전역 상태로 관리하고 있다.
const SignStackNavigator = () =>{
const navigation = useNavigation()
const setRootStackNavigation = useRootStackNavigation(state=>state.setRootStackNavigation)
useEffect(()=>{
setRootStackNavigation(navigation)
},[])
...
}
그리고 goBackRootNavigator가 바로 rootStackNavigation의 첫 번째 소요다. goBackRootNavigator는 말 그대로 root stack navigator를 기준으로 뒤로가기를 호출해주는 역할을 수행한다. 이 함수는 주로 현재 활성화 되어 있는 하위 navigator를 즉시 닫을 때 사용된다. root stack 입장에서는 하위에 속한 nested navigator들을 모두 screen으로 취급하므로, 뒤로가기 호출 시 하위 navigator의 현재 스크린 스택과 무관하게 즉시 navigator를 닫아버릴 수 있기 때문이다.
goBackRootNavigator는 대표적으로 로그인 이후 SignStackNavigator를 닫을 때 사용되고 있다. 로그인이 완료되면 SignStackNavigator에 screen stack이 몇개가 쌓여있든지, 즉시 stack navigator를 종료하고 이전 플로우로 돌아가야 하기 때문이다. 로그인하기 페이지에서 바로 로그인이 됐다면 navigation.goBack으로 충분하겠지만, 만약 회원가입 완료 결과로 로그인 됐을 때는 navigation.goBack을 몇번 호출해야 할까? 만약 나중에 비밀번호 찾기 페이지에서 바로 로그인 시켜버리는 기획이 등장하면 어떻게 해야할까? 이런 다양한 케이스에 대응하고자, 하위 navigator의 screen stack이 몇개인지와 무관하게 root navigator 단에서 goBack을 실행해 하위 navigator를 즉시 닫아버리는 기능을 구현했다.
안녕하세요,
useNavigation 을 위와 같이 custom hook으로 래핑 하려고 개발 중 모르는 부분이 있어서 찾다가 들어왔습니다.
export type RootStackParamList = {
Login: { page: string; data?: object } | undefined;
Foo: undefined;
Bar: undefined;
};
와 같을 경우, navigate 함수를 만들기 위해
const navigate = (
page: keyof RootStackParamList,
params: RootStackParamList[keyof RootStackParamList] = undefined,
) => {navigation.navigate(page, params) }
를 하게되면 params의 타입이 잡히지 않아서( page의 타입이 undefined인지 {~~}인지 ) 타입에러가 나고 있는데 이럴 땐 어떻게 해야 하나요?