1. 문제 상황
탭은 페이지보다는 더 부드럽게 전환되기를 사용자 입장에서 기대하는데, 위 영상을 보면 탭을 이동할 때마다 화면이 깜빡이는 것을 볼 수 있다. 이는 좋지 않은 사용자 경험을 제공한다고 생각하였다.
2. 해결 과정
그래서, 이를 reacat-navigation/bottom-tabs의 BottomTobBar로 구현해보고자 한다.
📚 Bottom Tab Navigator 다이브
react-navigation/bottom-tabs라이브러리를 사용하여 구현 가능하다.
- Bottom Tab Navigator : 화면 아래에 탭을 사용하여 탐색하는 방법
- ↔ Stack Navigation은 화면 위에 새로운 화면을 쌓아서 탐색하는 방법
- 함수
createBottomTabNavigator : Tab Navigator를 만드는데 사용한다.
const Tab = createBottomTabNavigator(); function MyTabs() { return ( <Tab.Navigator> <Tab.Screen name="Home" component={HomeScreen} /> <Tab.Screen name="Settings" component={SettingsScreen} /> </Tab.Navigator> ); }
- props : id, initialRouteName, screenOptions, detachInactiveScreens ..
- options : title, tabBarLabel, tabBarShowLabel, lazy, unmountOnBlur, freeszeOnBlur
TabBar를 보여주는 페이지가 있고, 보여주지 않아야 할 페이지가 있다. TobBar를 보여줄 페이지는 위 영상처럼 4개의 페이지(각 탭의 메인)에서이다. 그리고 그 탭에서 이동한 페이지이거나, 로그인 이전에는 보여주지 말아야 한다.
그래서 이 둘을 구분하고자 기존에 TabBar가 없는 컴포넌트를 기존 명칭인 WebViewContainer그대로 사용하고, TabBar를 보여줄 컴포넌트를 TabBarNavigatior라고 하였다.
// TabBarNavigator.tsx
export const WEBVIEW_SCREENS = [
{name: 'Matching', url: SOURCE_URL + '/grouping'},
{name: 'Union', url: SOURCE_URL + '/meeting/participate'},
{name: 'Community', url: SOURCE_URL + '/community'},
{name: 'Profile', url: SOURCE_URL + '/profile'},
];
const TabBarNavigator = () => {
const Screens = useMemo(
() => WEBVIEW_SCREENS.map(BottomTabNavigator.createScreen),
[],
);
return <BottomTabNavigator>{Screens}</BottomTabNavigator>;
};
export default TabBarNavigator;
BottomTabNavigator.createScreen을 이용해서 각 스크린을 만든다. 그리고 이 만든 스크린을 BottomTabNavigator의 children으로 넘긴다.
// BottomTabNavigator.tsx
const BottomTab = createBottomTabNavigator();
const BottomTabNavigator = ({children}: PropsWithChildren) => {
const props = useMemo(
() => ({
tabBar: (props: BottomTabBarProps) => <TabBar {...props} />,
screenOptions: {
headerShown: false,
lazy: false,
},
}),
[],
);
return <BottomTab.Navigator {...props}>{children}</BottomTab.Navigator>;
};
BottomTabNavigator.createScreen = ({
name,
url,
}: {
name: string;
url: string;
}) => (
<BottomTab.Screen
name={name}
component={WebViewContainer}
initialParams={{url, edges: ['top']}}
/>
);
export default BottomTabNavigator;
createBottomTabNaviagtor를 이용하여 BottomTab을 만들고, 스크린으로는 TabBar가 없는 WebViewContainer를 넘긴다.
고민
1. TabBar를 보여주고 보여주지 않는 것을 어떤 식으로 웹뷰와 통신할까?
추후 React Native스토리지로 마이그레이션하게되면 ReactNative 스토리지에서 토큰을 체크하고 → 존재한다면 TobBar를 보여주고, 존재하지 않는다면 보여주지 않는 방식으로 해도 될 거 같다.
하지만 TabBar를 웹뷰에서 관리하는 현 시점에서는, TabBar가 필요한 시점에 웹뷰 → 앱으로 메시지 전송, 필요하지 않을 때도 전송하는 방식으로 구현을 하고자 한다.
- ‘TabBar가 필요해요’메시지를 RN에 전송한다 : 토큰 설정 이후
- 로그인 이후(join/step1)
- 회원가입 이후(join/step5)
export default function useLogin() {
const { refresh, replace } = useAppRouter();
const login = async ({ accessToken, refreshToken, userId }: LoginProps) => {
setTokenAtCookie({ accessToken, refreshToken, userId });
await sendMessageToReactNative({ type: 'SIGN_IN', data: 'LOGIN_SUCCESS' });
refresh();
replace('/grouping');
};
return { login };
}
- ‘TabBar가 필요하지 않아요’메시지를 RN에 전송한다 : 토큰 삭제 이후
- 로그아웃 이후
- 탈퇴 이후
export default function useLogout() {
const router = useRouter();
const logout = () => {
router.push('/join');
removeToken();
sendMessageToReactNative({ type: 'AUTH', data: 'LOG_OUT' });
};
return { logout };
}
2. 환경이 앱인지 웹인지 어떻게 구분할까?
웹에서는 Footer를 보여주고, 웹이 아니라면 Footer를 보여주지 않으려 한다. 서버인지 / 아닌지(웹 혹은 앱) 구분하는 방법은 전역객체로 판단하면 된다. typeof global이 undefined가 아니라면, 서버이다.
Footer는 서버 컴포넌트여서 서버에서 동작한다. 서버에서는 사용자가 웹인지 / 앱인지 구분하지 못한다. 서버(Node.js환경)에는 navigator가 없다. 즉, 클라이언트에 와서 판단해야 한다는 말이다.
브라우저에 왔는데, 웹이 아니라 앱이여서 Footer를 제거한다면, 서버에서 만든 HTML과 달라서 아래 이미지와 같이 Hydration에러가 발생할 것이다.
다음과 같은 두 가지 방법이 있을 수 있다. 각각 장단점이 존재한다.
- 서버 환경이거나, 앱이라면(클라이언트에서 판단) 탭을 안보여준다.
- 이 경우, 웹환경에서 Hydration에러가 발생한다.
- 서버에서 탭이 없다 → 웹이여서 탭이 생겼다. 이 과정에서 두 HTML이 달라 Hydration에러가 발생한다.
- 앱이라면 탭을 안보여준다.
- 이 경우, 앱환경에서 Hydration에러가 발생한다.
- 서버에서 탭이 있다 → 앱이여서 탭이 없어진다. 이 과정에서 에러가 발생한다.
3. 서버에서 아얘 렌더링을 하지 않고, 클라이언트에서 렌더링을 할 지 유무를 결정할 수 는 없을까? 이를테면 Lazy Loading?
그렇다. Next.js에서는 LazyLoading을 제공한다. next/dynamic의 dynamic import를 사용하면 된다.
Footer를 사용하는 측에서 다음과 같이 Footer를 import하면 된다.
const Footer = dynamic(() => import('@/components/Footer/Footer.server'), { ssr: false });
3. 해결 후
TabBar로 적용을 하니, 확실히 깜빡임 현상도 없고, 한 번 렌더링을 하면 그 이후로 자연스럽게 화면이 바뀐다. 너무 깔끔하다.ㅎㅎㅎ