Skip to content
Published on

React Native vs Flutter 2025: 크로스플랫폼 모바일 개발의 모든 것

Authors

목차

1. 2025년 크로스플랫폼 모바일 개발 전망

시장 현황

2025년 모바일 개발 시장에서 크로스플랫폼 프레임워크의 점유율은 계속 증가하고 있습니다. 네이티브 개발 대비 비용 절감과 빠른 출시가 핵심 동기입니다.

프레임워크GitHub Stars주간 다운로드/사용주요 기업
React Native120k+npm 2M+/주Meta, Microsoft, Shopify
Flutter165k+pub.dev 활발Google, BMW, Alibaba
KMP (Compose)성장 중JetBrains 생태계Netflix, Cash App

핵심 선택 기준

  • 팀 기술 스택: 웹 개발자 팀이라면 React Native, Dart 학습 의향이 있다면 Flutter
  • UI 커스터마이징: 플랫폼 네이티브 룩을 원하면 RN, 완전한 커스텀 UI는 Flutter
  • 성능 요구사항: 애니메이션 집약적이면 Flutter, 일반 앱은 둘 다 충분
  • 기존 코드: 웹 코드 재사용은 RN, 새 프로젝트는 Flutter도 좋은 선택
  • 채용 시장: JavaScript 개발자 풀이 넓어 RN이 채용 유리

2. React Native 2025

New Architecture (GA)

React Native의 New Architecture는 2024년에 GA(Generally Available)가 되었고, 2025년에는 사실상 표준이 되었습니다.

핵심 구성 요소:

  1. Fabric: 새로운 렌더링 시스템. 동기식 렌더링으로 UI 응답성 향상
  2. TurboModules: 네이티브 모듈의 지연 로딩. 앱 시작 시간 단축
  3. JSI (JavaScript Interface): JavaScript와 네이티브 코드 간 직접 통신. 브릿지 제거
  4. Codegen: 타입 안전한 네이티브 코드 자동 생성
// TurboModule 정의 (New Architecture)
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getConstants(): {
    platform: string;
    version: number;
  };
  multiply(a: number, b: number): Promise<number>;
}

export default TurboModuleRegistry.getEnforcing<Spec>('Calculator');

Hermes 엔진

Hermes는 React Native의 기본 JavaScript 엔진으로, 모바일 환경에 최적화되었습니다.

  • 바이트코드 사전 컴파일: 앱 시작 시간 단축
  • 메모리 효율: 가비지 컬렉션 최적화
  • 크기 감소: 앱 번들 크기 절감
  • Static Hermes (실험적): AOT 컴파일로 추가 성능 향상

React Native의 핵심 패턴

// 함수형 컴포넌트 + Hooks
import React, { useState, useCallback } from 'react';
import { View, Text, FlatList, StyleSheet, Pressable } from 'react-native';

interface User {
  id: string;
  name: string;
  email: string;
}

const UserListScreen: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [refreshing, setRefreshing] = useState(false);

  const onRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const response = await fetch('https://api.example.com/users');
      const data = await response.json();
      setUsers(data);
    } finally {
      setRefreshing(false);
    }
  }, []);

  const renderItem = useCallback(({ item }: { item: User }) => (
    <Pressable style={styles.card}>
      <Text style={styles.name}>{item.name}</Text>
      <Text style={styles.email}>{item.email}</Text>
    </Pressable>
  ), []);

  return (
    <FlatList
      data={users}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      refreshing={refreshing}
      onRefresh={onRefresh}
    />
  );
};

const styles = StyleSheet.create({
  card: {
    padding: 16,
    marginHorizontal: 16,
    marginVertical: 8,
    backgroundColor: '#fff',
    borderRadius: 8,
    elevation: 2,
  },
  name: { fontSize: 18, fontWeight: '600' },
  email: { fontSize: 14, color: '#666', marginTop: 4 },
});

3. Flutter 2025

Impeller 렌더링 엔진

Impeller는 Flutter의 새로운 렌더링 엔진으로, Skia를 대체합니다.

주요 개선사항:

  • 셰이더 사전 컴파일: 첫 실행 jank(끊김) 제거
  • 예측 가능한 성능: 프레임 드롭 최소화
  • Metal/Vulkan 네이티브: 플랫폼별 GPU API 직접 활용
  • iOS에서 기본 활성화, Android에서도 Stable

Dart 3

Dart 3는 Flutter의 개발 경험을 크게 향상시켰습니다.

// 패턴 매칭 (Dart 3)
sealed class Result<T> {
  const Result();
}
class Success<T> extends Result<T> {
  final T data;
  const Success(this.data);
}
class Error<T> extends Result<T> {
  final String message;
  const Error(this.message);
}
class Loading<T> extends Result<T> {
  const Loading();
}

// switch 표현식으로 패턴 매칭
Widget buildContent(Result<User> result) => switch (result) {
  Success(:final data) => UserCard(user: data),
  Error(:final message) => ErrorWidget(message: message),
  Loading() => const CircularProgressIndicator(),
};

// Records
(String name, int age) getUser() => ('Kim', 30);

// 사용
final (name, age) = getUser();
print('$name is $age years old');

// Named fields in records
({String name, int age}) getUserNamed() => (name: 'Kim', age: 30);

Flutter의 핵심 패턴

// StatelessWidget + 상태 관리
class UserListScreen extends StatelessWidget {
  const UserListScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Users')),
      body: Consumer<UserNotifier>(
        builder: (context, notifier, child) {
          return switch (notifier.state) {
            LoadingState() => const Center(
              child: CircularProgressIndicator(),
            ),
            ErrorState(:final message) => Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(message),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: notifier.retry,
                    child: const Text('Retry'),
                  ),
                ],
              ),
            ),
            SuccessState(:final data) => ListView.builder(
              itemCount: data.length,
              itemBuilder: (context, index) {
                final user = data[index];
                return UserCard(user: user);
              },
            ),
          };
        },
      ),
    );
  }
}

class UserCard extends StatelessWidget {
  final User user;
  const UserCard({super.key, required this.user});

  
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: ListTile(
        title: Text(user.name,
          style: Theme.of(context).textTheme.titleMedium),
        subtitle: Text(user.email),
        trailing: const Icon(Icons.chevron_right),
      ),
    );
  }
}

4. 기능 비교

성능

항목React NativeFlutter
렌더링네이티브 컴포넌트자체 렌더링 (Impeller)
애니메이션Reanimated (60fps)내장 (120fps 가능)
앱 시작 시간Hermes로 개선됨AOT 컴파일로 빠름
메모리 사용량중간중간~높음
번들 크기더 작음 (~7MB)더 큼 (~15MB)
JS 브릿지JSI로 제거됨해당 없음 (네이티브)

개발자 경험 (DX)

항목React NativeFlutter
언어JavaScript/TypeScriptDart
Hot ReloadFast RefreshHot Reload/Hot Restart
IDEVS Code, WebStormVS Code, Android Studio
디버깅Flipper, Chrome DevToolsDevTools, Observatory
패키지 생태계npm (방대)pub.dev (성장 중)
학습 곡선웹 개발자에게 낮음Dart 학습 필요

네이티브 모듈 통합

항목React NativeFlutter
네이티브 APITurboModules + JSIPlatform Channels / FFI
카메라expo-cameracamera 패키지
지도react-native-mapsgoogle_maps_flutter
푸시 알림expo-notificationsfirebase_messaging
생체 인증expo-local-authenticationlocal_auth

5. Expo 딥다이브

Expo 개요

Expo는 React Native 개발을 극적으로 간소화하는 플랫폼입니다. 2025년에는 대부분의 React Native 프로젝트가 Expo를 사용합니다.

Expo Router v4

파일 기반 라우팅으로 Next.js와 유사한 경험을 제공합니다.

app/
  _layout.tsx        ← 루트 레이아웃
  index.tsx          ← 홈 화면 (/)
  (tabs)/
    _layout.tsx      ← 탭 네비게이션
    home.tsx/home
    profile.tsx/profile
  users/
    [id].tsx/users/123 (동적 라우트)
    index.tsx/users
  settings/
    _layout.tsx      ← 설정 레이아웃
    index.tsx/settings
// app/_layout.tsx
import { Stack } from 'expo-router';

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

// app/users/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';

export default function UserDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return (
    <View>
      <Text>User ID: {id}</Text>
    </View>
  );
}

// 타입 안전한 네비게이션
import { router } from 'expo-router';

function navigateToUser(userId: string) {
  router.push(`/users/${userId}`);
}

EAS (Expo Application Services)

# EAS Build - 클라우드 빌드
npx eas-cli build --platform ios --profile production
npx eas-cli build --platform android --profile production

# EAS Submit - 스토어 제출
npx eas-cli submit --platform ios
npx eas-cli submit --platform android

# EAS Update - OTA 업데이트 (코드만)
npx eas-cli update --branch production --message "Bug fix"

Expo SDK 모듈

// expo-camera
import { CameraView, useCameraPermissions } from 'expo-camera';

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

  if (!permission?.granted) {
    return <Button title="Grant Permission" onPress={requestPermission} />;
  }

  return <CameraView style={{ flex: 1 }} facing="back" />;
}

// expo-notifications
import * as Notifications from 'expo-notifications';

async function schedulePushNotification() {
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "Reminder",
      body: 'Check your tasks!',
    },
    trigger: { seconds: 60 },
  });
}

6. 성능 벤치마크

앱 시작 시간 (ms)

시나리오React Native (Hermes)Flutter (AOT)Native
Cold Start (Android)~350ms~280ms~200ms
Cold Start (iOS)~400ms~300ms~250ms
Warm Start~150ms~120ms~100ms

스크롤 성능 (FPS)

시나리오React NativeFlutter
단순 리스트 (1000개)58-60 fps59-60 fps
이미지 리스트55-60 fps58-60 fps
복잡한 카드 레이아웃50-58 fps56-60 fps

애니메이션 성능

시나리오React Native (Reanimated)Flutter
단순 전환60 fps60 fps
복잡한 제스처55-60 fps58-60 fps
파티클 시스템45-55 fps55-60 fps

메모리 사용량 (MB)

시나리오React NativeFlutter
빈 앱~40 MB~50 MB
리스트 뷰 (100개)~65 MB~75 MB
이미지 갤러리~120 MB~130 MB

결론: Flutter가 애니메이션/렌더링에서 약간 우위, RN은 메모리와 번들 크기에서 유리. 일반 앱에서는 차이가 미미합니다.


7. 상태 관리

React Native 상태 관리

Zustand (가장 인기 있는 선택)

import { create } from 'zustand';

interface UserStore {
  users: User[];
  isLoading: boolean;
  error: string | null;
  fetchUsers: () => Promise<void>;
}

const useUserStore = create<UserStore>((set) => ({
  users: [],
  isLoading: false,
  error: null,
  fetchUsers: async () => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch('https://api.example.com/users');
      const users = await response.json();
      set({ users, isLoading: false });
    } catch (error) {
      set({ error: (error as Error).message, isLoading: false });
    }
  },
}));

// 컴포넌트에서 사용
function UserList() {
  const { users, isLoading, fetchUsers } = useUserStore();

  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  if (isLoading) return <ActivityIndicator />;

  return (
    <FlatList
      data={users}
      renderItem={({ item }) => <UserCard user={item} />}
    />
  );
}

TanStack Query (서버 상태)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json()),
    staleTime: 5 * 60 * 1000, // 5분
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newUser: CreateUserRequest) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }).then(r => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

Flutter 상태 관리

Riverpod (가장 추천)

// Provider 정의

class UserNotifier extends _$UserNotifier {
  
  FutureOr<List<User>> build() async {
    return await ref.read(userRepositoryProvider).getUsers();
  }

  Future<void> addUser(CreateUserRequest request) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(userRepositoryProvider).createUser(request);
      return ref.read(userRepositoryProvider).getUsers();
    });
  }
}

// 위젯에서 사용
class UserListScreen extends ConsumerWidget {
  const UserListScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(userNotifierProvider);

    return usersAsync.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => Center(child: Text('Error: $error')),
      data: (users) => ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) => UserCard(user: users[index]),
      ),
    );
  }
}

Bloc (엔터프라이즈 선택)

// Event
sealed class UserEvent {}
class LoadUsers extends UserEvent {}
class CreateUser extends UserEvent {
  final CreateUserRequest request;
  CreateUser(this.request);
}

// State
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final List<User> users;
  UserLoaded(this.users);
}
class UserError extends UserState {
  final String message;
  UserError(this.message);
}

// Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
  final UserRepository repository;

  UserBloc(this.repository) : super(UserInitial()) {
    on<LoadUsers>((event, emit) async {
      emit(UserLoading());
      try {
        final users = await repository.getUsers();
        emit(UserLoaded(users));
      } catch (e) {
        emit(UserError(e.toString()));
      }
    });
  }
}

8. 네비게이션

Expo Router (React Native)

// 타입 안전한 라우팅
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#2196F3' }}>
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

// 딥 링킹 자동 지원
// myapp://users/123 -> app/users/[id].tsx

go_router (Flutter)

final router = GoRouter(
  routes: [
    ShellRoute(
      builder: (context, state, child) => ScaffoldWithNavBar(child: child),
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: '/users',
          builder: (context, state) => const UserListScreen(),
          routes: [
            GoRoute(
              path: ':id',
              builder: (context, state) {
                final id = state.pathParameters['id']!;
                return UserDetailScreen(userId: id);
              },
            ),
          ],
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
);

// 사용
context.go('/users/123');
context.push('/users/123');
context.pop();

9. 테스팅

React Native 테스팅

Detox (E2E)

// e2e/users.test.ts
describe('User Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should show user list', async () => {
    await expect(element(by.id('user-list'))).toBeVisible();
  });

  it('should navigate to user detail', async () => {
    await element(by.id('user-card-1')).tap();
    await expect(element(by.text('User Detail'))).toBeVisible();
  });

  it('should pull to refresh', async () => {
    await element(by.id('user-list')).swipe('down');
    await waitFor(element(by.id('loading-indicator')))
      .not.toBeVisible()
      .withTimeout(5000);
  });
});

Maestro (간편한 E2E)

# .maestro/user-flow.yaml
appId: com.example.myapp
---
- launchApp
- assertVisible: "Users"
- tapOn: "Kim"
- assertVisible: "User Detail"
- back
- assertVisible: "Users"

Flutter 테스팅

Widget Test

void main() {
  testWidgets('UserCard displays user info', (tester) async {
    final user = User(id: '1', name: 'Kim', email: 'kim@test.com');

    await tester.pumpWidget(
      MaterialApp(home: UserCard(user: user)),
    );

    expect(find.text('Kim'), findsOneWidget);
    expect(find.text('kim@test.com'), findsOneWidget);
  });

  testWidgets('UserListScreen shows loading indicator', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: UserListScreen()),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });
}

Integration Test

// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('full user flow test', (tester) async {
    await tester.pumpWidget(const MyApp());
    await tester.pumpAndSettle();

    // 사용자 목록 확인
    expect(find.text('Users'), findsOneWidget);

    // 사용자 카드 탭
    await tester.tap(find.text('Kim'));
    await tester.pumpAndSettle();

    // 상세 화면 확인
    expect(find.text('User Detail'), findsOneWidget);
  });
}

Patrol (고급 E2E)

void main() {
  patrolTest('User flow with native interactions', ($) async {
    await $.pumpWidgetAndSettle(const MyApp());

    // 권한 대화상자 처리
    await $.native.grantPermissionWhenInUse();

    // Flutter 위젯과 네이티브 UI 모두 테스트
    await $(#userList).scrollTo(find.text('Kim'));
    await $(find.text('Kim')).tap();
  });
}

10. 채용 시장과 급여

2025년 시장 현황

지표React NativeFlutter
LinkedIn 공고 (글로벌)약 35,000+약 25,000+
한국 공고약 800+약 500+
평균 연봉 (미국)약 13만~17만 달러약 12만~16만 달러
평균 연봉 (한국)약 5,500만~8,500만 원약 5,000만~8,000만 원
프리랜서 시급 (미국)약 80~150 달러약 70~130 달러

어떤 것을 먼저 배울까

React Native을 먼저 배워야 하는 경우:

  • 이미 JavaScript/TypeScript를 알고 있음
  • 웹 개발 경험이 있음
  • 빠른 취업이 목표
  • 기존 React 웹 프로젝트와 코드 공유하고 싶음

Flutter를 먼저 배워야 하는 경우:

  • 프로그래밍을 처음 시작
  • 커스텀 UI/애니메이션이 중요한 프로젝트
  • 데스크톱/웹까지 하나의 코드로 지원하고 싶음
  • Google 생태계에 관심

11. 프로젝트별 선택 프레임워크

의사 결정 플로우차트

1. 팀이 JavaScript/TypeScript에 익숙한가?

  • Yes → React Native 우선 고려
  • No → 계속 평가

2. 복잡한 커스텀 UI/애니메이션이 핵심인가?

  • Yes → Flutter 우선 고려
  • No → 계속 평가

3. 웹 코드를 공유해야 하는가?

  • Yes → React Native (React 코드 공유)
  • No → 계속 평가

4. 데스크톱 지원이 필요한가?

  • Yes → Flutter (더 성숙한 데스크톱 지원)
  • No → 프로젝트 요구사항에 따라 선택

프로젝트 유형별 추천

프로젝트 유형추천이유
E-커머스 앱React Native웹과 코드 공유, Shopify 연동
소셜 미디어Flutter커스텀 UI, 애니메이션
핀테크 앱React Native큰 JavaScript 생태계
게임Flutter커스텀 렌더링 강점
기업용 앱React Native채용 용이성
IoT 대시보드Flutter데스크톱 + 모바일 통합
MVP/스타트업Expo (RN)가장 빠른 개발 속도
미디어/스트리밍Flutter성능 최적화

12. 인터뷰 질문과 퀴즈

인터뷰 질문 10선

Q1: React Native New Architecture의 핵심 구성 요소를 설명하세요.

New Architecture는 Fabric(새 렌더링 시스템), TurboModules(네이티브 모듈 지연 로딩), JSI(JavaScript Interface)로 구성됩니다. JSI는 기존 브릿지를 대체하여 JavaScript와 네이티브 코드 간 직접 동기 통신을 가능하게 합니다. Codegen은 타입 안전한 네이티브 인터페이스를 자동 생성합니다.

Q2: Flutter의 Impeller가 Skia를 대체한 이유는?

Skia 기반 렌더링은 셰이더 컴파일을 런타임에 수행하여 첫 프레임에서 jank(끊김)가 발생했습니다. Impeller는 셰이더를 빌드 타임에 사전 컴파일하여 이 문제를 해결합니다. Metal(iOS)과 Vulkan(Android)을 직접 사용하여 예측 가능한 성능을 제공합니다.

Q3: React Native와 Flutter의 렌더링 방식 차이는?

React Native는 플랫폼의 네이티브 UI 컴포넌트를 사용합니다(UIKit/Android Views). 따라서 플랫폼 룩앤필을 자연스럽게 따릅니다. Flutter는 자체 렌더링 엔진(Impeller)으로 모든 픽셀을 직접 그립니다. 완전한 UI 커스터마이징이 가능하지만 네이티브 룩 구현에 추가 작업이 필요합니다.

Q4: Expo의 장점과 한계를 설명하세요.

장점: EAS Build로 CI/CD 간소화, OTA 업데이트(EAS Update), 파일 기반 라우팅(Expo Router), 풍부한 SDK 모듈, Prebuild로 네이티브 코드 관리 자동화. 한계: 일부 네이티브 모듈 미지원 시 config plugin 또는 eject 필요, 빌드 시간이 로컬보다 길 수 있음.

Q5: Flutter에서 Riverpod를 Provider보다 선호하는 이유는?

Riverpod는 컴파일 타임 안전성(런타임 에러 감소), BuildContext 의존성 제거, 프로바이더 간 의존성을 명시적으로 관리, 테스트 용이성(오버라이드 지원), autodispose로 자동 메모리 관리를 제공합니다. Provider의 한계(런타임 에러, 유연성 부족)를 해결한 진화된 버전입니다.

Q6: React Native에서 성능 최적화 방법을 설명하세요.

FlatList 사용(가상화 스크롤), React.memo/useMemo/useCallback으로 불필요한 리렌더링 방지, Reanimated로 UI 스레드 애니메이션, Hermes 엔진 활성화, 이미지 최적화(캐싱, 리사이징), 번들 분석으로 불필요한 의존성 제거, New Architecture(JSI) 활성화.

Q7: Dart 3의 패턴 매칭과 Records가 Flutter에 미친 영향은?

패턴 매칭으로 sealed class와 switch 표현식 결합 시 타입 안전한 상태 처리가 가능합니다. Records는 여러 값을 반환하는 함수를 간결하게 만듭니다. 이 조합으로 Flutter의 상태 관리 코드가 더 안전하고 읽기 쉬워졌습니다.

Q8: React Native와 Flutter 중 하나를 선택해야 한다면, 결정 기준은?

팀 기술 스택(JS 경험이면 RN), UI 요구사항(커스텀 UI면 Flutter), 코드 공유 전략(웹 공유면 RN), 채용 시장(JS 개발자 풀이 넓음), 성능 요구사항(그래픽 집약적이면 Flutter), 기존 인프라(React 웹이 있으면 RN). 대부분의 일반 앱에서는 큰 차이가 없으므로 팀 역량이 가장 중요합니다.

Q9: 크로스플랫폼 앱에서 네이티브 기능 접근 방법을 비교하세요.

React Native: TurboModules + JSI로 네이티브 코드(Java/Kotlin, ObjC/Swift)와 직접 통신합니다. Expo는 config plugin으로 네이티브 설정을 관리합니다. Flutter: Platform Channels(메서드/이벤트 채널)로 비동기 통신하거나, FFI로 C 라이브러리를 직접 호출합니다. 두 프레임워크 모두 대부분의 네이티브 API를 커버합니다.

Q10: 크로스플랫폼 테스팅 전략을 설명하세요.

단위 테스트(비즈니스 로직), 위젯/컴포넌트 테스트(UI), 통합 테스트(사용자 시나리오), E2E 테스트(실제 디바이스)의 테스트 피라미드를 따릅니다. RN에서는 Jest + Testing Library + Detox/Maestro, Flutter에서는 test + widget test + integration_test + Patrol을 사용합니다.

퀴즈 5선

Q1: React Native New Architecture에서 JSI의 역할은?

정답:

JSI(JavaScript Interface)는 JavaScript와 네이티브 코드 간의 직접적인 동기 통신을 가능하게 하는 인터페이스입니다. 기존의 비동기 JSON 직렬화 브릿지를 대체합니다. C++로 작성되어 JavaScript 엔진에 독립적이며, JavaScript에서 C++ 객체를 직접 참조할 수 있습니다. 이를 통해 성능이 크게 향상되고, TurboModules와 Fabric의 기반 기술이 됩니다.

Q2: Flutter에서 StatelessWidget vs StatefulWidget의 선택 기준은?

정답:

StatelessWidget은 내부 상태가 없는 위젯에 사용합니다. 부모로부터 받은 데이터만 표시하며 리빌드 시 항상 같은 결과를 반환합니다. StatefulWidget은 내부에서 변경 가능한 상태가 필요할 때 사용합니다. 그러나 2025년에는 Riverpod이나 Bloc 같은 상태 관리 라이브러리 사용이 일반적이므로, 대부분의 위젯을 StatelessWidget + 상태 관리 라이브러리 조합으로 작성합니다.

Q3: Expo Router의 파일 기반 라우팅과 기존 React Navigation의 차이점은?

정답:

React Navigation은 명시적으로 네비게이터와 스크린을 코드로 정의합니다. 타입 정의가 별도로 필요하고 중첩이 복잡해질 수 있습니다. Expo Router는 Next.js처럼 파일 시스템 구조가 곧 라우트입니다. 자동 딥 링킹, 타입 추론, 레이아웃 시스템을 제공하며, 웹과 모바일에서 동일한 URL 구조를 사용할 수 있습니다.

Q4: React Native의 Hermes 엔진이 V8/JavaScriptCore보다 좋은 이유는?

정답:

Hermes는 모바일 환경에 최적화된 JavaScript 엔진입니다. JavaScript를 바이트코드로 사전 컴파일(AOT)하여 앱 시작 시 파싱 시간을 제거합니다. 가비지 컬렉션이 모바일에 최적화되어 메모리 사용량이 적습니다. 앱 번들 크기도 더 작습니다. V8이나 JavaScriptCore는 범용 엔진이지만 Hermes는 모바일 React Native에 특화되어 있습니다.

Q5: Flutter와 React Native에서 각각 Hot Reload가 동작하는 원리의 차이는?

정답:

Flutter의 Hot Reload는 Dart VM에서 변경된 소스 코드를 주입하고 위젯 트리를 리빌드합니다. 상태가 보존되어 UI 변경을 즉시 확인할 수 있습니다. React Native의 Fast Refresh는 React 컴포넌트를 핫 스와핑하여 상태를 유지하면서 UI를 업데이트합니다. 두 방식 모두 앱 재시작 없이 변경사항을 반영하지만, Flutter는 VM 레벨에서, RN은 번들러(Metro) 레벨에서 동작합니다.


참고 자료

  1. React Native 공식 문서 - 프레임워크 가이드
  2. Flutter 공식 문서 - 프레임워크 가이드
  3. Expo 문서 - Expo 플랫폼 가이드
  4. React Native New Architecture - 새 아키텍처 가이드
  5. Flutter Impeller - 임펠러 렌더링 엔진
  6. Dart 공식 문서 - Dart 언어 가이드
  7. Expo Router - 파일 기반 라우팅
  8. Riverpod - Flutter 상태 관리
  9. Zustand - React 상태 관리
  10. TanStack Query - 서버 상태 관리
  11. Detox - React Native E2E 테스팅
  12. Patrol - Flutter 고급 테스팅
  13. go_router - Flutter 네비게이션
  14. Reanimated - RN 애니메이션 라이브러리