Expo Router로 배우는 React Native 파일 기반 내비게이션 완벽 가이드

Expo Router를 활용한 React Native 파일 기반 라우팅을 체계적으로 다룹니다. 레이아웃, 동적 라우트, 타입 안전 내비게이션, 탭, 모달, 미들웨어 등 2026년 최신 패턴을 종합적으로 설명합니다.

React Native 모바일 앱을 위한 Expo Router 파일 기반 내비게이션 시스템

Expo Router는 React Native 애플리케이션에서 파일 시스템 기반의 라우팅을 구현할 수 있게 해주는 강력한 내비게이션 프레임워크입니다. Next.js에서 영감을 받은 이 라우터는 파일과 폴더 구조만으로 앱의 전체 내비게이션을 자동으로 생성합니다. 기존 React Navigation에서 수동으로 스크린을 등록하고 네비게이터를 중첩하던 복잡한 설정 과정을 획기적으로 단순화하며, 웹과 모바일 플랫폼 모두에서 일관된 라우팅 경험을 제공합니다. 이 가이드에서는 Expo Router의 핵심 개념부터 동적 라우트, 타입 안전 라우팅, 인증 가드까지 실무에서 필요한 모든 내비게이션 패턴을 체계적으로 다룹니다.

빠른 설정

Expo Router 프로젝트를 시작하는 가장 빠른 방법은 npx create-expo-app@latest --template tabs 명령어를 사용하는 것입니다. 이 템플릿에는 파일 기반 라우팅, 탭 내비게이션, TypeScript 설정이 모두 포함되어 있어 별도의 구성 없이 바로 개발을 시작할 수 있습니다.

Expo Router의 파일 기반 라우팅 원리

Expo Router의 핵심 철학은 단순합니다. app/ 디렉토리 안에 파일을 생성하면, 해당 파일의 경로가 곧 앱의 URL이 됩니다. app/settings.tsx 파일을 만들면 /settings 경로가 자동으로 등록되고, app/profile/edit.tsx 파일을 만들면 /profile/edit 경로가 생성됩니다. 별도의 라우팅 설정 파일이나 네비게이터 등록 코드가 필요하지 않습니다.

이러한 파일 기반 접근 방식은 여러 가지 실질적인 이점을 제공합니다. 첫째, 프로젝트의 파일 구조를 보는 것만으로 앱의 전체 내비게이션 흐름을 파악할 수 있습니다. 둘째, 새로운 화면을 추가할 때 파일 하나만 생성하면 되므로 개발 속도가 크게 향상됩니다. 셋째, 딥 링킹이 기본적으로 지원되어 외부에서 앱의 특정 화면으로 직접 이동하는 것이 자연스럽게 가능합니다.

다음은 가장 기본적인 홈 화면 구현 예시입니다. Link 컴포넌트를 사용하여 다른 화면으로의 내비게이션을 선언적으로 처리할 수 있습니다.

app/index.tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { Link } from 'expo-router'

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome</Text>
      {/* Link maps directly to file path */}
      <Link href="/settings" style={styles.link}>
        Open Settings
      </Link>
      <Link href="/profile/edit" style={styles.link}>
        Edit Profile
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 },
  link: { fontSize: 16, color: '#61DAFB', marginTop: 12 },
})

Link 컴포넌트의 href 속성에 전달하는 경로는 app/ 디렉토리 내의 실제 파일 경로와 정확히 일치합니다. 이 직관적인 매핑 덕분에 내비게이션 코드의 의도를 즉시 파악할 수 있으며, 잘못된 경로를 참조할 가능성도 크게 줄어듭니다.

프로젝트 구조와 레이아웃 파일

Expo Router 프로젝트에서 파일과 폴더의 구조는 곧 앱의 내비게이션 아키텍처를 의미합니다. 각 파일과 특수 명명 규칙이 어떤 역할을 하는지 이해하는 것이 효과적인 라우팅 설계의 첫걸음입니다.

text
app/
  _layout.tsx          # Root layout (Stack or custom)
  index.tsx            # Home screen (/)
  (tabs)/              # Tab group (parentheses = route group)
    _layout.tsx        # Tab navigator
    home.tsx           # /home tab
    search.tsx         # /search tab
    profile.tsx        # /profile tab
  settings/
    _layout.tsx        # Settings stack layout
    index.tsx          # /settings
    notifications.tsx  # /settings/notifications
    privacy.tsx        # /settings/privacy

이 구조에서 주목해야 할 몇 가지 핵심 규칙이 있습니다. _layout.tsx 파일은 해당 디렉토리와 그 하위 경로들을 감싸는 레이아웃 래퍼 역할을 합니다. 이 파일은 URL 경로에 포함되지 않으며, Stack, Tabs 등의 네비게이터를 정의하는 데 사용됩니다. index.tsx 파일은 해당 디렉토리의 기본 경로를 나타냅니다. 괄호로 감싼 폴더명(예: (tabs))은 라우트 그룹으로, URL에 영향을 주지 않으면서 관련 화면들을 논리적으로 묶을 수 있게 해줍니다.

루트 레이아웃 파일은 앱의 최상위 내비게이션 구조를 정의합니다. 일반적으로 Stack 네비게이터를 사용하여 전체 앱의 화면 전환 방식을 설정합니다.

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

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: '600' },
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="settings" options={{ title: 'Settings' }} />
    </Stack>
  )
}

screenOptions 속성을 통해 모든 하위 화면에 공통으로 적용될 헤더 스타일을 지정할 수 있으며, 개별 Stack.Screenoptions를 통해 특정 화면의 설정을 오버라이드할 수 있습니다. (tabs) 라우트 그룹에 headerShown: false를 설정한 것은 탭 네비게이터가 자체적으로 헤더를 관리하기 때문입니다.

Expo Router로 탭 내비게이션 구현하기

탭 내비게이션은 모바일 앱에서 가장 보편적으로 사용되는 내비게이션 패턴입니다. Expo Router에서는 라우트 그룹과 Tabs 컴포넌트를 조합하여 직관적으로 탭 내비게이션을 구현할 수 있습니다.

app/(tabs)/_layout.tsxtypescript
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#61DAFB',
        tabBarInactiveTintColor: '#888',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopColor: '#2d2d44',
        },
      }}
    >
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  )
}

Tabs 컴포넌트는 (tabs) 폴더 내의 파일들을 자동으로 탭 화면으로 인식합니다. screenOptions를 통해 탭 바의 전체적인 스타일링을 제어하고, 각 Tabs.Screen에서 개별 탭의 아이콘과 제목을 설정합니다. tabBarIcon 속성은 렌더 함수를 받아 현재 활성 상태에 따라 적절한 색상의 아이콘을 표시합니다.

탭 내비게이션의 장점은 (tabs) 라우트 그룹이 URL 경로에 포함되지 않는다는 것입니다. 따라서 app/(tabs)/home.tsx 파일의 실제 경로는 /home이 되어, 깔끔한 URL 구조를 유지할 수 있습니다. 이는 딥 링킹이나 웹 플랫폼에서의 URL 공유 시 특히 유용합니다.

동적 라우트와 라우트 파라미터

실제 앱에서는 상품 상세 페이지, 사용자 프로필 등 동적인 데이터에 따라 화면이 변해야 하는 경우가 대부분입니다. Expo Router는 대괄호 표기법을 사용하여 동적 라우트 세그먼트를 지원합니다.

app/product/[id].tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { useLocalSearchParams, Stack } from 'expo-router'

export default function ProductScreen() {
  // Extract the dynamic parameter from the URL
  const { id } = useLocalSearchParams<{ id: string }>()

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: `Product ${id}` }} />
      <Text style={styles.heading}>Product Details</Text>
      <Text style={styles.id}>ID: {id}</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  id: { fontSize: 16, color: '#888' },
})

[id] 폴더 또는 파일명은 해당 세그먼트가 동적 파라미터임을 나타냅니다. useLocalSearchParams 훅을 통해 현재 화면의 라우트 파라미터에 접근할 수 있으며, 제네릭 타입을 지정하면 TypeScript의 타입 추론 혜택도 누릴 수 있습니다. Stack.Screenoptions를 컴포넌트 내부에서 동적으로 설정하여 파라미터 값에 따라 헤더 제목을 변경하는 것도 가능합니다.

더 복잡한 URL 패턴이 필요한 경우에는 캐치올(catch-all) 라우트를 활용할 수 있습니다. [...slug] 표기법을 사용하면 여러 세그먼트를 배열로 캡처할 수 있습니다.

app/docs/[...slug].tsxtypescript
import { useLocalSearchParams } from 'expo-router'

export default function DocsScreen() {
  // /docs/getting-started/installation → slug = ['getting-started', 'installation']
  const { slug } = useLocalSearchParams<{ slug: string[] }>()

  return <DocViewer path={slug.join('/')} />
}

캐치올 라우트는 문서 뷰어, 콘텐츠 관리 시스템, 중첩된 카테고리 구조 등 깊이가 유동적인 경로를 처리해야 할 때 특히 유용합니다. /docs/getting-started/installation 경로로 접근하면 slug 파라미터에 ['getting-started', 'installation'] 배열이 전달됩니다.

React Native 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

타입 안전 라우팅(Typed Routes)

대규모 애플리케이션에서는 존재하지 않는 경로로의 내비게이션이 런타임 오류를 유발할 수 있습니다. Expo Router의 타입 안전 라우팅 기능을 활성화하면 컴파일 타임에 잘못된 경로 참조를 감지할 수 있어, 이러한 오류를 사전에 방지할 수 있습니다.

먼저 app.json에서 실험적 기능을 활성화합니다.

json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

이 설정을 활성화하면 Expo CLI가 app/ 디렉토리의 파일 구조를 분석하여 타입 정의 파일을 자동으로 생성합니다. 이후 router.push(), router.replace(), Link 컴포넌트 등에서 사용하는 모든 경로 문자열에 대해 TypeScript가 유효성을 검증합니다.

app/checkout.tsxtypescript
import { router } from 'expo-router'

function handleCheckout(cartId: string) {
  // TypeScript validates this route exists
  router.push(`/product/${cartId}`)

  // This would cause a compile error if /nonexistent doesn't exist
  // router.push('/nonexistent')
}

타입 안전 라우팅은 특히 팀 프로젝트에서 큰 효과를 발휘합니다. 한 개발자가 파일명을 변경하거나 경로 구조를 리팩토링할 때, 해당 경로를 참조하는 모든 코드에서 즉시 컴파일 에러가 발생하므로 누락된 수정 사항을 빠르게 발견할 수 있습니다. 코드 리뷰 과정에서도 잘못된 경로 참조를 자동으로 검출하여 품질을 보장합니다.

모달 화면과 프레젠테이션 설정

모달은 현재 컨텍스트를 유지하면서 추가 정보를 입력받거나 표시해야 할 때 자주 사용되는 UI 패턴입니다. Expo Router에서는 Stack.Screenpresentation 옵션을 'modal'로 설정하는 것만으로 모달 화면을 구현할 수 있습니다.

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      {/* Modal screen slides up from the bottom */}
      <Stack.Screen
        name="create-post"
        options={{
          presentation: 'modal',
          headerTitle: 'New Post',
        }}
      />
    </Stack>
  )
}

모달로 표시될 화면의 구현은 일반 화면과 동일합니다. 차이점은 레이아웃 파일에서 프레젠테이션 방식만 지정한다는 것입니다. router.back()을 호출하면 모달이 닫히며 이전 화면으로 자연스럽게 복귀합니다.

app/create-post.tsxtypescript
import { View, TextInput, Button, StyleSheet } from 'react-native'
import { router } from 'expo-router'
import { useState } from 'react'

export default function CreatePostModal() {
  const [title, setTitle] = useState('')

  const handleSubmit = () => {
    // Submit logic here
    router.back() // Dismiss the modal
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Post title"
        value={title}
        onChangeText={setTitle}
      />
      <Button title="Publish" onPress={handleSubmit} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    marginBottom: 16,
  },
})

모달 화면은 iOS에서 기본적으로 하단에서 위로 슬라이드되는 애니메이션으로 표시되며, Android에서는 플랫폼 기본 전환 애니메이션이 적용됩니다. presentation 옵션에는 'modal' 외에도 'transparentModal', 'containedModal', 'fullScreenModal' 등 다양한 프레젠테이션 스타일을 지정할 수 있어 디자인 요구사항에 맞는 화면 전환을 유연하게 구현할 수 있습니다.

프로그래밍 방식의 내비게이션과 Router API

선언적인 Link 컴포넌트 외에도, Expo Router는 router 객체를 통해 명령적(프로그래밍 방식) 내비게이션을 지원합니다. 폼 제출 후 리다이렉트, 조건부 내비게이션, 사용자 인터랙션에 따른 동적 화면 전환 등의 시나리오에서 router API가 필수적으로 활용됩니다.

typescript
import { router } from 'expo-router'

// Push a new screen onto the stack
router.push('/profile/settings')

// Replace the current screen (no back button)
router.replace('/login')

// Go back to the previous screen
router.back()

// Navigate with parameters
router.push({
  pathname: '/product/[id]',
  params: { id: '42', source: 'recommendations' },
})

// Check if going back is possible
import { useRouter } from 'expo-router'

function BackButton() {
  const router = useRouter()
  return router.canGoBack() ? (
    <Button title="Back" onPress={() => router.back()} />
  ) : null
}
내비게이션과 리다이렉트

router.replace()<Redirect /> 컴포넌트는 모두 현재 화면을 대체하지만, 사용 맥락이 다릅니다. router.replace()는 이벤트 핸들러나 비동기 로직 내에서 명령적으로 사용하고, <Redirect href="/login" />은 렌더링 시점에 선언적으로 사용합니다. 인증 가드처럼 렌더링 조건에 따라 리다이렉트해야 하는 경우에는 <Redirect /> 컴포넌트가 더 적합합니다.

router.push()는 내비게이션 스택에 새 화면을 추가하므로 사용자가 뒤로가기를 통해 이전 화면으로 돌아갈 수 있습니다. 반면 router.replace()는 현재 화면을 대체하므로 뒤로가기 히스토리에 남지 않습니다. 이 차이는 로그인 후 대시보드로 이동하는 경우처럼, 이전 화면으로의 복귀가 논리적으로 맞지 않는 플로우에서 중요합니다.

router.canGoBack() 메서드는 현재 내비게이션 스택에서 뒤로가기가 가능한지 확인하는 데 유용합니다. 앱의 최상위 화면에서는 뒤로가기 버튼을 숨기는 등 조건부 UI 렌더링에 활용할 수 있습니다.

미들웨어와 라우트 보호

실제 프로덕션 앱에서는 특정 화면에 대한 접근을 인증 상태에 따라 제한해야 하는 경우가 많습니다. Expo Router는 서버 측 미들웨어와 클라이언트 측 레이아웃 가드 두 가지 방식으로 라우트 보호를 지원합니다.

서버 측 미들웨어는 +middleware.ts 파일을 통해 구현합니다. 요청이 라우트에 도달하기 전에 인터셉트하여 인증 토큰 검증, 리다이렉트 등의 로직을 수행할 수 있습니다.

app/+middleware.tstypescript
import { type MiddlewareRequest } from 'expo-router/server'

export function middleware(request: MiddlewareRequest) {
  const { pathname } = request.nextUrl

  // Protect dashboard routes
  const protectedPaths = ['/dashboard', '/settings', '/profile']
  const isProtected = protectedPaths.some(p => pathname.startsWith(p))

  if (isProtected) {
    const token = request.cookies.get('session')
    if (!token) {
      return Response.redirect(new URL('/login', request.url))
    }
  }

  return undefined // Continue to route
}
플랫폼 인식

서버 미들웨어(+middleware.ts)는 웹 플랫폼에서만 실행됩니다. iOS 및 Android 네이티브 환경에서는 서버가 존재하지 않으므로 미들웨어가 동작하지 않습니다. 네이티브 플랫폼에서 라우트 보호가 필요한 경우에는 반드시 클라이언트 측 레이아웃 가드를 함께 구현해야 합니다.

클라이언트 측에서는 레이아웃 파일 내에서 인증 상태를 확인하고, 미인증 사용자를 리다이렉트하는 패턴이 가장 일반적입니다. 라우트 그룹을 활용하면 보호가 필요한 화면들을 논리적으로 묶어 관리할 수 있습니다.

app/(authenticated)/_layout.tsxtypescript
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@/hooks/useAuth'

export default function AuthenticatedLayout() {
  const { isLoggedIn, isLoading } = useAuth()

  if (isLoading) return null
  if (!isLoggedIn) return <Redirect href="/login" />

  return <Stack />
}

이 패턴에서 (authenticated) 라우트 그룹 내의 모든 화면은 AuthenticatedLayout을 거치게 됩니다. 인증 상태 확인 중에는 null을 반환하여 깜빡임을 방지하고, 미인증 상태에서는 <Redirect /> 컴포넌트를 통해 로그인 화면으로 즉시 리다이렉트합니다. 인증이 완료된 사용자에게만 <Stack />을 통해 하위 화면들이 렌더링됩니다.

서버 미들웨어와 클라이언트 레이아웃 가드를 함께 사용하면 웹과 네이티브 플랫폼 모두에서 견고한 인증 체계를 구축할 수 있습니다. 서버 미들웨어가 웹에서의 1차 방어선 역할을 하고, 클라이언트 가드가 모든 플랫폼에서의 최종 보호 역할을 수행합니다.

결론

Expo Router는 React Native 앱의 내비게이션을 파일 시스템 기반으로 근본적으로 단순화합니다. 이 가이드에서 다룬 핵심 내용을 정리하면 다음과 같습니다.

  • 파일 기반 라우팅app/ 디렉토리의 파일 구조가 곧 앱의 URL 체계가 되는 직관적인 패러다임을 제공합니다.
  • 레이아웃 파일(_layout.tsx)을 통해 Stack, Tabs 등의 네비게이터를 각 라우트 세그먼트에 적용할 수 있습니다.
  • 라우트 그룹(괄호 폴더)은 URL에 영향을 주지 않으면서 관련 화면들을 논리적으로 구성하는 데 활용됩니다.
  • 동적 라우트캐치올 라우트는 파라미터 기반의 유연한 화면 구성을 가능하게 합니다.
  • 타입 안전 라우팅을 활성화하면 잘못된 경로 참조를 컴파일 타임에 감지하여 런타임 오류를 예방할 수 있습니다.
  • 모달 프레젠테이션은 레이아웃 설정만으로 간편하게 구현되며, 다양한 표시 스타일을 지원합니다.
  • Router APIpush, replace, back 등의 메서드를 통해 프로그래밍 방식의 세밀한 내비게이션 제어를 제공합니다.
  • 라우트 보호는 서버 미들웨어와 클라이언트 레이아웃 가드의 조합으로 모든 플랫폼에서 안전한 인증 체계를 구현할 수 있습니다.

Expo Router를 도입하면 내비게이션 설정에 소요되는 시간을 대폭 줄이고, 앱의 핵심 기능 개발에 집중할 수 있습니다. 파일 구조의 변경만으로 내비게이션을 리팩토링할 수 있어 프로젝트의 유지보수성도 크게 향상됩니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#react-native
#expo
#expo-router
#navigation
#mobile-development
#tutorial

공유

관련 기사