이 글에서는 React Native 프로젝트의 라우팅을 위해 사용한 React Navigation 라이브러리에서 발생한 컴포넌트 초기화 이슈 해결 과정에 대해 설명해보려 한다.
나는 [Fig. 1]의 UI를 구현하기 위해 하단 네비게이션 바가 필요했고, React Navigation에서 제공하는 Bottom Tabs Navigatior를 사용하여 비교적 빠르게 구현할 수 있었다. React Navigation는 React Router과 유사하게 Tab.Navigator
가 각Tab.Screen
의 컴포턴트를 렌더링해주는 방식이다.(💡여기서 나는 React Router와 동작이 같은 것이라 생각했고 삽질은 이제 시작된다...^^)
공식 문서를 참고하여 [Fig.2]와 같은 구조로 네비게이션을 아래와 같이 구현할 수 있다.
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
const Tab = createBottomTabNavigator();
export default function MainContainer() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name={techTreeName} component={TechTree} />
<Tab.Screen name={homeName} component={Home} />
<Tab.Screen name={myPageName} component={Mypage} />
</Tab.Navigator>
</NavigationContainer>
);
}
네비게이션을 구현 완료한 후 각 페이지의 구조를 짜던 중 페이지가 전환될 때 Home의 state가 초기화되지 않고 그대로 남아있는 버그를 발견했다. 아래 영상을 보면 Home calendar에서 선택한 날짜(day state)에 표기되는 보라색 선이 페이지 전환 후에도 그대로 남아있는 것을 확인할 수 있다.
처음으로는 리액트 프로젝트에서 함수의 조건부 실행에 가장 많이 사용되는 useEffect
를 시도해보았다.
곰곰히 생각해본다면 state는 컴포넌트가 리렌더링될 때마다 초기화되어야 하는데 화면이 전환되어도 남아있다는 것은 리렌더링이 일어나지 않고 있다는 뜻이다.
useEffect(() => {
setDays(markedDays);
}, []);
let [reRender, setRerender] = useState(true);
// Rerendering button
// ✉️ setRerender를 전역 상태로 관리해서 navigation tab 바뀔 때마다 rerendering
const refreshClicked = () => {
setRerender((prev) => !prev);
};
// fetching data and set today at first rendering
useEffect(() => {
setDays(markedDays);
}, [reRender]);
1-3.의 방법 1에서 화면이 전환되어도 리렌더링이 일어나지 않는다는 사실을 깨닫고 내가 React Native를 React와 너무 동일시하고 있었다는 문제를 자각했다. React Native는 자바스크립트를 사용하여 React와 비슷한 개발 환경을 사용하지만 엄연히 구동 방식이 다른 프레임워크이다.
그렇다. React Native는 브라우저 환경이 아니므로 DOM이 아니라 모바일 API를 사용한다. 그러면 모바일 상에서 화면 전환은 어떻게 일어날까?
React에서 사용하는 두 가지 라우팅 라이브러리를 비교하여 웹과 모바일 앱의 차이점을 자세히 알아보자. A/B/C 스크린(컴포넌트)의 화면 전환을 간단하게 [Fig. 3]과 같은 그림으로 나타내보았다.
웹 개발자에게는 아주 익숙한 React Router를 먼저 살펴보자. React는 SPA 기반으로 React Router에서는 컴포넌트 속성에 설정된 URL과 현재 경로가 일치하면 해당하는 컴포넌트를 렌더링한다. [Fig. 3]의 좌측과 같이 화면 A ↔️ B의 전환은 각 컴포넌트가 교체되면서 일어난다. 즉 DOM에 컴포넌트를 끼워넣고 빼면서 mount/unmount 되므로, 다시 화면 A로 되돌아갔을 때 컴포넌트가 mount되며 자체적으로 초기화된다.
모바일 앱에 사용되는 React Navigator에서는 stack 구조로 되어있어 화면을 이동하면 전 화면이 사라지는 것이 아니라 기존의 화면 위에 새로운 화면이 쌓인다. 따라서 화면을 클릭하면 stack에 push되고 뒤로가기를 클릭하면 pop되면서 이전 화면이 등장한다. 이 구조의 특이한 점은 [Fig. 3]의 우측과 같이 화면 A가 B로 전환되어도 A가 unmount 되지 않고 그대로 stack에 남아있다(공식 문서 참조). 즉 화면 A로 재전환되어도 A는 이미 mount 상태이므로 초기화가 일어나지 않으므로 앞서 발생한 문제가 야기된 것이다.
React Router: 화면 전환 = 기존 컴포넌트 unmount 후 새 컴포넌트 mount
React Navigator: 화면 전환 = 기존 컴포넌트가 stack에서 mount된 상태로 유지되면서 새 컴포넌트가 스택의 가장 위로 pop
React Navigation 공식 문서에는 친절하게도 해결 방법이 잘 나와있다. React Navigation에서는 사용자의 스크린 in/out에 대한 event를 focus/blur로 제공하며 [Fig. 4]로 쉽게 이해할 수 있다. 이 이벤트는 두 가지 방법으로 구현할 수 있으며 나는 사용이 더 간편한 useFocusEffect
hook을 활용하였다.
(다만 버전 5부터 이 해결법을 제시하고 있으므로 그 아래 버전을 사용한다면 해당 공식 문서 참조를 추천한다.)
React.useEffect(
() => navigation.addListener('focus', () => alert('Screen was focused')),
[]
);
React.useEffect(
() => navigation.addListener('blur', () => alert('Screen was unfocused')),
[]
);
import { useFocusEffect } from '@react-navigation/native';
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
};
}, [])
나는 day state의 초기화가 필요한 Home 컴포넌트 내부에 아래 코드와 같이 useFocusEffect hook을 추가하였다. 아래 영상을 통해 #1-2.와는 다르게 선택된 날짜의 보라색 선이 화면 재전환 시 사라지는 것을 확인할 수 있다.
useFocusEffect(
useCallback(() => {
setDays(markedDays);
}, [])
);
리액트에 비해 상대적으로 유저가 많이 적은 리액트 네이티브를 사용하다보니 디버깅이 굉장히 까다롭다고 느끼고 있다. 이 프로젝트를 하며 느끼는 점은 어렵긴 하지만 공식 문서를 직접 읽어봐야 한다는 것이다. 내가 원하는 내용을 10을 얻기 위해서 적게는 50 많게는 100정도의 양을 읽어야 하지만, 혼자 삽질하는 것보다는 백만배 낫다는 것을 뼈져리게 느끼고 있다. 클라이언트 상태 관리도 이렇게 어려운데 서버 데이터가 들어오면 어떨지... ^^
추가로 Home이 아닌 다른 화면에서는 초기화가 되지 않는 점을 잘 활용할 수 있을 것 같다. 아마 react navigation 팀이 의도한 사용법이지 않을까...
오늘의 교훈: 공식 문서를 열심히 읽자!(이런 짤이 있는 걸 보니 나만 보기 싫어하는 건 아닌가보다..^^)
https://reactnavigation.org/docs/navigation-lifecycle
https://im-developer.tistory.com/215
https://daesiker.tistory.com/39
https://velog.io/@soyi47/React-Component의-Lifecycle
이 글에 사용된 모든 자료는 직접 제작하였습니다.
감사합니다... 덕분에 골치 아픈 문제를 해결했네요 ㅎㅎ