화면 이동 구현 — Expo Router




“Next.js의 파일 기반 라우팅, 앱에서도 똑같이 됩니다.”
이 글에서는 Expo Router로 여러 화면을 만들고 이동하는 방법을 코드와 함께 살펴봅니다.


Expo Router란?

Expo Router는 파일 구조가 곧 라우팅 구조가 되는 방식입니다.
Next.js의 App Router와 거의 동일한 개념이라 웹 개발자에게 매우 익숙합니다.

app/
├── index.tsx          → 홈 화면 (/)
├── profile.tsx        → 프로필 화면 (/profile)
├── settings/
│   └── index.tsx      → 설정 화면 (/settings)
└── post/
    └── [id].tsx       → 동적 화면 (/post/123)

파일을 만들면 자동으로 화면(라우트)이 생성됩니다.


기본 화면 이동

Link 컴포넌트 — 선언적 이동

// app/index.tsx
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}>홈 화면</Text>

      {/* 기본 이동 */}
      <Link href="/profile" style={styles.link}>
        프로필로 이동
      </Link>

      {/* 버튼 스타일로 이동 */}
      <Link href="/settings" asChild>
        <TouchableOpacity style={styles.button}>
          <Text style={styles.buttonText}>설정으로 이동</Text>
        </TouchableOpacity>
      </Link>
    </View>
  );
}

useRouter — 코드로 이동

// app/index.tsx
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';

export default function HomeScreen() {
  const router = useRouter();

  const handleLogin = () => {
    // 로그인 처리 후 이동
    router.push('/profile');        // 스택에 쌓으며 이동
    router.replace('/home');        // 현재 화면을 교체 (뒤로가기 불가)
    router.back();                  // 이전 화면으로 이동
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.button} onPress={handleLogin}>
        <Text style={styles.buttonText}>로그인</Text>
      </TouchableOpacity>
    </View>
  );
}

동적 라우팅 — 파라미터 전달

파라미터 넘기기

// app/index.tsx
import { Link } from 'expo-router';

export default function HomeScreen() {
  return (
    // /post/123 으로 이동
    <Link href="/post/123">게시글 보기</Link>
  );
}

파라미터 받기

// app/post/[id].tsx
import { View, Text, StyleSheet } from 'react-native';
import { useLocalSearchParams } from 'expo-router';

export default function PostScreen() {
  const { id } = useLocalSearchParams();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>게시글 #{id}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold' },
});

쿼리 파라미터 전달

// 이동할 때
router.push({
  pathname: '/search',
  params: { keyword: '리액트', category: 'dev' },
});

// 받을 때 (app/search.tsx)
const { keyword, category } = useLocalSearchParams();

스택 네비게이션

스택은 화면을 쌓아가며 이동하는 방식입니다.
뒤로가기 버튼으로 이전 화면으로 돌아갈 수 있습니다.

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen
        name="index"
        options={{
          title: '홈',
          headerStyle: { backgroundColor: '#007AFF' },
          headerTintColor: '#fff',
        }}
      />
      <Stack.Screen
        name="profile"
        options={{
          title: '프로필',
          headerBackTitle: '뒤로',
        }}
      />
      <Stack.Screen
        name="post/[id]"
        options={{ title: '게시글' }}
      />
    </Stack>
  );
}

탭 네비게이션

하단 탭 메뉴를 구성하는 방식입니다.
앱에서 가장 많이 사용되는 네비게이션 패턴입니다.

app/
├── _layout.tsx          # 루트 레이아웃
└── (tabs)/
    ├── _layout.tsx      # 탭 레이아웃
    ├── index.tsx        # 홈 탭
    ├── explore.tsx      # 탐색 탭
    └── profile.tsx      # 프로필 탭
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#007AFF',
        tabBarInactiveTintColor: '#999',
        tabBarStyle: {
          backgroundColor: '#fff',
          borderTopColor: '#eee',
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: '홈',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: '탐색',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" color={color} size={size} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: '프로필',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" color={color} size={size} />
          ),
        }}
      />
    </Tabs>
  );
}

모달 화면

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

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen
        name="modal"
        options={{
          presentation: 'modal',  // 모달로 표시
          title: '모달 화면',
        }}
      />
    </Stack>
  );
}
// app/modal.tsx
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';

export default function ModalScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>모달 화면입니다</Text>
      <TouchableOpacity style={styles.button} onPress={() => router.back()}>
        <Text style={styles.buttonText}>닫기</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  title: { fontSize: 20, marginBottom: 20 },
  button: {
    backgroundColor: '#007AFF',
    padding: 12,
    borderRadius: 8,
  },
  buttonText: { color: '#fff', fontWeight: '600' },
});

헤더 커스터마이징

// app/profile.tsx
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Stack, useRouter } from 'expo-router';

export default function ProfileScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      {/* 이 화면의 헤더만 커스터마이징 */}
      <Stack.Screen
        options={{
          title: '내 프로필',
          headerRight: () => (
            <TouchableOpacity onPress={() => router.push('/settings')}>
              <Text style={styles.headerButton}>설정</Text>
            </TouchableOpacity>
          ),
        }}
      />
      <Text>프로필 화면</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  headerButton: { color: '#007AFF', fontSize: 16 },
});

실전 예제 — 탭 + 스택 조합 구조

대부분의 앱이 사용하는 구조입니다.

app/
├── _layout.tsx              # 루트 (Stack)
├── (tabs)/
│   ├── _layout.tsx          # 하단 탭
│   ├── index.tsx            # 홈 탭
│   ├── search.tsx           # 검색 탭
│   └── profile.tsx          # 프로필 탭
├── post/
│   └── [id].tsx             # 게시글 상세 (탭 위에 스택으로 쌓임)
└── modal.tsx                # 모달
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      {/* 탭 전체를 하나의 스크린으로 */}
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="post/[id]" options={{ title: '게시글' }} />
      <Stack.Screen name="modal" options={{ presentation: 'modal' }} />
    </Stack>
  );
}

Next.js vs Expo Router 비교

항목Next.js (App Router)Expo Router
라우팅 방식파일 기반파일 기반 ✅ 동일
동적 라우팅[id].tsx[id].tsx ✅ 동일
레이아웃_layout.tsx_layout.tsx ✅ 동일
이동 방법useRouter, <Link>useRouter, <Link> ✅ 동일
네비게이션 타입웹 히스토리스택 / 탭 / 모달

Next.js 경험이 있다면 Expo Router는 매우 빠르게 익힐 수 있습니다.


정리

상황방법
선언적 이동<Link href="/path">
코드로 이동router.push('/path')
파라미터 전달href="/post/123" 또는 params
파라미터 수신useLocalSearchParams()
스택 구성<Stack> + <Stack.Screen>
탭 구성<Tabs> + <Tabs.Screen>
모달presentation: 'modal'

다음 편에서는 상태 관리와 API 연동으로 실제 데이터를 다루는 방법을 살펴보겠습니다.


시리즈 목차

  • 1편 Expo 개발 환경 세팅 & 첫 앱 실행
  • 2편 화면 구성 & 핵심 컴포넌트
  • 3편 화면 이동 구현 (Expo Router) ← 현재 글
  • 4편 상태 관리 & API 연동
  • 5편 디바이스 기능 활용 (카메라, 위치, 알림)
  • 6편 Firebase 연동
  • 7편 빌드 & 앱스토어 배포



댓글 남기기