DevLog

[React Native] React Navigation, 모바일 화면 전환과 렌더 트리 | 웹 화면 전환과 모바일 화면 전환 과정 차이에 대하여 본문

Front-end

[React Native] React Navigation, 모바일 화면 전환과 렌더 트리 | 웹 화면 전환과 모바일 화면 전환 과정 차이에 대하여

김만콩 2025. 5. 11. 00:33

리액트 네이티브에서 화면 전환 기능을 구현할 때 가장 대중적으로 사용된다는 react-navigation 라이브러리.
이번 앱개발 프로젝트를 진행하면서 사용해보고 있는데.. React 웹 환경과 React Native 앱 환경에서의 네비게이션은 다르다는 것을 몸소 체감하는 중이다.

이놈의 버그, 버그, 버그.
처음 리액트 네이티브를 시작했을 때는 기존의 리액트 문법과 크게 다를 게 없어서 다행이라고 생각했는데, 네비게이션 과정에서 React Router를 쓰던 것과 유사한 방식으로 접근하려니 예상치 못한 결과가 튀어나온다. 아놔..
이래서 선이론 후실습이 중요한 건데, 익숙한 문법을 쓰다 보니 본인이 RN 초심자라는 사실을 간과했다.

공식문서야~ 도와줘~
https://reactnavigation.org/docs/hello-react-navigation/

 

Hello React Navigation | React Navigation

In a web browser, you can link to different pages using an anchor (``) tag. When the user clicks on a link, the URL is pushed to the browser history stack. When the user presses the back button, the browser pops the item from the top of the history stack,

reactnavigation.org

 

01. Web 화면 전환과 Mobile App 화면 전환의 차이

모바일 앱 환경에서 네비게이션을 구현하면서 가장 새로웠던 개념은 'Stack' 이었다.

물론 브라우저에도 History 기능이 존재하고 스택 자료구조의 특징을 떠올려 보기만 해도 어떤 식으로 돌아가는지 이해할 수 있지만, 웹에서는 화면 전환에 있어서 stack이나 depth 같은 개념을 깊이 있게 고민해본 적이 없다 보니 꽤나 생소했다. 사실 아직도 depth를 고려하면서 기능을 설계하고 코드를 짜는 과정이 조금은 어색하다.

// Router.tsx

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./routes/Home";
import Details from "./routes/Details";

const Router = () => {
  return (
    <BrowserRouter basename={process.env.PUBLIC_URL}>
      <>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/details" element={<Details />} />
        </Routes>
      </>
    </BrowserRouter>
  );
};

export default Router;

SPA 기반 웹에서는 화면 전환이 이루어질 때 URL 경로를 비교하면서 현재 경로에 해당하는 컴포넌트를 렌더링한다.

기본 경로에 있을 때에는 `<Home />` 컴포넌트를 보여주고, /details 경로에 있을 때는 `<Details />` 컴포넌트를 보여주는 등 미리 설정한 컴포넌트들을 경로에 맞게 DOM에 마운트/언마운트 하면서 화면을 렌더링한다. 즉 화면 B에서 A로 이동한다는 것은 화면에 보여주던 컴포넌트 B를 A로 갈아끼워 새롭게 렌더링하는 것을 의미하며, 이때 리렌더링 과정에서 컴포넌트 A는 초기화된다.

출처: https://velog.io/@skyu_dev/React-Navigation-Bottom-Tab%EC%9D%98-Require-cycle-warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0


반면 모바일 환경에서 화면 전환은 앞서 언급한 Stack 구조를 따르는데, 화면이 전환될 때마다 기존 페이지가 교체되는 것이 아니라 스택에 그대로 쌓이게 된다. 예를 들어 화면 A에서 B로 이동할 때 A는 unmount되지 않고 스택에 저장되는데, 이러한 특성 때문에 이후 A로 화면을 재전환하는 상황에서도 A는 이미 mount된 상태로 인식되어 컴포넌트 초기화 없이 state가 그대로 유지된다. 아하

이전 화면이 스택에 저장된다는 것은 해당 스크린의 컴포넌트 인스턴스와 상태(state), 컴포넌트 구조 등이 메모리에 유지되는 것을 의미한다. 화면 전환이 이루어질 때마다 새로운 렌더 트리를 매번 다시 그리는 것이 아니라, 렌더 트리에서 활성화(포커스) 된 스크린과 비활성화 된 스크린을 구분하여 렌더링할 요소를 결정한다. 이를 위해 React Navigation은 스택에 쌓인 각 스크린을 useIsFocused() 훅으로 구분하여 관리하면서 유저에게 isFocused 스크린(=스택 최상단)만 보이도록 한다.

비활성화된 스크린은 렌더링이 멈추더라도 언마운트되지 않고 메모리에 그대로 남아 있는데, 해당 스크린이 스택에서 완전히 pop되는 경우 언마운트되어 메모리에서 해제된다.

02. StackNavigator

스택 구조를 활용한 네비게이션은 Native Stack Navigator로 구현된다.

const Stack = createNativeStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

StackNavigator 안에 route를 정의하게 되며, 각 route는 네비게이션에 사용할 이름과 화면에 보여줄 컴포넌트를 포함한다.
따로 initialRouteName을 설정하지 않으면 맨 앞의 route가 기본 화면으로 보여진다.

import React from 'react';
import {View, Button} from 'react-native';

function HomeScreen({navigation}) {
  return (
    <View>
      <Button
        title="Detail 열기"
        onPress={() => navigation.navigate('Detail')}
      />
    </View>
  );
}

export default HomeScreen;

스크린으로 사용된 컴포넌트는 `navigation` 및 `route` 객체를 props로 받아와서 화면 전환을 시도할 수 있다.
이외 컴포넌트에서 네비게이션 동작을 수행하기 위해서는 `useNavigation()` 및 `useRouter()` 훅을 사용할 수 있다.

03. 타입스크립트와 React Navigation

navigation이나 route 객체를 props(any 타입)로 받아서 사용하는 경우에는 문제가 없었는데,
타입스크립트 기반으로 `useNavigation()` 및 `useRoute()` 훅을 사용하려고 하니 다음과 같은 에러가 발생했다.

Argument of type '[string]' is not assignable to parameter of type 'never'.
쉽게 말해 타입스크립트가 `navigation.navigate('name')` 과 같은 코드에서 라우트 name을 제대로 인식하지 못해 에러가 발생했다.

위 문제는 여타 타입스크립트 타입 에러처럼 스택 네비게이터의 타입을 정의해주면 쉽게 해결할 수 있었다.
여기서 네비게이터 타입이란 각 라우트가 가지는 파라미터 타입을 정의해주는 것이라고 한다. 이걸 기반으로 navigation이나 route에 타입을 붙여서 안전하게 사용할 수 있다고!

사용 예시는 아래와 같다.

// types.ts
export type HomeStackParamList = {
  'Home': undefined;		// 파라미터를 갖지 않음
  'Details': { id: number }; // 화면 전환 시 params로 id를 넘겨받음
};

// HomeStack.tsx
const Stack = createNativeStackNavigator<HomeStackParamList>();

export default function HomeStack() {
  return (
	<Stack.Navigator>
	  <Stack.Screen name="Home" component={HomeView} />
	  <Stack.Screen name="Details" component={DetailView} />
	</Stack.Navigator>
  );
}
// useNavigation() 예시
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList, 'Home'>>();
(...)
navigation.navigate('Home');

// useRoute() 예시
type DetailRouteProp = RouteProp<HomeStackParamList, 'Details'>;

export default function DetailView() {
	const route = useRoute<DetailRouteProp>();
	const { id } = route.params;
    	(...)

 04. 마치며

React의 DOM과 React Native의 렌더 트리,
웹 환경의 react-router와 모바일 환경의 react-navigation.

비슷하면서도 다른 두 개념과 react-navigation의 기본적인 활용법에 대해 살펴보았다.
아 헷갈려

운영체제 과목을 수강할 때 교수님이 지나가듯이 모바일 메모리에 대해 알려주셨던 것 같은데, 잘 기억이 나지 않는다.
필기 자료를 뒤져봐야겠다. 아니 그냥 이참에 운영체제 내용을 전체적으로 복습하는 게 좋을지도