모바일 개발자 면접 준비

모바일 개발자 면접 질문

자주 묻는 모바일 개발자 면접 질문 20개. 모바일 개발은 안정적인 iOS, Android, 크로스 플랫폼 앱을 만들고, 제품, 성능, 릴리스, 오프라인, 기기 연동 과제를 해결하는 데 초점을 맞춘 수요가 높은 분야입니다. 이 질문들은 여러 난이도를 다루며, 면접 트레이너에서 큰 소리로 답변하는 연습을 할 수 있습니다.

모바일 개발자 AI 면접 시작하기신용카드가 필요하지 않습니다. 무료 세션 1회 제공.
영어 기술 면접 연습비원어민이 기술 면접 통과를 연습할 수 있는 모드입니다.

초급 질문

1SharedPreferences나 UserDefaults와 같은 표준 키-값 저장소에 인증 토큰을 저장하는 것과 Android Keystore나 iOS Keychain과 같은 플랫폼 보안 저장소에 저장하는 것의 차이점은 무엇인가요?

Android의 SharedPreferences나 iOS의 UserDefaults와 같은 표준 저장소 메커니즘은 가볍고 민감하지 않은 설정값을 저장하도록 설계되었습니다. 이들은 애플리케이션의 샌드박스 디렉터리 내에 암호화되지 않은 평문 파일(XML 또는 plist)로 데이터를 저장합니다. 따라서 루팅이나 탈옥된 기기에 물리적으로 접근할 수 있거나, 암호화되지 않은 기기 백업 파일 또는 파일 시스템에 접근 권한이 있는 사람은 누구나 이 토큰을 그대로 읽을 수 있습니다. 반면 플랫폼 보안 저장소, 즉 iOS Keychain과 Android Keystore(주로 EncryptedSharedPreferences를 통해 사용됨)는 유휴 데이터 암호화(encryption at rest)를 제공합니다. iOS Keychain은 기기 하드웨어(예: Secure Enclave)와 연동된 키를 사용해 저장된 항목을 암호화하며 세분화된 접근 정책을 지원합니다. Android Keystore는 하드웨어 격리 보안 모듈(TEE(Trusted Execution Environment) 또는 StrongBox) 내부에서 암호화 키를 생성하고 저장하므로, 암호화 키가 애플리케이션 메모리에 노출되지 않으며 파일 시스템에서도 추출할 수 없습니다.

// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk

// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()
val securePrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()

// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk

// SECURE (Keychain Services API):
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "user_auth_token",
    kSecValueData as String: token.data(using: .utf8)!,
    kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
AI 코치와 함께 이 질문에 답해 보세요

2모바일 CI/CD (Continuous Integration/Continuous Delivery) 파이프라인의 주요 목적은 무엇이며, 웹이나 백엔드 배포 파이프라인과 구별되는 고유한 단계와 제약 조건에는 어떤 것들이 있나요?

모바일 CI/CD 파이프라인의 주요 목적은 모바일 클라이언트 애플리케이션의 빌드, 테스트, 코드 서명, 배포 과정을 자동화하여 일관된 품질과 재현 가능한 릴리스를 유지하는 것입니다. 지속적 통합(CI)은 자동화된 빌드, 정적 분석, 단위 테스트를 통해 코드 변경 사항을 검증합니다. 지속적 제공/배포(CD)는 바이너리 아티팩트를 패키징하고 서명하여 테스트 트랙(TestFlight, Firebase App Distribution 등)이나 앱 스토어로 배포합니다. 모바일 CI/CD는 다음과 같은 주요 측면에서 웹 및 백엔드 파이프라인과 차이가 있습니다. 1. 아티팩트 형태: 서버나 컨테이너 이미지 내부에서 직접 실행되는 형태가 아니라 컴파일된 클라이언트 바이너리(.ipa, .apk, .aab)를 생성합니다. 2. 러너 하드웨어 제약: 백엔드 파이프라인은 주로 경량 Linux 컨테이너에서 실행되는 반면, iOS 컴파일에는 macOS 하드웨어와 Xcode 명령줄 도구가 필수적으로 요구됩니다. 3. 릴리스 지연 시간 및 승인 절차: 최종 사용자 배포 시 서드파티 앱 스토어 검수 과정(Apple App Store / Google Play Store)을 거쳐야 하므로, 서버 재배포처럼 즉시 롤백할 수 없습니다. 문제 해결을 위해서는 서명된 새 바이너리를 제출하거나 런타임 피처 플래그를 활용해야 합니다.

name: Mobile CI/CD Workflow

on:
  pull_request:
    branches: [main]
  push:
    tags:
      - 'v*.*.*'

jobs:
  ci_validation:
    name: Lint & Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Linter
        run: ./gradlew lint
      - name: Run Unit Tests
        run: ./gradlew testDebugUnitTest

  cd_release_ios:
    name: Build & Distribute iOS
    needs: ci_validation
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Install Signing Certificates
        run: fastlane match appstore --readonly
      - name: Build & Upload to TestFlight
        run: fastlane ios beta
AI 코치와 함께 이 질문에 답해 보세요

3크로스 플랫폼 모바일 애플리케이션에서 프레젠테이션, 도메인, 데이터 계층을 분리하는 목적은 무엇이며, 이러한 구조가 iOS와 Android 전반에서 코드 공유를 어떻게 용이하게 하나요?

애플리케이션을 프레젠테이션, 도메인, 데이터 계층으로 분리하면 명확한 책임 경계가 수립됩니다: 1. **프레젠테이션 계층(Presentation Layer):** UI 렌더링, 사용자 입력 수신, 화면 수준의 상태 관리(예: View, Widget, ViewModel, Presenter)를 담당합니다. 2. **도메인 계층(Domain Layer):** 순수 비즈니스 로직, 도메인 모델/엔티티, 유스케이스(use case)를 캡슐화합니다. UI 및 플랫폼 프레임워크로부터 독립성을 유지합니다. 3. **데이터 계층(Data Layer):** 리포지토리 및 데이터 소스를 통해 원격 API, 로컬 데이터베이스, 기기 캐시로부터 데이터를 조회하고 영속화하는 작업을 관리합니다. 크로스 플랫폼 모바일 개발에서 이러한 구조는 도메인과 데이터 계층이 플랫폼에 종속되지 않기 때문에 코드 공유를 수월하게 만듭니다. 핵심 비즈니스 규칙, 데이터 변환, 네트워크 처리를 iOS와 Android 간에 100% 공유할 수 있어, 팀은 전체 스택을 공유(Flutter, React Native)하거나 플랫폼별 네이티브 프레젠테이션 계층을 유지하면서 비즈니스/데이터 로직만 공유(Kotlin Multiplatform)할 수 있습니다.

[Presentation Layer] (UI, Screens, ViewModels)
         │
         ▼ calls
[Domain Layer]       (Use Cases, Business Rules, Entities)
         ▲
         │ implements interfaces
[Data Layer]         (Repositories, API Clients, Local Storage)
AI 코치와 함께 이 질문에 답해 보세요

4모바일 엔지니어링에서 명령형과 선언형 UI(User Interface) 개발 패러다임의 핵심적인 차이점은 무엇이며, Flutter 위젯과 React Native 컴포넌트는 선언형 모델을 어떻게 구현하고 있나요?

명령형 UI 개발에서는 개발자가 UI 요소를 생성, 수정, 제거하기 위한 명시적이고 단계적인 명령을 직접 작성합니다(예: ID로 뷰를 찾은 뒤 setText나 setVisibility 같은 메서드를 직접 호출). 반면 선언형 UI 모델은 주어진 상태에 대해 UI가 어떻게 보여야 하는지를 기술합니다(흔히 UI = f(state)로 표현됨). 상태가 변경되면 프레임워크가 시각적 표현을 효율적으로 업데이트하는 방법을 결정합니다. Flutter와 React Native는 모두 이러한 선언형 패러다임을 따릅니다. Flutter에서 위젯은 불변(immutable)의 설정 명세이며, 동적 데이터가 변경되어 StatefulWidget에서 setState()를 호출하면 재빌드가 예약되고, build() 메서드가 새로운 위젯 트리 명세를 반환하여 Flutter가 이를 Element 및 RenderObject 트리와 비교·조정(reconciliation)합니다. React Native에서는 컴포넌트가 JSX를 반환하는 함수 또는 클래스이며, state나 props가 업데이트되면 다시 렌더링이 트리거되어 React가 가상 요소 트리를 비교·조정한 후 브리지 또는 네이티브 런타임을 통해 필요한 최소한의 네이티브 업데이트를 적용합니다.

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});
  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _increment,
      child: Text('Count: $_count'),
    );
  }
}
AI 코치와 함께 이 질문에 답해 보세요

5모바일 애플리케이션에서 콜드 스타트(cold start), 웜 스타트(warm start), 핫 스타트(hot start) 간의 동작상 차이점은 무엇이며, 콜드 스타트가 성능 예산(performance budget)에서 가장 중요한 지표인 이유는 무엇인가요?

모바일 앱 수명 주기에서 시작 상태는 앱의 프로세스와 메모리 상태가 운영체제에 이미 존재하는지 여부에 따라 달라집니다. - 콜드 스타트(Cold Start): 운영체제가 아직 프로세스를 생성하지 않았거나 이전에 프로세스가 종료되어 앱이 처음부터 완전히 새로 시작되는 상태입니다. 운영체제는 새 프로세스를 할당하고, 바이너리와 런타임을 로드하며, Application 및 런타임 컨텍스트를 초기화하고, 첫 번째 뷰를 인플레이트해야 합니다. 따라서 지연 시간이 가장 깁니다. - 웜 스타트(Warm Start): 앱의 프로세스는 대체로 메모리에 남아 있지만, 액티비티나 뷰 계층 구조가 소멸되었거나 제거된 상태입니다(예: 백그라운드 재생성 또는 뒤로 가기 탐색). 운영체제는 프로세스를 처음부터 새로 포크(fork)하거나 생성할 필요 없이 UI 및 액티비티만 다시 생성합니다. - 핫 스타트(Hot Start): 앱과 UI 상태가 여전히 메모리에 완전히 상주해 있는 상태입니다(예: 사용자가 홈 버튼을 눌렀다가 즉시 복귀한 경우). 운영체제는 초기화 오버헤드가 거의 없이 기존 뷰 계층 구조를 포그라운드로 가져오기만 합니다. 콜드 스타트는 사용자에게 가장 느린 첫인상을 전달하는 경험이기 때문에 성능 예산에서 가장 중요한 지표입니다. 콜드 스타트 시간이 길어지면 즉각적인 사용자 이탈, 리텐션 저하, 그리고 앱 스토어 순위 하락(예: Android Vitals 임계치 초과)으로 직결됩니다.

Cold Start: [OS Process Spawn] -> [App Runtime Init] -> [Activity/UI Creation] -> [First Frame Drawn]
Warm Start: [Existing Process]  -> [Activity/UI Recreation] -> [First Frame Drawn]
Hot Start:  [Existing Process]  -> [Bring Existing View to Foreground] (Instant)
AI 코치와 함께 이 질문에 답해 보세요

6크로스 플랫폼 모바일 앱에서 네이티브 브리지란 무엇이며, 기능을 순수 Flutter나 React Native 코드로만 작성하지 않고 브리지를 사용하는 경우는 언제인가요?

네이티브 브리지는 크로스 플랫폼 코드가 특정 플랫폼(iOS 또는 Android) 전용 코드를 호출하고, 네이티브 코드가 결과나 이벤트를 다시 전달할 수 있도록 해 주는 통신 계층입니다. Flutter에서는 주로 플랫폼 채널(platform channels)이나 플러그인을 통해 구현됩니다. React Native에서는 주로 네이티브 모듈(native modules), TurboModules 또는 네이티브 UI 컴포넌트를 통해 구현됩니다. 플랫폼 API, 기기 고유 기능, 네이티브 SDK, 성능에 민감한 네이티브 구현, 네이티브 UI 컴포넌트처럼 순수 Flutter나 React Native 코드만으로는 접근하기 어려운 기능이 필요할 때 브리지를 사용합니다. 대표적인 예로는 카메라 기능, Bluetooth, 푸시 알림, 결제, 보안 스토리지, 백그라운드 서비스, 헬스케어 API, 또는 Swift/Objective-C나 Kotlin/Java 연동만 제공하는 SDK 등이 있습니다. 잘 설계된 브리지는 실제 세부 iOS 및 Android 처리를 플랫폼별 구현에 맡기면서, 크로스 플랫폼 계층에는 getBatteryLevel, startBluetoothScan, openNativePaymentSheet처럼 작고 명확한 API를 노출합니다.

Cross-platform screen:
  calls NativePayments.openPaymentSheet(orderId)

iOS implementation:
  uses Apple Pay / native payment SDK

Android implementation:
  uses Google Pay / native payment SDK
AI 코치와 함께 이 질문에 답해 보세요

7모바일 개발에서 오프라인 우선 아키텍처는 무엇을 의미하며, 표준적인 HTTP (Hypertext Transfer Protocol) 응답 캐싱과는 근본적으로 어떻게 다른가요?

모바일 개발에서 오프라인 우선(offline-first) 아키텍처는 로컬 저장소를 읽기 및 쓰기 작업 모두에 대한 기본 데이터 소스(source of truth)로 취급하는 방식을 의미합니다. 화면을 렌더링하거나 사용자 상호작용을 허용하기 전에 네트워크 요청이 완료되기를 기다리는 대신, 앱이 로컬 데이터베이스나 스토리지 계층과 직접 상호작용하며 백그라운드 동기화 프로세스가 네트워크 연결 시 로컬 변경 사항을 원격 서버와 조정합니다. 이는 다음과 같은 점에서 표준 HTTP 응답 캐싱과 근본적으로 다릅니다. 1. **데이터 모델**: HTTP 캐싱은 요청 URL과 헤더를 키로 삼아 원시 네트워크 응답(예: JSON 페이로드)을 저장합니다. 반면 오프라인 우선 아키텍처는 로컬 데이터베이스(Room, SQLite, SwiftData/Core Data 등)에 구조화된 도메인 엔티티를 저장합니다. 2. **쓰기 작업과 읽기 작업**: HTTP 캐싱은 주로 읽기 최적화 메커니즘이며 오프라인 상태에서의 쓰기 트랜잭션이나 데이터 변경(mutation)을 기본적으로 지원하지 않습니다. 반면 오프라인 우선 아키텍처는 로컬에서 완전한 쓰기를 허용하고, 이후 서버 동기화를 위해 변경 사항을 큐에 저장합니다. 3. **쿼리 기능 및 수명 주기**: 캐시된 HTTP 응답은 자동 캐시 만료 정책의 적용을 받으며 임의로 쿼리하거나 필터링, 조인할 수 없습니다. 반면 오프라인 우선 영속성은 네트워크 상태와 무관하게 완전한 쿼리, 인덱싱 및 결정론적인 수명 주기 관리를 제공합니다.

/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.

/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
AI 코치와 함께 이 질문에 답해 보세요

8모바일 앱에서 원격 푸시 알림과 로컬 알림의 차이는 무엇입니까?

로컬 알림과 원격 푸시 알림의 핵심 차이는 알림이 어디에서 생성되고 어떻게 트리거되는지에 있습니다. 1. 로컬 알림(Local Notification)은 OS API(iOS의 UserNotifications, Android의 AlarmManager/WorkManager/NotificationManager 등)를 사용하여 기기 내의 애플리케이션에서 전적으로 생성되고 예약되며 트리거됩니다. 특정 날짜/시간, 카운트다운 타이머, 지오펜싱(geofencing, 지리적 경계) 등 기기 측 조건에 따라 발동합니다. 운영체제 데몬에서 로컬로 실행되므로 전달 시점에 백엔드 서버나 활성 인터넷 연결이 필요하지 않습니다. 2. 원격 푸시 알림(Remote Push Notification)은 외부 애플리케이션 서버에서 생성되어 플랫폼 푸시 게이트웨이(Apple 기기의 경우 APNs, Android의 경우 FCM)를 통해 인터넷으로 전송됩니다. 게이트웨이가 기기의 OS 수준 데몬으로 페이로드를 전달하여 앱을 깨우거나 배너를 표시합니다. 원격 알림은 수신 채팅 메시지, 친구 요청, 속보와 같은 실시간 외부 이벤트를 수신할 때 필수적입니다.

let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default

// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)

UNUserNotificationCenter.current().add(request) { error in
    if let error = error {
        print("Failed to schedule notification: \(error)")
    }
}
AI 코치와 함께 이 질문에 답해 보세요

9앱 스토어를 통해 앱을 배포할 때 Android App Bundle(.aab), APK(Android Package), iOS IPA 아카이브 간의 구조적 및 운영상 차이점은 무엇인가요?

APK(Android Package)는 컴파일된 DEX 바이트코드, 리소스, 에셋, 네이티브 라이브러리를 포함하며 Android 기기에 직접 설치 및 실행할 수 있는 실행 가능한 패키지 포맷입니다. 유니버설 APK는 모든 화면 밀도, CPU 아키텍처 및 언어에 대한 리소스를 하나로 묶기 때문에 다운로드 크기가 커집니다. Android App Bundle(.aab)은 스토어 배포를 위해 Google Play에서 요구하는 업로드/게시용 포맷입니다. AAB는 기기에 직접 설치할 수 없으며, 대신 Google Play가 번들과 Play 앱 서명(Play App Signing)을 사용하여 특정 기기의 아키텍처, 화면 밀도, 언어에 동적으로 맞춤화된 최적화된 분할 APK(split APK)를 생성합니다(동적 전송, Dynamic Delivery). iOS IPA(.ipa)는 애플리케이션 아카이브 파일(Payload 디렉터리, `.app` 번들, 서명 에셋, 메타데이터를 포함하는 zip 컨테이너)입니다. App Store Connect에 업로드되면 Apple은 앱 시닝(App Thinning, 예: App Slicing)을 수행하여 다운로드하는 기기에 필요한 에셋과 바이너리만 전달합니다. 운영 관점에서 APK와 IPA는 (서명 및 프로비저닝 조건이 충족되면) 실제 테스트 기기에 직접 설치할 수 있는 반면, AAB는 로컬 기기에 설치하기 전에 먼저 분할 APK로 변환해야 합니다(예: `bundletool` 사용).

# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk

# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device

# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
AI 코치와 함께 이 질문에 답해 보세요

10전송 계층 보안(TLS, Transport Layer Security)은 모바일 애플리케이션의 네트워크 호출에 어떤 보호를 제공하며, 최신 모바일 운영체제는 안전한 전송 기본 설정을 어떻게 강제하나요?

TLS(Transport Layer Security)는 모바일 네트워크 호출에 대해 세 가지 핵심 보장을 제공합니다: 기밀성(전송 중인 데이터를 암호화하여 도청자가 페이로드나 헤더를 읽지 못하도록 보호), 데이터 무결성(전송 중 요청 및 응답의 위변조 여부를 감지), 서버 인증(신뢰할 수 있는 인증 기관(CA)을 통해 서버의 디지털 인증서를 검증하여 중간자 공격(MitM) 방지). 최신 모바일 운영체제는 암호화되지 않은 일반 텍스트 HTTP 트래픽을 기본적으로 차단하여 안전한 전송을 강제합니다. 1. iOS는 ATS(App Transport Security)를 적용하여 Info.plist에 도메인 예외를 명시적으로 정의하지 않는 한 네트워크 연결(예: URLSession을 통한 연결)에 TLS 1.2 이상을 사용하는 HTTPS를 요구합니다. 2. Android(API 28 이상)는 일반 텍스트 HTTP 트래픽을 기본적으로 비활성화합니다(`cleartextTrafficPermitted=false`). 암호화되지 않은 HTTP를 허용하려면 네트워크 보안 구성(`network_security_config.xml`) 또는 매니페스트 속성 `android:usesCleartextTraffic`을 통해 명시적으로 예외를 설정해야 합니다.

<!-- Android: res/xml/network_security_config.xml -->
<network-security-config>
    <!-- Cleartext HTTP blocked by default. Exceptions require explicit declaration -->
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
    </domain-config>
</network-security-config>

<!-- iOS: Info.plist ATS configuration -->
<!-- ATS blocks HTTP by default; exceptions must be declared inside NSAppTransportSecurity -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>
AI 코치와 함께 이 질문에 답해 보세요

중급 질문

11생체 인증 검증이 성공했을 때만 암호화 키가 잠금 해제되도록 플랫폼 보안 스토리지를 활용한 생체 인증(Face ID / 지문)을 어떻게 구현합니까?

생체 인증으로 암호화 키를 안전하게 보호하려면 애플리케이션이 생체 인증 프롬프트의 단순 불리언(boolean) UI 콜백에만 의존해서는 안 됩니다. 대신 키 사용 전에 생체 인증을 강제하는 접근 제어 정책을 설정하여 하드웨어 지원 스토리지(iOS의 Secure Enclave, Android의 Android Keystore / StrongBox) 내에서 암호화 키를 생성하고 저장해야 합니다. iOS에서는 `.biometryCurrentSet`(또는 `.userPresence`)과 같은 플래그와 함께 `SecAccessControlCreateWithFlags`를 사용하여 키나 Keychain 항목을 구성합니다. 서명 또는 복호화를 위해 프라이빗 키가 요청되면 OS가 자동으로 사용자에게 Face ID / Touch ID 인증을 요청합니다. Android에서는 `KeyGenParameterSpec.Builder`에 `.setUserAuthenticationRequired(true)`를 설정하여 키를 생성합니다. 암호화 작업(예: `Cipher` 또는 `Signature`)을 초기화하고 이를 `BiometricPrompt.CryptoObject`로 래핑하여 `BiometricPrompt.authenticate()`에 전달합니다. 키는 인증에 성공했을 때 `onAuthenticationSucceeded` 내에서 해당 인증된 `CryptoObject`를 통해서만 잠금 해제되어 사용할 수 있습니다. 또한 iOS의 `.biometryCurrentSet` 또는 Android의 `setInvalidatedByBiometricEnrollment(true)`를 사용하면 기기에 새로운 지문이나 생체 정보가 등록될 때 기존 키가 영구적으로 무효화되므로, 기기 비밀번호가 유출되었을 때의 무단 접근을 방지할 수 있습니다.

val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
    "biometric_auth_key",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setInvalidatedByBiometricEnrollment(true)
    .build()

keyGen.init(spec)
keyGen.generateKey()
AI 코치와 함께 이 질문에 답해 보세요

12크로스 플랫폼 모바일 CI(Continuous Integration) 러너에서 풀 리퀘스트 빌드 시간을 대폭 단축하기 위해 어떤 캐싱 전략과 파이프라인 최적화를 적용할 수 있나요?

모바일 CI 러너에서 풀 리퀘스트 빌드 시간을 획기적으로 줄이려면 의존성 해결, 컴파일 캐싱, 선별적 실행을 목표로 최적화해야 합니다. 첫째, 락 파일(lockfile) 해시(예: `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml`, SPM 패키지 리졸브 파일 등)를 키로 사용하는 효과적인 의존성 캐싱을 구현합니다. 이를 통해 깨끗한 러너 인스턴스에서 외부 패키지를 다시 다운로드하거나 의존성을 재해결하는 작업을 방지합니다. 둘째, 네이티브 빌드 캐시를 구성합니다. Android의 경우 원격 HTTP 캐싱 또는 `~/.gradle/caches` 및 빌드 디렉터리에 대한 CI 네이티브 영구 캐싱과 함께 Gradle Build Cache(`--build-cache`)를 활성화합니다. iOS의 경우 Xcode `DerivedData` 및 Swift Package/CocoaPods 컴파일 산출물을 신중하게 캐시하거나, Tuist나 Bazel과 같은 최신 원격 캐싱 도구를 활용합니다. React Native나 Flutter의 경우 JS 번들 출력물, node_modules 및 Flutter 엔진 SDK 캐시를 캐싱합니다. 셋째, 선별적 작업 실행(변경 사항 감지 및 테스트 영향도 분석)을 적용합니다. Git 경로 필터를 사용하여 Android나 백엔드 파일만 변경되었을 때 iOS 빌드를 건너뛰고, 비용이 큰 UI 테스트나 컴파일 단계 전에 빠른 린트 및 단위 테스트 단계를 먼저 실행하며, 시뮬레이터/디버그 빌드나 단위 테스트만으로 충분한 PR 실행에서는 전체 릴리스 바이너리(AAB나 범용 IPA 등) 생성을 피합니다.

name: PR Fast Check
on:
  pull_request:
    paths:
      - 'android/**'
      - 'shared/**'

jobs:
  android-unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      # Gradle build caching for dependencies & task outputs
      - uses: gradle/actions/setup-gradle@v3
        with:
          cache-read-only: false

      - name: Run Unit Tests & Lint Only
        run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
AI 코치와 함께 이 질문에 답해 보세요

13프레젠테이션, 도메인 유스케이스, 데이터 접근 계층이 iOS와 Android 전반에서 독립적으로 테스트 가능하고 재사용될 수 있도록 공유 기능을 어떻게 구조화하시겠습니까?

iOS와 Android 전반에서 독립적인 테스트 가능성과 재사용성을 보장하기 위해 공유 기능을 프레젠테이션, 도메인, 데이터 계층으로 구분하는 클린 계층형 아키텍처를 채택합니다. 1. **도메인 계층 (순수 공유 로직):** 순수 비즈니스 엔티티, 리포지토리 인터페이스(계약), 유스케이스/인터랙터(예: `GetCartUseCase`, `ApplyPromoCodeUseCase`)를 포함합니다. 이 계층은 UI 프레임워크(UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter 위젯)나 플랫폼 API에 대한 의존성을 전혀 갖지 않습니다. 각 유스케이스는 단일 비즈니스 연산을 캡슐화하므로 순수 목(mock) 객체를 활용해 100% 단위 테스트가 가능합니다. 2. **데이터 계층 (데이터 접근 및 캡슐화):** 도메인의 리포지토리 인터페이스를 구현합니다. 원격 데이터 소스(REST/GraphQL) 및 로컬 영속성 저장소(SQLite/Key-Value)와 통신합니다. 네트워크 DTO(Data Transfer Object)와 데이터베이스 엔티티는 이 계층을 벗어나기 전에 순수 도메인 엔티티로 엄격히 매핑되므로, 외부 직렬화 스키마나 DB 변경 사항이 도메인 또는 UI 계층으로 전파되는 것을 방지합니다. 3. **프레젠테이션 계층 (UI 및 상태 관리):** 상태 홀더(ViewModel, Bloc, Presenter)와 UI 뷰/위젯으로 구성됩니다. 상태 홀더는 도메인 유스케이스를 호출하고, UI 상태(로딩, 성공, 에러)를 관리하며, 관찰 가능한 상태를 뷰에 노출합니다. 뷰는 단순히 이 상태를 관찰하여 렌더링합니다. 이러한 구조를 통해 UI 없이 유스케이스를 독립적으로 단위 테스트하고, 리포지토리를 모킹하여 뷰 모델을 테스트할 수 있으며, 도메인과 데이터 로직을 완전히 재사용하면서 플랫폼 간 프레젠테이션 구현만 교체할 수 있습니다.

[ UI View / Compose / SwiftUI ]
         |
         v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
         |
         v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
         |
         v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
         ^
         | (Implements)
[ Repository Implementation ] (Data Layer)
         |---- Maps NetworkDTO -> DomainEntity
         |---- Maps DatabaseEntity -> DomainEntity
    [ API Client / Local DB / Storage ]
AI 코치와 함께 이 질문에 답해 보세요

14로컬 컴포넌트 상태만으로는 불충분한 시점을 어떻게 판단하며, 다수의 개발자가 참여하는 크로스 플랫폼 코드베이스에 적합한 아키텍처 상태 관리 방식을 어떻게 선택하겠습니까?

크로스 플랫폼 모바일 개발에서 로컬 컴포넌트 상태(`useState`/`StatefulWidget`)로부터 아키텍처 상태 관리 솔루션으로 전환하는 기준은 상태의 범위, 생명주기 요구사항, 테스트 용이성에 따라 결정됩니다. 1. **로컬 상태가 불충분해지는 시점**: - **컴포넌트 간 공유 및 Prop Drilling**: 서로 다른 내비게이션 경로 간이나 위젯/컴포넌트 트리의 멀리 떨어진 브랜치 간에 상태를 접근하거나 수정해야 할 때. - **생명주기 지속성(Lifecycle Persistence)**: 화면 소멸, 경로 전환, 내비게이션 리셋 후에도 데이터가 유지되어야 할 때(예: 사용자 세션, 장바구니 내용, 캐시된 피드). - **관심사 분리 및 테스트 용이성**: 비즈니스 로직, 유효성 검사, 사이드 이펙트가 UI 컴포넌트와 뒤엉켜 헤드리스(headless) 자동화 단위 테스트가 어려워질 때. 2. **멀티 개발자 코드베이스를 위한 아키텍처 선택**: - **단방향 데이터 흐름 / 예측 가능한 컨테이너(예: BLoC, Redux, Riverpod)**: UI가 명시적인 이벤트/액션을 디스패치하고 전용 비즈니스 로직 단위에서 방출된 불변 상태를 렌더링하도록 엄격한 분리를 강제합니다. 이를 통해 명확한 계약이 형성되고 부수 효과가 줄어들며 비즈니스 로직의 독립적인 단위 테스트가 가능해집니다. - **원자적 / 범위 기반 반응형 스토어(예: Zustand, MobX, Provider)**: 보일러플레이트 코드가 적고 유연한 셀렉터 구독을 제공하므로, 팀이 가벼운 디커플링과 타깃 컴포넌트 재렌더링을 원할 때 적합합니다. 팀 협업 환경에서 아키텍처의 주된 목표는 순수 비즈니스 로직을 UI 렌더링 계층에서 격리하고, 선택적 구독을 통해 불필요한 전체 트리 재렌더링을 방지하는 것입니다.

// Zustand store: pure state container isolated from React UI hierarchy
import { create } from 'zustand';

interface CartState {
  items: string[];
  addItem: (item: string) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set({ items: [] }),
}));

// Usage in components uses selective subscriptions to avoid unnecessary re-renders:
// const itemCount = useCartStore((state) => state.items.length);
AI 코치와 함께 이 질문에 답해 보세요

15이미지와 동적 콘텐츠가 포함된 긴 목록을 스크롤할 때 버벅거림이 발생하는 크로스 플랫폼 모바일 앱 화면에서 버벅거림(jank) 현상을 어떻게 조사하고 개선하시겠습니까?

스크롤 시 발생하는 버벅거림은 프레임 예산 초과, JS 또는 Dart 작업, 메인/UI 스레드 작업, GPU/래스터화 작업, 레이아웃 계산, 이미지 디코딩, 메모리 부족, 네트워크 로딩 등 다양한 원인에서 비롯될 수 있으므로, 먼저 실제 기기에서 현상을 재현하고 프로파일링을 수행하겠습니다. 60Hz 화면에서는 프레임당 약 16.7ms, 120Hz 화면에서는 약 8.3ms의 시간이 주어지므로, 스크롤 중에 무거운 렌더링, 디코딩, 레이아웃 연산 또는 동기 연산이 실행되면 프레임 드롭이 발생할 수 있습니다. Flutter DevTools, React Native 성능 도구 또는 Flipper, Android Studio Profiler, Xcode Instruments, 프레임 타임라인 뷰 등의 도구를 활용해 실제 병목 구간을 찾겠습니다. 긴 목록의 경우 React Native의 FlatList, FlashList, RecyclerListView나 Flutter의 ListView.builder, SliverList처럼 지연 로딩(lazy) 또는 가상화(virtualized) 처리가 되어 있는지 확인하겠습니다. 안정적인 고유 키(key)를 사용하고, 부모 상태가 변경될 때마다 모든 행이 다시 렌더링되거나 재빌드되지 않도록 하며, 필요한 경우 행 컴포넌트나 셀렉터를 메모이제이션하고, build/renderItem 로직을 가볍게 유지하겠습니다. 또한 정렬, 필터링, JSON 파싱, 포맷팅, 이미지 처리 작업은 스크롤 실행 경로 밖으로 분리하겠습니다. 아이템 크기를 예측할 수 있다면 React Native의 getItemLayout이나 Flutter의 고정/프로토타입 item extent와 같은 레이아웃 힌트를 제공하겠습니다. 이미지의 경우 적절한 크기의 썸네일을 제공하고, 캐싱을 적용하며, 작은 셀에 원본 고해상도 이미지를 디코딩하지 않도록 하고, 플레이스홀더 및 지연 로딩을 사용하며 과도한 비트맵 생성으로 인한 메모리 요동(churn)을 방지하겠습니다. 아울러 지나치게 복잡한 행 레이아웃을 단순화하고, 그림자/클리핑/오버드로우/투명도 연산 등 비용이 큰 처리를 줄이며, 데이터를 배치 또는 페이지네이션 단위로 불러온 뒤 다시 프로파일링하여 프레임 드롭과 렌더링 시간이 개선되었는지 확인하겠습니다.

1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
AI 코치와 함께 이 질문에 답해 보세요

16네이티브 브리지 호출 시 스레드 경계는 어떻게 동작하며, 네이티브 모듈에서 무거운 백그라운드 작업을 수행할 때 스레드 전환 지연이나 UI (User Interface) 버벅임(jank)을 어떻게 방지하나요?

크로스 플랫폼 프레임워크는 브리지 호출 시 특정한 스레딩 규칙을 사용합니다. 예를 들어 Flutter의 표준 `MethodChannel` 호출은 플랫폼의 메인 UI 스레드에 도착하는 반면, 기존 React Native 브리지 호출은 전용 JavaScript 스레드에서 실행된 후 네이티브 스레드로 비동기 디스패치됩니다. 네이티브 브리지 메서드가 네이티브 UI/메인 스레드에서 실행될 때 CPU 집약적인 연산, 긴 동기식 디스크 I/O 또는 블로킹 네트워크 작업을 실행하면 메인 런 루프가 차단됩니다. 이는 프레임 드롭과 시각적인 UI 버벅임을 유발하며, Android에서는 ANR (Application Not Responding) 오류를, iOS에서는 워치독 강제 종료를 초래할 수 있습니다. 이를 방지하려면 네이티브 브리지 핸들러가 무겁거나 블로킹되는 작업을 백그라운드 실행 풀(예: `Dispatchers.IO`/`Dispatchers.Default`를 사용하는 Kotlin Coroutines, Android `ThreadPoolExecutor`, 또는 Swift `Task.detached` / GCD `DispatchQueue.global()`)로 위임(offload)해야 합니다. 작업이 완료되면 프레임워크가 요구하는 브리지 스레드로 결과를 다시 전달하여(예: 표준 MethodChannel의 경우 UI 스레드에서 결과 반환 또는 백그라운드 태스크 큐 활용) 크로스 플랫폼 상태 업데이트가 UI 렌더링 사이클을 방해하지 않도록 해야 합니다.

class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        if (call.method == "applyFilter") {
            val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
            
            // Dispatch heavy computation off the main UI thread
            scope.launch {
                try {
                    val processed = processBitmapBytes(imageBytes) // CPU heavy work
                    withContext(Dispatchers.Main) {
                        result.success(processed)
                    }
                } catch (e: Exception) {
                    withContext(Dispatchers.Main) {
                        result.error("PROCESSING_FAILED", e.localizedMessage, null)
                    }
                }
            }
        } else {
            result.notImplemented()
        }
    }
}
AI 코치와 함께 이 질문에 답해 보세요

17SWR (Stale-While-Revalidate)과 조건부 HTTP 헤더를 활용하여 목록 및 피드 화면의 캐싱과 무효화 전략을 어떻게 설계하시겠습니까?

피드 및 목록 화면을 위한 안정적인 캐싱 및 무효화 전략은 SWR (Stale-While-Revalidate) 패턴과 조건부 HTTP 검증 헤더(`ETag` / `If-None-Match` 또는 `Last-Modified` / `If-Modified-Since`)를 결합하는 것입니다. 화면이 열리면 로컬 데이터베이스(단일 진실 공급원, 예: Room 또는 Core Data/SQLite)에서 캐시된 데이터를 즉시 읽어 렌더링함으로써 사용자에게 즉각적인 UI를 제공합니다. 동시에 캐시된 `If-None-Match: <etag>` 헤더를 포함한 백그라운드 네트워크 요청을 전송합니다. 서버가 `304 Not Modified`로 응답하면 페이로드가 전송되지 않으므로, 대역폭과 배터리를 절약하면서 로컬 캐시의 유효성을 검증할 수 있습니다. 서버가 `200 OK`로 응답하면 트랜잭션 내에서 새로운 데이터와 갱신된 `ETag`를 로컬 데이터베이스에 기록하고, 반응형 데이터베이스 관찰자(observer)가 변경된 목록을 UI로 자동 방출합니다. 페이지네이션의 경우 페이지 토큰이나 오프셋을 캐시된 레코드와 함께 저장합니다. 캐시 무효화는 TTL 만료, 사용자 작업(예: 조건부 헤더를 우회하는 당겨서 새로고침), 또는 로컬 데이터 변경의 부수 효과(예: 로컬에서 항목을 생성하거나 삭제할 때 낙관적으로 데이터베이스를 업데이트하고 캐시된 페이지 커서를 재검증 대상으로 표시)를 통해 트리거됩니다.

fun getFeed(): Flow<List<FeedItem>> = flow {
    val cached = feedDao.getFeedItems()
    if (cached.isNotEmpty()) emit(cached)
    
    val lastEtag = feedDao.getFeedEtag()
    try {
        val response = api.fetchFeed(ifNoneMatch = lastEtag)
        if (response.code() == 200 && response.body() != null) {
            feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
            emit(feedDao.getFeedItems())
        }
    } catch (e: Exception) {
        if (cached.isEmpty()) throw e
    }
}
AI 코치와 함께 이 질문에 답해 보세요

고급 질문

18금융 모바일 애플리케이션은 기기에 리프레시 토큰을 안전하게 저장하고, 생체 인증 잠금 해제를 지원하며, OS(Operating System) 업그레이드 후에도 유지되고, 일시적인 오프라인 세션 중에도 안전하게 동작해야 합니다. 이러한 토큰 저장 및 접근 아키텍처를 어떻게 설계하시겠습니까?

견고한 모바일 토큰 저장 아키텍처는 하드웨어 기반 보안 모듈을 활용합니다. 즉, iOS에서는 Secure Enclave 기반의 Keychain을, Android에서는 StrongBox 또는 TEE(Trusted Execution Environment) 기반의 Keystore를 사용합니다. 리프레시 토큰은 생체 인증 암호화 게이팅(iOS의 `kSecAccessControlBiometryCurrentSet` 또는 `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, Android의 `setUserAuthenticationRequired(true)` 및 `AUTH_BIOMETRIC_STRONG` 등)으로 보호되는 키를 사용하여 저장 시 암호화(encryption at rest)되어야 합니다. 암호화 게이팅 방식을 사용하면 애플리케이션 코드 내에서 우회 가능한 불리언 확인에 의존하지 않고, 생체 인증이 성공했을 때만 하드웨어에 의해 암호화 키가 복호화(unwrap)됩니다. OS 업그레이드 후에도 무단 접근을 방지하면서 상태를 유지하기 위해, 키를 기기 하드웨어에 바인딩하는 동시에 새로운 생체 정보가 등록될 경우 키를 무효화하거나 재인증을 요구하는 등록 변경 정책(예: Android의 `setInvalidatedByBiometricEnrollment(true)`)을 적용합니다. 일시적인 오프라인 세션의 경우 수명이 짧은 암호화된 액세스 토큰과 제한된 로컬 오프라인 세션 상태를 지정된 로컬 TTL 및 제한된 권한 범위 내에서 운영하고, 권한이 필요한 리프레시 작업은 네트워크 연결이 복원될 때까지 연기합니다. 재연결 시 백엔드에서 1회용 재전송 탐지(single-use replay detection)가 포함된 리프레시 토큰 로테이션을 통해 세션을 검증하며, 토큰 재사용이나 계정 해지가 감지되면 백엔드가 무효화 신호를 반환하여 클라이언트가 하드웨어 기반 키를 폐기하고 오프라인 스토리지를 초기화하도록 합니다.

val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
    "refreshTokenKeyAlias",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
    .setInvalidatedByBiometricEnrollment(true)
    .build()
keyGenerator.init(spec)
keyGenerator.generateKey()
AI 코치와 함께 이 질문에 답해 보세요

19제한적이고 비용이 많이 드는 macOS 러너 용량을 관리하면서 여러 팀을 지원하는 크로스 플랫폼 모바일 모노레포를 위한 확장 가능한 CI/CD(Continuous Integration/Continuous Deployment) 인프라를 어떻게 설계하겠습니까?

부족하고 비용이 많이 드는 macOS 용량을 최적화하면서 크로스 플랫폼 모바일 모노레포를 위한 확장 가능한 CI/CD 인프라를 설계하려면, 빌드 그래프 기반 변경 감지(affected build detection), 하이브리드 러너 풀 전반에 걸친 엄격한 워크로드 분기, 그리고 macOS용 가상화/임시(ephemeral) 오케스트레이션에 의존해야 합니다. 첫째, 분산 원격 캐싱을 지원하는 모노레포 빌드 그래프 도구(예: Bazel, Nx, Turborepo)를 구현합니다. PR(Pull Request)이 생성될 때마다 CI 파이프라인은 타깃 브랜치 대비 DAG(Directed Acyclic Graph, 방향성 비순환 그래프) 차이점을 계산하여 변경되지 않은 모듈을 완전히 건너뛰고 영향을 받는 패키지와 하위 종속 항목만 빌드 및 테스트합니다. 둘째, 하이브리드 러너 할당 전략을 구현합니다. Xcode가 반드시 필요하지 않은 모든 작업(TypeScript/Dart 린팅 및 컴파일, 단위 테스트, 정적 코드 분석, 보안 검사, Android Gradle 빌드 등)은 비용 효율적이고 확장이 용이한 Linux/Kubernetes 러너로 오프로드합니다. macOS 러너는 최종 iOS 어셈블리, Swift/Objective-C 컴파일, 코드 서명, iOS 시뮬레이터 테스트 실행에만 엄격히 제한하여 사용합니다. 셋째, 가상화 인프라(Tart, Anka 또는 Nomad/Kubernetes로 오케스트레이션되는 AWS/MacStadium 베어메탈 노드 등)를 사용하여 macOS 러너를 관리합니다. 각 iOS 빌드는 미리 워밍된 툴체인 및 파생 데이터(derived-data) 캐시가 갖춰진 깨끗한 임시 가상 머신에서 실행되어 러너 드리프트를 방지하고 파이프라인 큐 깊이에 따라 빠르게 자동 확장을 수행할 수 있습니다.

name: Cross-Platform Monorepo CI
on: [pull_request]
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      ios_affected: ${{ steps.filter.outputs.ios }}
      android_affected: ${{ steps.filter.outputs.android }}
    steps:
      - uses: actions/checkout@v4
      - id: filter
        run: |
          # Determine affected targets via monorepo tool (e.g. nx / bazel)
          echo "ios=$(./tools/affected.sh ios)" >> $GITHUB_OUTPUT
          echo "android=$(./tools/affected.sh android)" >> $GITHUB_OUTPUT

  build-android:
    needs: detect-changes
    if: needs.detect-changes.outputs.android_affected == 'true'
    runs-on: ubuntu-latest-16core
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew assembleRelease

  build-ios:
    needs: detect-changes
    if: needs.detect-changes.outputs.ios_affected == 'true'
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - run: bundle exec fastlane ios build_and_test
AI 코치와 함께 이 질문에 답해 보세요

20대부분의 비즈니스 로직을 iOS와 Android 간에 공유하면서도 플랫폼별 UI (User Interface), 내비게이션 및 네이티브 연동을 독립적으로 발전시킬 수 있는 크로스 플랫폼 모바일 아키텍처를 어떻게 설계하시겠습니까?

도메인 모델, 유효성 검사, 유스케이스, 비즈니스 규칙, 데이터 접근 계약 등 안정적인 비즈니스 동작을 담당하는 공통 코어(shared core)를 중심으로 앱을 설계하겠습니다. 플랫폼별 UI, 내비게이션, 네이티브 SDK, 권한, 생명주기(lifecycle) 처리, 앱 셸(app-shell) 관련 관심사는 코어 외부에 두어야 합니다. 공통 코드는 UIKit, SwiftUI, Jetpack, Android 프레임워크 API, React Native 내비게이션, Flutter 내비게이션 또는 네이티브 SDK 세부사항을 임포트해서는 안 됩니다. 대신 각 플랫폼이 구현하는 AuthRepository, SecureStorage, Analytics, CameraService, PaymentProvider와 같은 좁은 범위의 인터페이스에만 의존해야 합니다. 또한 하나의 거대한 공유 계층을 만들기보다는 가능한 한 기능(feature) 단위로 코드를 구성하겠습니다. 예를 들어 결제(checkout), 검색, 계정, 메시징은 각각 공유 도메인/유스케이스 코드, 상태 계약, 리포지토리 인터페이스를 가질 수 있습니다. 그런 다음 iOS 및 Android 셸이 화면 구성, 네이티브 UI 패턴, 내비게이션 스택, 권한, 어댑터 연결을 담당합니다. 프레젠테이션 로직의 경우 단순한 상태와 액션을 방출하는 리듀서(reducer), 상태 머신, 뷰모델 계약처럼 UI 프레임워크에 완전히 종속되지 않는 경우에만 공유할 수 있으며, 각 플랫폼이 독립적으로 발전할 수 있도록 실제 뷰와 내비게이션은 플랫폼 고유 영역으로 유지해야 합니다. 여기서 핵심적인 절충점은 공통적이고 안정적인 비즈니스 로직을 적극적으로 공유하되, 실제 플랫폼 간의 차이를 억지로 감추는 과도한 추상화를 피하는 것입니다. 모듈 의존성 규칙, 의존성 주입, 공개 API, 계약 테스트, 아키텍처 검증 도구, 명확한 소유권을 통해 이러한 경계를 강제하겠습니다. 네이티브 연동에는 포트 및 어댑터(ports/adapters) 패턴을 사용하여, 공통 기능 코드는 공통 역량만을 바라보고 각 플랫폼이 SDK 동작, 생명주기, 권한, 오류 처리 및 UX 차이를 자체 계층에서 처리하도록 구성해야 합니다.

shared-core/
  checkout/
    domain/          # Money, Cart, CheckoutRules, PlaceOrderUseCase
    ports/           # PaymentGateway, TaxRepository, Analytics
    state/           # CheckoutStateMachine or ViewModel contract

ios-app/
  checkout-ui/       # SwiftUI/UIKit screens and iOS navigation
  adapters/          # ApplePayPaymentGateway, KeychainStorage

android-app/
  checkout-ui/       # Compose/XML screens and Android navigation
  adapters/          # GooglePayPaymentGateway, EncryptedSharedPrefsStorage
AI 코치와 함께 이 질문에 답해 보세요