๐ฑ RN/Expo ๊ธฐ๋ฅ ๊ตฌํ ์ด์ ๋ฆฌ
์์ฝ ํ ์ด๋ธ
| ๊ธฐ๋ฅ | ๊ถ์ฅ ๋ผ์ด๋ธ๋ฌ๋ฆฌ / 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>
- iOS:
- ๋ฅ๋งํฌ:
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'); // ๋ฏผ๊ฐ์ ๋ณด