# React Native: 2026년 완성도 높은 모바일 앱 구축하기
> React Native를 활용한 iOS, Android 모바일 앱 개발 실전 가이드입니다. 환경 설정부터 스토어 배포까지 핵심 기초를 모두 다룹니다.
- Published: 2026-01-10
- Updated: 2026-04-06
- Author: SharpSkill
- Tags: react native, mobile development, javascript, ios, android
- Reading time: 15 min
---
React Native는 하나의 JavaScript 코드베이스로 iOS와 Android 네이티브 모바일 앱을 동시에 구축할 수 있는 프레임워크입니다. Meta가 개발하고 유지 관리하며, Instagram, Facebook, Discord 등의 앱에서 채택하고 있습니다. 빠른 개발 속도와 높은 성능을 동시에 달성할 수 있습니다.
> **2026년에 React Native를 선택하는 이유**
>
> 새로운 아키텍처(Fabric과 TurboModules)가 안정화되면서, React Native는 네이티브에 근접한 성능을 유지하면서도 웹 개발의 생산성을 그대로 활용할 수 있습니다. 상위 500개 앱 중 40% 이상이 React Native를 사용하고 있습니다.
## 개발 환경 설정
개발을 시작하기 전에 필요한 도구를 설치해야 합니다. React Native에는 두 가지 접근 방식이 있습니다. Expo(초보자에게 권장)와 React Native CLI(더 세밀한 제어가 필요한 경우)입니다.
```bash
# setup.sh
# Node.js installation (LTS version recommended)
# Check installed version
node --version # >= 18.x required
npm --version # >= 9.x required
# Install Expo CLI tool globally
npm install -g expo-cli
# Create a new project with Expo
npx create-expo-app@latest MyApp --template blank-typescript
# Navigate to the project
cd MyApp
# Start the development server
npx expo start
```
Expo를 사용하면 네이티브 설정을 자동으로 처리해 주므로 개발이 훨씬 수월해집니다. 앱을 테스트하려면 스마트폰에 Expo Go 앱을 설치하고 화면에 표시되는 QR 코드를 스캔하면 됩니다.
```bash
# structure.sh
# Generated project structure
MyApp/
├── App.tsx # Application entry point
├── app.json # Expo configuration
├── package.json # Dependencies
├── tsconfig.json # TypeScript configuration
├── babel.config.js # Babel configuration
└── assets/ # Images and resources
├── icon.png
└── splash.png
```
## React Native 핵심 컴포넌트
React Native는 HTML 요소 대신 네이티브 컴포넌트를 제공합니다. 각 컴포넌트는 iOS 또는 Android의 네이티브 컴포넌트로 직접 변환됩니다.
```tsx
// App.tsx
import React from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
StatusBar
} from 'react-native';
// Main application component
export default function App() {
return (
// SafeAreaView prevents overlap with status bar
{/* StatusBar configures system bar appearance */}
{/* View is the equivalent of div */}
{/* Text is required to display text */}
Welcome to MyApp
A React Native application
);
}
// StyleSheet.create optimizes styles for native
const styles = StyleSheet.create({
container: {
flex: 1, // Takes all available space
backgroundColor: '#ffffff',
},
header: {
padding: 20,
alignItems: 'center', // Centers horizontally
justifyContent: 'center', // Centers vertically
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#1a1a1a',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666666',
},
});
```
> **기본값은 Flexbox**
>
> React Native에서는 `flexDirection: 'column'`이 기본값입니다(웹에서는 `row`가 기본값). 이 차이는 레이아웃 설계 시 반드시 알아두어야 합니다.
## 사용자 인터랙션 처리
터치 상호작용은 전용 컴포넌트로 관리합니다. 각 인터랙션 유형에 맞게 성능이 최적화된 전용 컴포넌트가 준비되어 있습니다.
```tsx
// components/InteractiveButton.tsx
import React, { useState } from 'react';
import {
TouchableOpacity,
TouchableHighlight,
Pressable,
Text,
StyleSheet,
View,
} from 'react-native';
// Button component with different interaction styles
export function InteractiveButton() {
const [count, setCount] = useState(0);
return (
{/* TouchableOpacity reduces opacity on touch */}
setCount(c => c + 1)}
>
Counter: {count}
{/* Pressable offers more control over states */}
[
styles.button,
styles.pressableButton,
pressed && styles.buttonPressed,
]}
onPress={() => console.log('Pressed!')}
onLongPress={() => console.log('Long press!')}
>
{({ pressed }) => (
{pressed ? 'Pressed!' : 'Press here'}
)}
);
}
const styles = StyleSheet.create({
container: {
gap: 16,
padding: 20,
},
button: {
backgroundColor: '#007AFF',
paddingVertical: 14,
paddingHorizontal: 28,
borderRadius: 10,
alignItems: 'center',
},
pressableButton: {
backgroundColor: '#34C759',
},
buttonPressed: {
backgroundColor: '#2DA44E',
transform: [{ scale: 0.98 }],
},
buttonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
});
```
## React Navigation을 활용한 화면 전환
내비게이션은 모바일 앱에서 필수적인 기능입니다. React Navigation은 모바일 패턴에 최적화된 다양한 내비게이션 방식을 제공하는 표준 솔루션입니다.
```bash
# install-navigation.sh
# Installing navigation dependencies
npx expo install @react-navigation/native
npx expo install @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context
```
```tsx
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// Screen imports
import { HomeScreen } from './screens/HomeScreen';
import { DetailScreen } from './screens/DetailScreen';
import { ProfileScreen } from './screens/ProfileScreen';
// Type definition for TypeScript navigation
export type RootStackParamList = {
Home: undefined; // No parameters
Detail: { itemId: number; title: string }; // Required parameters
Profile: { userId?: string }; // Optional parameter
};
// Creating typed navigator
const Stack = createNativeStackNavigator();
export default function App() {
return (
// NavigationContainer manages navigation state
({
title: route.params.title
})}
/>
);
}
```
```tsx
// screens/HomeScreen.tsx
import React from 'react';
import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../App';
// Typing navigation props
type Props = NativeStackScreenProps;
// Sample data
const ITEMS = [
{ id: 1, title: 'First item' },
{ id: 2, title: 'Second item' },
{ id: 3, title: 'Third item' },
];
export function HomeScreen({ navigation }: Props) {
return (
{/* FlatList for performant lists */}
item.id.toString()}
renderItem={({ item }) => (
{
// Navigation with typed parameters
navigation.navigate('Detail', {
itemId: item.id,
title: item.title,
});
}}
>
{item.title}
)}
ItemSeparatorComponent={() => }
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
item: {
backgroundColor: '#ffffff',
padding: 20,
},
itemText: {
fontSize: 16,
color: '#1a1a1a',
},
separator: {
height: 1,
backgroundColor: '#e0e0e0',
},
});
```
## Context와 Hooks를 활용한 상태 관리
단순하거나 중간 규모의 상태 관리에는 React Context와 Hooks의 조합이 외부 라이브러리 없이도 효과적인 솔루션이 됩니다.
```tsx
// context/AuthContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';
// Types for authentication
interface User {
id: string;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
isLoading: boolean;
signIn: (email: string, password: string) => Promise;
signOut: () => void;
}
// Creating context with default value
const AuthContext = createContext(undefined);
// Provider that wraps the application
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
// Sign in function
const signIn = useCallback(async (email: string, password: string) => {
setIsLoading(true);
try {
// Simulated API call
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setUser(data.user);
} catch (error) {
console.error('Login error:', error);
throw error;
} finally {
setIsLoading(false);
}
}, []);
// Sign out function
const signOut = useCallback(() => {
setUser(null);
}, []);
return (
{children}
);
}
// Custom hook to use the context
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
```
```tsx
// screens/LoginScreen.tsx
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert,
} from 'react-native';
import { useAuth } from '../context/AuthContext';
export function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { signIn, isLoading } = useAuth();
// Form submission handler
const handleSubmit = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
try {
await signIn(email, password);
} catch (error) {
Alert.alert('Error', 'Invalid credentials');
}
};
return (
Login
{isLoading ? (
) : (
Sign In
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
backgroundColor: '#ffffff',
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 32,
textAlign: 'center',
},
input: {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 10,
padding: 16,
marginBottom: 16,
fontSize: 16,
backgroundColor: '#f9f9f9',
},
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 10,
alignItems: 'center',
marginTop: 8,
},
buttonDisabled: {
backgroundColor: '#99c9ff',
},
buttonText: {
color: '#ffffff',
fontSize: 18,
fontWeight: '600',
},
});
```
> **전역 상태와 로컬 상태**
>
> 공유 상태가 많은 복잡한 앱에서는 Zustand나 Redux Toolkit이 더 적합할 수 있습니다. Context는 변경 빈도가 낮은 상태(테마, 인증)에 최적입니다.
## API 호출과 데이터 관리
백엔드 통신은 모바일 앱의 핵심입니다. 여기서는 API 호출을 위한 추상화 레이어를 활용한 견고한 패턴을 소개합니다.
```tsx
// services/api.ts
// Base API configuration
const API_BASE_URL = 'https://api.example.com';
// Type for API errors
interface ApiError {
message: string;
code: string;
status: number;
}
// Utility function for requests
async function request(
endpoint: string,
options: RequestInit = {}
): Promise {
const url = `${API_BASE_URL}${endpoint}`;
const config: RequestInit = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message || 'An error occurred');
}
return response.json();
} catch (error) {
// Network error handling
if (error instanceof TypeError) {
throw new Error('Network connection issue');
}
throw error;
}
}
// Types for entities
interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
}
// Products service
export const productApi = {
// Get all products
getAll: () => request('/products'),
// Get product by ID
getById: (id: string) => request(`/products/${id}`),
// Create a product
create: (data: Omit) =>
request('/products', {
method: 'POST',
body: JSON.stringify(data),
}),
// Update a product
update: (id: string, data: Partial) =>
request(`/products/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
};
```
```tsx
// hooks/useProducts.ts
import { useState, useEffect, useCallback } from 'react';
import { productApi } from '../services/api';
interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
}
// Custom hook for product management
export function useProducts() {
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Initial products loading
const fetchProducts = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await productApi.getAll();
setProducts(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, []);
// Refresh (pull-to-refresh)
const refresh = useCallback(async () => {
await fetchProducts();
}, [fetchProducts]);
// Load on mount
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
return {
products,
isLoading,
error,
refresh,
};
}
```
```tsx
// screens/ProductListScreen.tsx
import React from 'react';
import {
View,
Text,
FlatList,
Image,
StyleSheet,
RefreshControl,
ActivityIndicator,
} from 'react-native';
import { useProducts } from '../hooks/useProducts';
export function ProductListScreen() {
const { products, isLoading, error, refresh } = useProducts();
// Display during initial loading
if (isLoading && products.length === 0) {
return (
Loading...
);
}
// Display on error
if (error) {
return (
{error}
);
}
return (
item.id}
contentContainerStyle={styles.list}
// Pull-to-refresh
refreshControl={
}
renderItem={({ item }) => (
{item.name}
${item.price.toFixed(2)}
)}
/>
);
}
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: '#666666',
},
errorText: {
fontSize: 16,
color: '#FF3B30',
textAlign: 'center',
},
list: {
padding: 16,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 12,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
image: {
width: '100%',
height: 200,
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
cardContent: {
padding: 16,
},
productName: {
fontSize: 18,
fontWeight: '600',
color: '#1a1a1a',
marginBottom: 4,
},
productPrice: {
fontSize: 16,
color: '#007AFF',
fontWeight: '500',
},
});
```
## 로컬 데이터 저장
로컬 저장소를 사용하면 세션 간에 데이터를 유지할 수 있습니다. 단순한 데이터에는 AsyncStorage가 표준 솔루션이며, 복잡한 구조화 데이터에는 SQLite가 적합합니다.
```tsx
// services/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
// Required installation: npx expo install @react-native-async-storage/async-storage
// Centralized storage keys
const STORAGE_KEYS = {
USER_TOKEN: '@app/user_token',
USER_PREFERENCES: '@app/user_preferences',
ONBOARDING_COMPLETE: '@app/onboarding_complete',
} as const;
// Types for user preferences
interface UserPreferences {
theme: 'light' | 'dark' | 'system';
notifications: boolean;
language: string;
}
// Typed storage service
export const storage = {
// Authentication token
async getToken(): Promise {
return AsyncStorage.getItem(STORAGE_KEYS.USER_TOKEN);
},
async setToken(token: string): Promise {
await AsyncStorage.setItem(STORAGE_KEYS.USER_TOKEN, token);
},
async removeToken(): Promise {
await AsyncStorage.removeItem(STORAGE_KEYS.USER_TOKEN);
},
// User preferences (JSON object)
async getPreferences(): Promise {
const data = await AsyncStorage.getItem(STORAGE_KEYS.USER_PREFERENCES);
return data ? JSON.parse(data) : null;
},
async setPreferences(prefs: UserPreferences): Promise {
await AsyncStorage.setItem(
STORAGE_KEYS.USER_PREFERENCES,
JSON.stringify(prefs)
);
},
// Onboarding
async isOnboardingComplete(): Promise {
const value = await AsyncStorage.getItem(STORAGE_KEYS.ONBOARDING_COMPLETE);
return value === 'true';
},
async setOnboardingComplete(): Promise {
await AsyncStorage.setItem(STORAGE_KEYS.ONBOARDING_COMPLETE, 'true');
},
// Complete cleanup
async clearAll(): Promise {
const keys = Object.values(STORAGE_KEYS);
await AsyncStorage.multiRemove(keys);
},
};
```
> **민감한 데이터 보안**
>
> 인증 토큰 등의 민감한 데이터에는 iOS Keychain과 Android Keystore를 통해 데이터를 암호화하는 expo-secure-store 사용을 권장합니다.
## 반응형 스타일과 테마 설정
완성도 높은 앱은 다양한 화면 크기에 대응하고 다크 모드를 지원해야 합니다.
```tsx
// theme/index.ts
import { Dimensions, PixelRatio, Platform } from 'react-native';
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
// Reference dimensions (iPhone 14)
const guidelineBaseWidth = 390;
const guidelineBaseHeight = 844;
// Scaling functions
export const scale = (size: number) =>
(SCREEN_WIDTH / guidelineBaseWidth) * size;
export const verticalScale = (size: number) =>
(SCREEN_HEIGHT / guidelineBaseHeight) * size;
export const moderateScale = (size: number, factor = 0.5) =>
size + (scale(size) - size) * factor;
// Light theme
export const lightTheme = {
colors: {
primary: '#007AFF',
secondary: '#5856D6',
success: '#34C759',
warning: '#FF9500',
error: '#FF3B30',
background: '#FFFFFF',
surface: '#F2F2F7',
text: '#000000',
textSecondary: '#8E8E93',
border: '#E5E5EA',
},
spacing: {
xs: scale(4),
sm: scale(8),
md: scale(16),
lg: scale(24),
xl: scale(32),
},
typography: {
h1: {
fontSize: moderateScale(32),
fontWeight: 'bold' as const,
lineHeight: moderateScale(40),
},
h2: {
fontSize: moderateScale(24),
fontWeight: 'bold' as const,
lineHeight: moderateScale(32),
},
body: {
fontSize: moderateScale(16),
lineHeight: moderateScale(24),
},
caption: {
fontSize: moderateScale(14),
lineHeight: moderateScale(20),
},
},
borderRadius: {
sm: scale(4),
md: scale(8),
lg: scale(12),
full: 9999,
},
};
// Dark theme
export const darkTheme = {
...lightTheme,
colors: {
...lightTheme.colors,
background: '#000000',
surface: '#1C1C1E',
text: '#FFFFFF',
textSecondary: '#8E8E93',
border: '#38383A',
},
};
export type Theme = typeof lightTheme;
```
```tsx
// context/ThemeContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme, Theme } from '../theme';
import { storage } from '../services/storage';
interface ThemeContextType {
theme: Theme;
isDark: boolean;
toggleTheme: () => void;
setThemeMode: (mode: 'light' | 'dark' | 'system') => void;
}
const ThemeContext = createContext(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const systemColorScheme = useColorScheme();
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>('system');
// Determine effective theme
const isDark =
themeMode === 'system'
? systemColorScheme === 'dark'
: themeMode === 'dark';
const theme = isDark ? darkTheme : lightTheme;
// Load preferences on startup
useEffect(() => {
storage.getPreferences().then((prefs) => {
if (prefs?.theme) {
setThemeMode(prefs.theme);
}
});
}, []);
// Toggle between light and dark
const toggleTheme = () => {
const newMode = isDark ? 'light' : 'dark';
setThemeMode(newMode);
storage.getPreferences().then((prefs) => {
storage.setPreferences({
...prefs,
theme: newMode,
notifications: prefs?.notifications ?? true,
language: prefs?.language ?? 'en',
});
});
};
return (
{children}
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
```
## 배포 준비
앱을 배포하기 전에 몇 가지 설정과 최적화 작업이 필요합니다.
```json
// app.json
{
"expo": {
"name": "MyApp",
"slug": "my-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#007AFF"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.example.myapp",
"buildNumber": "1"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#007AFF"
},
"package": "com.example.myapp",
"versionCode": 1
},
"plugins": [
"expo-router"
]
}
}
```
```bash
# build.sh
# EAS Build configuration (Expo Application Services)
npm install -g eas-cli
# Log in to Expo account
eas login
# Configure the project
eas build:configure
# Build for iOS (simulator)
eas build --platform ios --profile development
# Build for Android (test APK)
eas build --platform android --profile preview
# Production build
eas build --platform all --profile production
# Store submission
eas submit --platform ios
eas submit --platform android
```
> **인증서와 키에 대하여**
>
> iOS 배포에는 Apple Developer 계정(연 $99), Android에는 Google Play Console 계정(1회 $25)이 필요합니다. EAS가 서명 인증서를 자동으로 관리합니다.
## 결론
React Native는 크로스 플랫폼 모바일 앱을 효율적으로 개발하기 위한 강력한 접근 방식입니다. 이 가이드에서 다룬 기초 지식을 활용하면 완성도 높은 프로페셔널한 앱을 구축할 수 있습니다.
### React Native 앱 성공 체크리스트
- Expo를 사용하여 환경을 빠르게 설정합니다
- 핵심 컴포넌트를 숙달합니다: View, Text, TouchableOpacity, FlatList
- React Navigation과 TypeScript 타입 정의로 내비게이션을 구현합니다
- Context API와 커스텀 Hooks로 상태를 관리합니다
- 전용 서비스 레이어로 API 호출을 구조화합니다
- AsyncStorage로 데이터를 로컬에 영구 저장합니다
- 다크 모드를 지원하는 반응형 테마 시스템을 구축합니다
React Native와 Expo의 공식 문서는 각 주제를 심화 학습하기 위한 가장 신뢰할 수 있는 자료입니다. 생태계는 빠르게 발전하고 있으며, React Query나 Zustand 같은 라이브러리를 활용하면 더 복잡한 앱에서의 데이터 관리를 한층 간소화할 수 있습니다.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/ko/blog/react-native/react-native-building-complete-mobile-app