본문으로 건너뛰기
Previous
Next
Expo Complete Guide | React Native Development Made Easy

Expo Complete Guide | React Native Development Made Easy

Expo Complete Guide | React Native Development Made Easy

이 글의 핵심

Expo removes the hard parts of React Native — native builds, code signing, OTA updates — so you can focus on the app. This guide covers everything from project setup to deploying to both app stores with EAS.

Why Expo?

React Native requires Xcode, Android Studio, certificates, and build configuration. Expo abstracts all of that:

  • Expo Go: test on device instantly, no build needed
  • EAS Build: build iOS/Android in the cloud (no Mac needed for iOS)
  • EAS Update: push JS-only updates without app store review
  • Expo Router: file-based routing (like Next.js for mobile)
  • SDK: 50+ pre-built native modules (camera, location, notifications)

1. Quick Start

# Create new project
npx create-expo-app@latest my-app
cd my-app

# Start development server
npx expo start

# Then press:
# i → iOS Simulator
# a → Android Emulator
# Scan QR code → Expo Go on physical device

2. Expo Router — File-Based Navigation

Expo Router is the recommended navigation system (replaces React Navigation boilerplate).

app/
  _layout.tsx        → root layout (tabs, drawer, stack)
  index.tsx          → / (home screen)
  (tabs)/
    _layout.tsx      → tab bar configuration
    index.tsx        → first tab
    settings.tsx     → second tab
  users/
    [id].tsx         → /users/123 (dynamic route)
  (auth)/
    login.tsx        → /login
    register.tsx     → /register

Root layout

// app/_layout.tsx
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'

export default function RootLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="(auth)" options={{ headerShown: false }} />
        <Stack.Screen name="users/[id]" options={{ title: 'User Profile' }} />
      </Stack>
    </>
  )
}

Tab layout

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

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#007AFF' }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="settings"
        options={{
          title: 'Settings',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="settings" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  )
}
// app/index.tsx
import { Link, router } from 'expo-router'
import { View, Text, TouchableOpacity } from 'react-native'

export default function HomeScreen() {
  return (
    <View>
      {/* Declarative navigation */}
      <Link href="/settings">Settings</Link>
      <Link href="/users/123">User 123</Link>

      {/* Programmatic navigation */}
      <TouchableOpacity onPress={() => router.push('/settings')}>
        <Text>Go to Settings</Text>
      </TouchableOpacity>

      <TouchableOpacity onPress={() => router.replace('/(auth)/login')}>
        <Text>Logout</Text>
      </TouchableOpacity>
    </View>
  )
}

Dynamic routes

// app/users/[id].tsx
import { useLocalSearchParams } from 'expo-router'
import { useEffect, useState } from 'react'

export default function UserScreen() {
  const { id } = useLocalSearchParams<{ id: string }>()
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetchUser(id).then(setUser)
  }, [id])

  return (
    <View>
      <Text>User ID: {id}</Text>
      {user && <Text>{user.name}</Text>}
    </View>
  )
}

3. Common Native Features

Camera

npx expo install expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera'
import { useState } from 'react'

export default function CameraScreen() {
  const [permission, requestPermission] = useCameraPermissions()

  if (!permission?.granted) {
    return (
      <View>
        <Text>Camera access needed</Text>
        <Button title="Grant Permission" onPress={requestPermission} />
      </View>
    )
  }

  return (
    <CameraView style={{ flex: 1 }} facing="back">
      <View style={{ position: 'absolute', bottom: 40, alignSelf: 'center' }}>
        <Button title="Take Photo" onPress={() => {/* capture */}} />
      </View>
    </CameraView>
  )
}

Location

npx expo install expo-location
import * as Location from 'expo-location'

async function getCurrentLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync()
  if (status !== 'granted') {
    console.log('Permission denied')
    return
  }

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.High,
  })

  console.log(location.coords.latitude, location.coords.longitude)
}

SecureStore (sensitive data)

npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store'

// Store auth token securely (encrypted on device)
await SecureStore.setItemAsync('authToken', token)

// Retrieve
const token = await SecureStore.getItemAsync('authToken')

// Delete
await SecureStore.deleteItemAsync('authToken')

4. Push Notifications

npx expo install expo-notifications expo-device
import * as Notifications from 'expo-notifications'
import * as Device from 'expo-device'

// Configure notification behavior
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
})

async function registerForPushNotifications() {
  if (!Device.isDevice) {
    console.log('Push notifications require a physical device')
    return
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync()
  let finalStatus = existingStatus

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync()
    finalStatus = status
  }

  if (finalStatus !== 'granted') {
    console.log('Push notification permission denied')
    return
  }

  // Get Expo push token
  const token = await Notifications.getExpoPushTokenAsync({
    projectId: 'your-project-id',  // from app.json
  })

  // Send token to your server
  await saveTokenToServer(token.data)

  return token
}

// Listen for notifications
function NotificationListener() {
  useEffect(() => {
    const sub1 = Notifications.addNotificationReceivedListener(notification => {
      console.log('Notification received:', notification)
    })

    const sub2 = Notifications.addNotificationResponseReceivedListener(response => {
      console.log('User tapped notification:', response)
      // Navigate based on notification data
      const data = response.notification.request.content.data
      if (data.screen) router.push(data.screen)
    })

    return () => {
      sub1.remove()
      sub2.remove()
    }
  }, [])
}

5. app.json / app.config.js

// app.config.js (dynamic config — preferred over app.json)
export default {
  expo: {
    name: 'My App',
    slug: 'my-app',
    version: '1.2.0',
    orientation: 'portrait',
    icon: './assets/icon.png',
    splash: {
      image: './assets/splash.png',
      resizeMode: 'contain',
      backgroundColor: '#ffffff',
    },
    ios: {
      bundleIdentifier: 'com.mycompany.myapp',
      buildNumber: '1',
      supportsTablet: true,
      infoPlist: {
        NSCameraUsageDescription: 'This app uses the camera to take photos.',
        NSLocationWhenInUseUsageDescription: 'This app uses your location to show nearby places.',
      },
    },
    android: {
      package: 'com.mycompany.myapp',
      versionCode: 1,
      adaptiveIcon: {
        foregroundImage: './assets/adaptive-icon.png',
        backgroundColor: '#ffffff',
      },
      permissions: ['ACCESS_FINE_LOCATION'],
    },
    plugins: [
      'expo-router',
      'expo-notifications',
      ['expo-camera', { cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera.' }],
    ],
    extra: {
      apiUrl: process.env.API_URL ?? 'https://api.myapp.com',
      eas: { projectId: 'your-eas-project-id' },
    },
  },
}

6. EAS Build — Cloud Builds

# Install EAS CLI
npm install -g eas-cli

# Login
eas login

# Configure
eas build:configure
// eas.json
{
  "cli": { "version": ">= 5.0.0" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "ios": { "simulator": true }
    },
    "production": {
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {}
  }
}
# Build for iOS (no Mac needed — runs in cloud)
eas build --platform ios --profile production

# Build for Android
eas build --platform android --profile production

# Build both
eas build --platform all --profile production

# Build development client (custom dev build)
eas build --platform all --profile development

7. EAS Update — Over-The-Air Updates

Push JS-only updates without going through app stores:

# Install expo-updates
npx expo install expo-updates

# Publish update to production channel
eas update --branch production --message "Fix login bug"

# Update specific platform
eas update --branch production --platform ios

# Preview updates before production
eas update --branch staging --message "New feature test"
// Check for updates programmatically
import * as Updates from 'expo-updates'

async function checkForUpdates() {
  if (__DEV__) return  // skip in development

  const update = await Updates.checkForUpdateAsync()
  if (update.isAvailable) {
    await Updates.fetchUpdateAsync()
    await Updates.reloadAsync()  // restart with new code
  }
}

8. EAS Submit — App Store Submission

# Submit iOS to App Store (requires Apple credentials)
eas submit --platform ios --latest

# Submit Android to Play Store
eas submit --platform android --latest

# Or submit specific build
eas submit --platform ios --id <build-id>

Expo vs Bare React Native

Expo (managed)Bare React Native
SetupMinutesHours
Native codeVia pluginsFull access
OTA updatesEAS UpdateManual (CodePush)
BuildEAS Build (cloud)Local + CI
EcosystemExpo SDKFull npm
When to useMost appsCustom native modules

Key Takeaways

  • Expo Router: file-based navigation — same mental model as Next.js
  • Expo SDK: camera, location, notifications, secure storage without native setup
  • EAS Build: build iOS without a Mac — critical for teams without Apple hardware
  • EAS Update: push bug fixes and content updates without app store review delays
  • Development builds: use when you need native modules not in Expo Go
  • app.config.js: dynamic config with environment variables for API URLs and feature flags

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Build React Native apps with Expo. Covers Expo Router, EAS Build, native modules, push notifications, over-the-air updat… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [React Native Complete Guide | Mobile Apps with JavaScript](/en/blog/react-native-complete-guide-en/
  • [React Hooks Deep Dive | useEffect· useMemo](/en/blog/react-hooks-deep-dive/
  • [TypeScript 5 Complete Guide | Decorators· satisfies](/en/blog/typescript-5-complete-guide-en/

이 글에서 다루는 키워드 (관련 검색어)

Expo, React Native, Mobile, iOS, Android, JavaScript 등으로 검색하시면 이 글이 도움이 됩니다.