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.Screen์˜ options๋ฅผ ํ†ตํ•ด ํŠน์ • ํ™”๋ฉด์˜ ์„ค์ •์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (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.Screen์˜ options๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ๋™์ ์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์— ๋”ฐ๋ผ ํ—ค๋” ์ œ๋ชฉ์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๋” ๋ณต์žกํ•œ 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.Screen์˜ presentation ์˜ต์…˜์„ '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 API๋Š” push, replace, back ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์˜ ์„ธ๋ฐ€ํ•œ ๋‚ด๋น„๊ฒŒ์ด์…˜ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋ผ์šฐํŠธ ๋ณดํ˜ธ๋Š” ์„œ๋ฒ„ ๋ฏธ๋“ค์›จ์–ด์™€ ํด๋ผ์ด์–ธํŠธ ๋ ˆ์ด์•„์›ƒ ๊ฐ€๋“œ์˜ ์กฐํ•ฉ์œผ๋กœ ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ์•ˆ์ „ํ•œ ์ธ์ฆ ์ฒด๊ณ„๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Expo Router๋ฅผ ๋„์ž…ํ•˜๋ฉด ๋‚ด๋น„๊ฒŒ์ด์…˜ ์„ค์ •์— ์†Œ์š”๋˜๋Š” ์‹œ๊ฐ„์„ ๋Œ€ํญ ์ค„์ด๊ณ , ์•ฑ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŒŒ์ผ ๊ตฌ์กฐ์˜ ๋ณ€๊ฒฝ๋งŒ์œผ๋กœ ๋‚ด๋น„๊ฒŒ์ด์…˜์„ ๋ฆฌํŒฉํ† ๋งํ•  ์ˆ˜ ์žˆ์–ด ํ”„๋กœ์ ํŠธ์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ๋„ ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

ํƒœ๊ทธ

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

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

React Native 0.84์—์„œ Hermes V1์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™”

React Native 0.84์˜ Hermes V1: ์„ฑ๋Šฅ ์ตœ์ ํ™”, ํ”„๋ฆฌ์ปดํŒŒ์ผ ๋ฐ”์ดํŠธ์ฝ”๋“œ, ๊ธฐ์ˆ  ๋ฉด์ ‘ ๊ฐ€์ด๋“œ

Hermes V1์ด React Native 0.84์˜ ๊ธฐ๋ณธ JavaScript ์—”์ง„์œผ๋กœ ์ฑ„ํƒ๋˜์–ด ๋ฐ”์ดํŠธ์ฝ”๋“œ ํ”„๋ฆฌ์ปดํŒŒ์ผ, Hades ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰ํ„ฐ, ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ํ†ตํ•ด ํš๊ธฐ์ ์ธ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์ˆ  ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” Hermes ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

React Native 0.86 ์—ฃ์ง€ํˆฌ์—ฃ์ง€ Android ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ

React Native 0.86 ์™„๋ฒฝ ๊ฐ€์ด๋“œ 2026: Android 15 ์—ฃ์ง€ํˆฌ์—ฃ์ง€ ์ง€์›๊ณผ DevTools ํ™œ์šฉ๋ฒ•

React Native 0.86์˜ Android 15 ์—ฃ์ง€ํˆฌ์—ฃ์ง€ ์ง€์›, DevTools ๊ฐœ์„  ์‚ฌํ•ญ, ๋ฉด์ ‘ ๋Œ€๋น„ ์งˆ๋ฌธ์„ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์‹ค์šฉ์ ์ธ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ์ตœ์‹  ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

React Native TypeScript ํƒ€์ž… ์•ˆ์ „ ์•„ํ‚คํ…์ฒ˜์™€ ์ฝ”๋“œ, ๋ชจ๋ฐ”์ผ ๋””๋ฐ”์ด์Šค

React Native์™€ TypeScript 2026: ํƒ€์ž… ์•ˆ์ „ํ•œ ์•„ํ‚คํ…์ฒ˜์™€ ๋ฉด์ ‘ ์งˆ๋ฌธ

2026๋…„ React Native์—์„œ TypeScript๋ฅผ ํ™œ์šฉํ•œ ํƒ€์ž… ์•ˆ์ „ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค. Codegen, TurboModules, Strict TypeScript API, ํƒ€์ž… ์•ˆ์ „ ๋‚ด๋น„๊ฒŒ์ด์…˜, ๋ฉด์ ‘ ์งˆ๋ฌธ์„ ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.