# Expo Router로 배우는 React Native 파일 기반 내비게이션 완벽 가이드
> Expo Router를 활용한 React Native 파일 기반 라우팅을 체계적으로 다룹니다. 레이아웃, 동적 라우트, 타입 안전 내비게이션, 탭, 모달, 미들웨어 등 2026년 최신 패턴을 종합적으로 설명합니다.
- Published: 2026-04-18
- Updated: 2026-04-18
- Author: SharpSkill
- Tags: react-native, expo, expo-router, navigation, mobile-development, tutorial
- Reading time: 9 min
---
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` 컴포넌트를 사용하여 다른 화면으로의 내비게이션을 선언적으로 처리할 수 있습니다.
```typescript
// app/index.tsx
import { View, Text, StyleSheet } from 'react-native'
import { Link } from 'expo-router'
export default function HomeScreen() {
return (
Welcome
{/* Link maps directly to file path */}
Open Settings
Edit Profile
)
}
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 네비게이터를 사용하여 전체 앱의 화면 전환 방식을 설정합니다.
```typescript
// app/_layout.tsx
import { Stack } from 'expo-router'
export default function RootLayout() {
return (
)
}
```
`screenOptions` 속성을 통해 모든 하위 화면에 공통으로 적용될 헤더 스타일을 지정할 수 있으며, 개별 `Stack.Screen`의 `options`를 통해 특정 화면의 설정을 오버라이드할 수 있습니다. `(tabs)` 라우트 그룹에 `headerShown: false`를 설정한 것은 탭 네비게이터가 자체적으로 헤더를 관리하기 때문입니다.
## Expo Router로 탭 내비게이션 구현하기
탭 내비게이션은 모바일 앱에서 가장 보편적으로 사용되는 내비게이션 패턴입니다. Expo Router에서는 라우트 그룹과 `Tabs` 컴포넌트를 조합하여 직관적으로 탭 내비게이션을 구현할 수 있습니다.
```typescript
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'
export default function TabLayout() {
return (
(
),
}}
/>
(
),
}}
/>
(
),
}}
/>
)
}
```
`Tabs` 컴포넌트는 `(tabs)` 폴더 내의 파일들을 자동으로 탭 화면으로 인식합니다. `screenOptions`를 통해 탭 바의 전체적인 스타일링을 제어하고, 각 `Tabs.Screen`에서 개별 탭의 아이콘과 제목을 설정합니다. `tabBarIcon` 속성은 렌더 함수를 받아 현재 활성 상태에 따라 적절한 색상의 아이콘을 표시합니다.
탭 내비게이션의 장점은 `(tabs)` 라우트 그룹이 URL 경로에 포함되지 않는다는 것입니다. 따라서 `app/(tabs)/home.tsx` 파일의 실제 경로는 `/home`이 되어, 깔끔한 URL 구조를 유지할 수 있습니다. 이는 딥 링킹이나 웹 플랫폼에서의 URL 공유 시 특히 유용합니다.
## 동적 라우트와 라우트 파라미터
실제 앱에서는 상품 상세 페이지, 사용자 프로필 등 동적인 데이터에 따라 화면이 변해야 하는 경우가 대부분입니다. Expo Router는 대괄호 표기법을 사용하여 동적 라우트 세그먼트를 지원합니다.
```typescript
// app/product/[id].tsx
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 (
Product DetailsID: {id}
)
}
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.Screen`의 `options`를 컴포넌트 내부에서 동적으로 설정하여 파라미터 값에 따라 헤더 제목을 변경하는 것도 가능합니다.
더 복잡한 URL 패턴이 필요한 경우에는 캐치올(catch-all) 라우트를 활용할 수 있습니다. `[...slug]` 표기법을 사용하면 여러 세그먼트를 배열로 캡처할 수 있습니다.
```typescript
// app/docs/[...slug].tsx
import { useLocalSearchParams } from 'expo-router'
export default function DocsScreen() {
// /docs/getting-started/installation → slug = ['getting-started', 'installation']
const { slug } = useLocalSearchParams<{ slug: string[] }>()
return
}
```
캐치올 라우트는 문서 뷰어, 콘텐츠 관리 시스템, 중첩된 카테고리 구조 등 깊이가 유동적인 경로를 처리해야 할 때 특히 유용합니다. `/docs/getting-started/installation` 경로로 접근하면 `slug` 파라미터에 `['getting-started', 'installation']` 배열이 전달됩니다.
## 타입 안전 라우팅(Typed Routes)
대규모 애플리케이션에서는 존재하지 않는 경로로의 내비게이션이 런타임 오류를 유발할 수 있습니다. Expo Router의 타입 안전 라우팅 기능을 활성화하면 컴파일 타임에 잘못된 경로 참조를 감지할 수 있어, 이러한 오류를 사전에 방지할 수 있습니다.
먼저 `app.json`에서 실험적 기능을 활성화합니다.
```json
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}
```
이 설정을 활성화하면 Expo CLI가 `app/` 디렉토리의 파일 구조를 분석하여 타입 정의 파일을 자동으로 생성합니다. 이후 `router.push()`, `router.replace()`, `Link` 컴포넌트 등에서 사용하는 모든 경로 문자열에 대해 TypeScript가 유효성을 검증합니다.
```typescript
// app/checkout.tsx
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.Screen`의 `presentation` 옵션을 `'modal'`로 설정하는 것만으로 모달 화면을 구현할 수 있습니다.
```typescript
// app/_layout.tsx
import { Stack } from 'expo-router'
export default function RootLayout() {
return (
{/* Modal screen slides up from the bottom */}
)
}
```
모달로 표시될 화면의 구현은 일반 화면과 동일합니다. 차이점은 레이아웃 파일에서 프레젠테이션 방식만 지정한다는 것입니다. `router.back()`을 호출하면 모달이 닫히며 이전 화면으로 자연스럽게 복귀합니다.
```typescript
// app/create-post.tsx
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 (
)
}
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() ? (