블로그 목록
React Native

Expo Router로 React Native 앱 만들기 — 파일 기반 라우팅으로 네비게이션 단순화하기

React Native 앱에서 화면 전환을 설정하는 데 생각보다 시간을 많이 썼다. React Navigation은 강력한데, 스택·탭·드로어를 조합하다 보면 설정 코드가 길어진다. 화면이 많아질수록 어떤 화면이 어디에 등록돼 있는지 추적하기도 힘들어진다.

Expo Router는 파일 시스템으로 라우팅을 정의한다. Next.js를 써본 사람이라면 금방 익숙해진다.

시작하기

npx create-expo-app my-app --template tabs
cd my-app
npx expo start

tabs 템플릿은 하단 탭 네비게이션 구조로 시작한다. app/ 디렉토리 아래 파일을 만들면 자동으로 라우트가 생긴다.

파일 구조

app/
  _layout.tsx          # 루트 레이아웃
  index.tsx            # / (홈)
  (tabs)/
    _layout.tsx        # 탭 네비게이터
    index.tsx          # 첫 번째 탭
    explore.tsx        # 두 번째 탭
  post/
    [id].tsx           # /post/123 (동적 라우트)
  +not-found.tsx       # 404 처리

괄호로 묶은 (tabs)는 URL에 포함되지 않는 라우트 그룹이다. 탭이나 인증 플로우 같은 논리적 그룹을 만들 때 쓴다.

레이아웃 파일

_layout.tsx는 해당 디렉토리의 모든 화면을 감싸는 레이아웃이다. 스택을 쓸지 탭을 쓸지 여기서 결정한다.

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: '홈', headerShown: false }} />
      <Stack.Screen name="post/[id]" options={{ title: '글 보기' }} />
    </Stack>
  );
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="index"
        options={{
          title: '홈',
          tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
        }}
      />
      <Tabs.Screen name="explore" options={{ title: '탐색' }} />
    </Tabs>
  );
}

화면 이동

import { Link, router } from 'expo-router';

// 링크 컴포넌트
<Link href="/post/123">글 보기</Link>

// 프로그래매틱 네비게이션
router.push('/post/123');
router.replace('/login');  // 뒤로가기 히스토리 없이
router.back();

Link 컴포넌트는 웹에서는 <a> 태그, 네이티브에서는 터치 핸들러로 동작한다. 플랫폼별로 다르게 처리해줄 필요가 없다.

동적 라우트에서 파라미터 읽기

// app/post/[id].tsx
import { useLocalSearchParams } from 'expo-router';

export default function PostPage() {
  const { id } = useLocalSearchParams<{ id: string }>();
  return <Text>포스트 ID: {id}</Text>;
}

인증이 필요한 화면 보호

// app/(protected)/_layout.tsx
import { Redirect, Slot } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';

export default function ProtectedLayout() {
  const { isSignedIn } = useAuth();

  if (!isSignedIn) {
    return <Redirect href="/login" />;
  }

  return <Slot />;
}

(protected) 그룹 안에 있는 화면은 _layout.tsx를 통과해야 하니까, 인증 체크가 한 곳에서 된다.

웹 지원

같은 코드가 웹에서도 동작한다.

npx expo start --web

플랫폼별로 다른 UI가 필요하면 .native.tsx, .web.tsx 확장자로 분리할 수 있다. 비즈니스 로직은 공유하고 렌더링만 다르게 가져가는 패턴이다.

components/
  Button.tsx           # 공통 로직
  Button.native.tsx    # 네이티브 전용 UI
  Button.web.tsx       # 웹 전용 UI

웹과 모바일 코드를 한 저장소에서 관리하는 모노레포 구조를 계획하고 있다면 이 패턴이 잘 맞는다.


React Navigation을 직접 설정하는 것보다 코드가 훨씬 단순해졌다. 화면 추가할 때 파일만 만들면 되고, 전체 라우트 구조가 디렉토리 트리에 그대로 드러나서 파악하기 쉽다. Next.js 경험이 있다면 적응이 빠를 거다.