posts

๐Ÿ“ฑ RN/Expo ๊ธฐ๋Šฅ ๊ตฌํ˜„ ์ด์ •๋ฆฌ

Oct 1, 2025 updated Oct 1, 2025 architectureexporeact-native

์š”์•ฝ ํ…Œ์ด๋ธ”

๊ธฐ๋Šฅ ๊ถŒ์žฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ / API (Expo ์šฐ์„ ) iOS/Android ๊ถŒํ•œ & ์„ค์ • ์ฃผ์š” ์ฃผ์˜์‚ฌํ•ญ / ํ•œ๊ณ„ ๋‚œ์ด๋„* ์˜คํ”„๋ผ์ธ/๋ฐฑ๊ทธ๋ผ์šด๋“œ
์นด๋ฉ”๋ผ ์ดฌ์˜ expo-camera iOS: NSCameraUsageDescription / AOS: CAMERA ์‹ค์‹œ๊ฐ„ ์Šค์บ”/์คŒ/ํ”Œ๋ž˜์‹œ ๋“ฑ์€ ๋””๋ฐ”์ด์Šค๋ณ„ ์ฐจ์ด. QR์Šค์บ”์€ expo-barcode-scanner ๋˜๋Š” expo-camera ๋‚ด ๊ธฐ๋Šฅ ์‚ฌ์šฉ 2 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โŒ
๊ฐค๋Ÿฌ๋ฆฌ(์•จ๋ฒ”) ์ ‘๊ทผ/์ €์žฅ expo-image-picker(์„ ํƒ) + expo-media-library(์ €์žฅ) iOS: NSPhotoLibraryUsageDescription / AOS: READ/WRITE_EXTERNAL_STORAGE(Android 13+๋Š” ๊ถŒํ•œ ์ฒด๊ณ„ ๋ณ€๊ฒฝ) iOS ์‚ฌ์ง„ โ€œ์„ ํƒ๋œ ์‚ฌ์ง„๋งŒ ํ—ˆ์šฉโ€ ์ƒํƒœ ๋Œ€์‘ ํ•„์š”. ์ €์žฅ์€ MediaLibrary ์‚ฌ์šฉ 2 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โŒ
GPS(์œ„์น˜) expo-location iOS: NSLocationWhenInUseUsageDescription(ํ•„์š” ์‹œ Always) / AOS: ACCESS_COARSE/ACCESS_FINE_LOCATION ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํŠธ๋ž˜ํ‚น์€ ์ถ”๊ฐ€ ์„ค์ •ยท๋ฐฐํ„ฐ๋ฆฌ ์ด์Šˆ. ์ •ํ™•๋„/๋นˆ๋„ ์กฐ์ ˆ ํ•„์š” 3 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โš ๏ธ(์„ค์ • ํ•„์š”)
ํ‘ธ์‹œ ์•Œ๋ฆผ(์›๊ฒฉ) expo-notifications (+ Expo Push, FCM/APNs ์—ฐ๋™) iOS: ์•Œ๋ฆผ ๊ถŒํ•œ / AOS: ์ฑ„๋„ ์„ค์ • ํ•„์ˆ˜ ๋””๋ฐ”์ด์Šค ํ† ํฐ ๋ฐœ๊ธ‰/๋ฐฑ์—”๋“œ ์—ฐ๋™ ํ•„์š”. AOS ์ฑ„๋„(์ค‘์š”๋„/์‚ฌ์šด๋“œ) ์‚ฌ์ „ ์ƒ์„ฑ 4 ์˜คํ”„๋ผ์ธ: โŒ / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โœ…
๋กœ์ปฌ ์•Œ๋ฆผ(์Šค์ผ€์ค„) expo-notifications iOS/AOS ์•Œ๋ฆผ ๊ถŒํ•œ ์˜ˆ์•ฝ/๋ฐ˜๋ณต ์Šค์ผ€์ค„ ์ง€์›. AOS ์ฑ„๋„ ์ค€๋น„. ์•ฑ ์ข…๋ฃŒ ์ƒํƒœ์—์„œ๋„ ํ‘œ์‹œ 2 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โœ…
ํŒŒ์ผ(์ฝ๊ธฐ/์“ฐ๊ธฐ/๋‹ค์šด๋กœ๋“œ) expo-file-system / ์„ ํƒ: expo-document-picker ์ผ๋ฐ˜ ๊ถŒํ•œ ็„ก, ํŠน์ • ๊ฒฝ๋กœ ์ ‘๊ทผ์€ ์ œํ•œ. AOS ์ €์žฅ๊ณต๊ฐ„ ์Šค์ฝ”ํ”„ ์ฃผ์˜ ๋Œ€์šฉ๋Ÿ‰ ๋‹ค์šด๋กœ๋“œ/์ง„ํ–‰๋ฅ /์ค‘๋‹จ ์žฌ๊ฐœ ๋“ฑ ๊ตฌํ˜„ ์‹œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•„์š” 3 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โš ๏ธ(์ œ์•ฝ)
์™ธ๋ถ€ ๋งํฌ ์—ด๊ธฐ Linking.openURL(url) / ์ธ์•ฑ: expo-web-browser ๋ณ„๋„ ๊ถŒํ•œ ็„ก ์ธ์•ฑ ํƒญ์€ WebBrowser.openBrowserAsync. ๋”ฅ๋งํฌ/์ปค์Šคํ…€์Šคํ‚ด์€ expo-linking 1 ์˜คํ”„๋ผ์ธ: โŒ / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โŒ
๊ณต์œ ํ•˜๊ธฐ(๊ณต์œ  ์‹œํŠธ) RN ๋‚ด์žฅ Share / ํŒŒ์ผ ๊ณต์œ : expo-sharing ๊ถŒํ•œ ็„ก(ํŒŒ์ผ ์•ก์„ธ์Šค๋Š” ๋ณ„๋„) ํŒŒ์ผ์€ ๋กœ์ปฌ ๊ฒฝ๋กœ/URI ํ•„์š”. ์ผ๋ถ€ ์•ฑ์œผ๋กœ ๊ณต์œ  ์ œํ•œ ์กด์žฌ 1-2 ์˜คํ”„๋ผ์ธ: โœ… / ๋ฐฑ๊ทธ๋ผ์šด๋“œ: โŒ
์„ค์ • ํ™”๋ฉด ์—ด๊ธฐ RN Linking.openSettings() ็„ก iOS๋Š” ์•ฑ ์„ค์ •์œผ๋กœ ์ด๋™, AOS๋Š” ๊ธฐ๊ธฐยทOS๋ณ„ ํŽธ์ฐจ 1 โ€”
์Šคํ† ์–ด ์—ด๊ธฐ(๋ฆฌ๋ทฐ/์„ค์น˜) Linking.openURL()๋กœ ์Šคํ† ์–ด ์Šคํ‚ด ็„ก iOS: itms-apps://itunes.apple.com/app/id{appId} / AOS: market://details?id={pkg} 1 โ€”
๋งํฌ๋ฅผ ๋ธŒ๋ผ์šฐ์ €๋กœ ์‹คํ–‰ Linking.openURL(url) / ์ธ์•ฑ: expo-web-browser ็„ก ์™ธ๋ถ€ ๋ธŒ๋ผ์šฐ์ € ๊ฐ•์ œ๋Š” Linking. ์ธ์•ฑ์€ WebBrowser 1 โ€”
์ „ํ™” ์—ฐ๊ฒฐ Linking.openURL('tel:${number}') ็„ก iOS ์ผ๋ถ€์—์„œ ํ™•์ธ ํŒ์—…. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ œํ•œ 1 โ€”
ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ expo-clipboard ็„ก ๋Œ€์šฉ๋Ÿ‰ ํ…์ŠคํŠธ/์ด๋ฏธ์ง€ ์ œ์•ฝ. ์„ฑ๊ณต/์‹คํŒจ UX ์ œ๊ณต 1 ์˜คํ”„๋ผ์ธ: โœ…
๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ๊ธฐ๋ณธ: @react-native-async-storage/async-storage / ๋ฏผ๊ฐ์ •๋ณด: expo-secure-store ็„ก ๋ฏผ๊ฐ์ •๋ณด๋Š” SecureStore(ํ‚ค์ฒด์ธ/Keystore). ๋นˆ๋ฒˆ ์ ‘๊ทผ/์„ฑ๋Šฅ์€ MMKV(์ถ”ํ›„) 2 ์˜คํ”„๋ผ์ธ: โœ…

*๋‚œ์ด๋„(์ฒด๊ฐ): 1(๋งค์šฐ ์‰ฌ์›€) ~ 5(์–ด๋ ต๊ณ  ์ธํ”„๋ผ ํ•„์š”)


๊ตฌํ˜„ ํŒ & ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๊ถŒํ•œ/ํ”„๋กฌํ”„ํŠธ

  • iOS: app.json/app.config.ts์˜ infoPlist์— ์šฉ๋„ ์„ค๋ช…(์˜ˆ: NSCameraUsageDescription) ๋ฐ˜๋“œ์‹œ ์ถ”๊ฐ€.
  • Android: android.permissions์— ํ•„์š”ํ•œ ๊ถŒํ•œ ์„ ์–ธ, Android 13+๋Š” ๋ฏธ๋””์–ด ์ ‘๊ทผ ๊ถŒํ•œ์ด ์„ธ๋ถ„ํ™”(์ด๋ฏธ์ง€/๋น„๋””์˜ค/์˜ค๋””์˜ค)๋˜์—ˆ์Œ.

ํ‘ธ์‹œ/์•Œ๋ฆผ

  • ์ฑ„๋„(AOS): Notifications.setNotificationChannelAsync('default', { importance: Notifications.AndroidImportance.HIGH, ... }) ์„ ํ–‰.
  • ํ† ํฐ: getExpoPushTokenAsync() ๋˜๋Š” FCM/APNs ์ง์ ‘ ํ†ตํ•ฉ. ์„œ๋ฒ„์—์„œ ํ† ํฐ ์ €์žฅ/ํƒ€๊ฒŸํŒ… ๋ฐœ์†ก ํ•„์š”.
  • ๋กœ์ปฌ ์•Œ๋ฆผ: ์˜ˆ์•ฝ/๋ฐ˜๋ณต, ๋ฐฐ์ง€/์‚ฌ์šด๋“œ ์„ค์ • ๊ฐ€๋Šฅ. ๊ถŒํ•œ ๊ฑฐ๋ถ€ ์‹œ ๋Œ€์ฒด UX ์ค€๋น„.

ํŒŒ์ผ/๊ฐค๋Ÿฌ๋ฆฌ

  • ๋‹ค์šด๋กœ๋“œ ๊ฒฝ๋กœ: ์•ฑ ์ƒŒ๋“œ๋ฐ•์Šค ๋‚ด URI ์‚ฌ์šฉ. ๊ฐค๋Ÿฌ๋ฆฌ์— ์ €์žฅํ•˜๋ ค๋ฉด expo-media-library.
  • ๋ฌธ์„œ ์„ ํƒ: ์‚ฌ์šฉ์ž ํŒŒ์ผ ํ”ผ์ปค๋Š” expo-document-picker(MIME ํ•„ํ„ฐ).

์œ„์น˜

  • ์ •ํ™•๋„/๋ฐฐํ„ฐ๋ฆฌ: accuracy ์˜ต์…˜์„ ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๊ตฌ๋ถ„(์ง€๋„ ์ฆ‰์‹œ ์œ„์น˜ vs. ํŠธ๋ž˜ํ‚น).
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ: ์‹ค์ œ ์ƒ์‹œ ํŠธ๋ž˜ํ‚น์€ ์ •์ฑ…/๋ฐฐํ„ฐ๋ฆฌ ์ด์Šˆ ํผ โ†’ ์‚ฌ์šฉ์ž ๊ฐ€์น˜/์„ค์ • ํ† ๊ธ€ ์ œ๊ณต ํ•„์ˆ˜.

๋งํฌ/์Šคํ† ์–ด/์ „ํ™”

  • ์Šคํ† ์–ด ๋งํฌ ํฌ๋งท
    • iOS: itms-apps://itunes.apple.com/app/id<APP_ID>
    • Android: market://details?id=<PACKAGE_NAME>
  • ๋”ฅ๋งํฌ: expo-linking์œผ๋กœ ์Šคํ‚ด/์œ ๋‹ˆ๋ฒ„์„ค ๋งํฌ ์ •์˜. ์™ธ๋ถ€ ์•ฑ ํ˜ธ์ถœ ์‹œ ์‹คํŒจ ํ•ธ๋“ค๋ง(try/catch) ํ•„์ˆ˜.

๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€

  • ์ผ๋ฐ˜ ๋ฐ์ดํ„ฐ: AsyncStorage(ํ‚ค-๊ฐ’).
  • ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ(ํ† ํฐ ๋“ฑ): expo-secure-store(์•”ํ˜ธํ™” ์ €์žฅ).
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋งค์šฐ ๋นˆ๋ฒˆ/๋Œ€์šฉ๋Ÿ‰์ด๋ฉด react-native-mmkv(Expo Dev Client ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ)๋ฅผ ์ฐจ๊ธฐ ๋‹จ๊ณ„๋กœ ๊ณ ๋ ค.

์ตœ์†Œ ์˜ˆ์‹œ(์Šค๋‹ˆํŽซ ๋ชจ์Œ)

ํ‘ธ์‹œ ์ฑ„๋„/๊ถŒํ•œ/ํ† ํฐ

import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

export async function initNotifications() {
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.HIGH,
    });
  }
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') return null;
  const token = await Notifications.getExpoPushTokenAsync();
  return token.data; // ์„œ๋ฒ„๋กœ ์ „์†ก
}

๋กœ์ปฌ ์•Œ๋ฆผ ์Šค์ผ€์ค„

await Notifications.scheduleNotificationAsync({
  content: { title: '๋ฆฌ๋งˆ์ธ๋”', body: '์ง€๊ธˆ ํ™•์ธํ•˜์„ธ์š”' },
  trigger: { seconds: 10, repeats: false },
});

์นด๋ฉ”๋ผ/์ด๋ฏธ์ง€ ์„ ํƒ

import * as ImagePicker from 'expo-image-picker';
const res = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images });
if (!res.canceled) {
  const uri = res.assets[0].uri; // ์—…๋กœ๋“œ/ํ‘œ์‹œ
}

์™ธ๋ถ€ ๋งํฌ/์ธ์•ฑ ๋ธŒ๋ผ์šฐ์ €/์ „ํ™”

import { Linking } from 'react-native';
import * as WebBrowser from 'expo-web-browser';

await Linking.openURL('tel:01012345678');
await Linking.openURL('market://details?id=com.example');
await WebBrowser.openBrowserAsync('https://example.com'); // ์ธ์•ฑ ํƒญ

ํด๋ฆฝ๋ณด๋“œ/์Šคํ† ๋ฆฌ์ง€

import * as Clipboard from 'expo-clipboard';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';

await Clipboard.setStringAsync('๋ณต์‚ฌํ•  ํ…์ŠคํŠธ');
await AsyncStorage.setItem('key', JSON.stringify({ v: 1 }));
await SecureStore.setItemAsync('token', 'SECRET'); // ๋ฏผ๊ฐ์ •๋ณด