본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 14. 08:40

30일 만에 웹에서 iOS로: Expo + AI 지원 코드 변환

요약

기존 웹 앱을 기반으로 Expo + React Native 아키텍처를 활용하여 iOS 앱을 약 30일 만에 출시한 경험을 공유합니다. 이 과정에서 AI(Claude Code 등)를 사용하여 대부분의 비즈니스 로직과 컴포넌트를 TypeScript로 변환하는 데 성공했으며, 이는 순수 네이티브 재작성 대비 시간 효율성을 극대화했습니다. AI는 상태 관리나 순수 로직 번역에 강점을 보였으나, 네비게이션 패턴이나 카메라 같은 플랫폼 특화 기능은 수동 작업이 필요했습니다. 이 접근 방식은 1인 창업자가 기존 코드베이스를 재사용하며 빠르게 시장에 진입하는 현실적인 대안을 제시합니다.

핵심 포인트

  • Expo + React Native 조합은 웹 기반 앱을 모바일로 전환할 때 성능과 개발 속도의 균형점을 제공한다.
  • AI 도구(Claude Code 등)는 컴포넌트 구조, 훅, 순수 비즈니스 로직의 번역 시간을 크게 단축시켜준다 (약 40-50% 절감).
  • 네비게이션이나 카메라 API처럼 플랫폼 특화된 부분은 AI가 어려워하므로 수동 작업이 필수적이다.
  • Supabase와 같은 백엔드 서비스는 클라이언트 종류에 관계없이 데이터 모델을 일관되게 유지할 수 있어 개발 효율성이 높다.

지난달 저는 Fitnit의 iOS 앱을 출시했습니다. Fitnit은 AI를 사용하여 휴대폰 카메라로 운동 횟수를 세고, 식단 사진을 읽어 영양 성분(macros)을 분석하는 피트니스 앱입니다. 제가 네이티브 출시를 결정했을 때 웹 버전의 사용자는 4,779명이었습니다. 저는 Swift 앱을 처음부터 새로 작성하지 않았습니다. 셸(shell)로는 Expo + React Native를 사용했고, 기존 JS의 대부분을 React Native에서 실행되는 TypeScript로 변환하기 위해 AI를 활용했으며, 성능이 중요한 부분에 대해서만 선택적으로 Swift 네이티브 모듈(native modules)을 작성했습니다. 처음부터 끝까지 약 30일이 걸렸고, 대부분 혼자 진행했습니다. 이는 "순수 네이티브 Swift 재작성"과는 다른 이야기입니다. 실제로 어떤 일이 있었는지 설명하겠습니다.

아키텍처 결정: 순수 Swift vs Capacitor vs Expo

시작할 때 저에게는 세 가지 옵션이 있었습니다. 각각의 계산 방식이 달랐습니다.

  1. 처음부터 만드는 순수 Swift / SwiftUI. "이론적으로 깨끗한" 경로입니다. 최고의 성능, 최고의 App Store 시민권 상태, 완전한 네이티브 UX를 제공합니다. 하지만 또한: 제가 매일 사용하지 않는 언어로 클라이언트 전체를 완전히 재작성해야 하며, 웹 버전으로부터의 코드 재사용이 전혀 없습니다. 1인 창업자의 계산으로는 불가능했습니다.

  2. Capacitor (네이티브 셸 내의 웹 앱). 가장 저렴한 경로입니다. 말 그대로 기존 웹 앱을 WebView로 감싸는 방식입니다. 하지만 Fitnit의 핵심 루프는 "카메라 → 30fps 포즈 감지(pose detection) → 실시간 자세 피드백"입니다. 이는 Capacitor의 브릿지(bridge)가 병목 현상이 되는 경우입니다. 하루 동안 시도해 보았고, 성능 격차가 감당할 수 없는 수준임을 확인한 후 넘어갔습니다.

  3. Expo + React Native + 선택적 네이티브 모듈. 중간 경로입니다. 셸과 대부분의 앱 로직에는 React Native를 사용하고, iOS 전용 접착제(glue) 역할로는 Expo의 툴링(EAS Build, EAS Submit, expo-router, expo-camera)을 사용하며, 성능이 실제로 중요한 부분에는 Expo Modules API를 통해 커스텀 Swift 네이티브 모듈을 사용합니다.

저는 3번 옵션을 선택했습니다. 절충안을 수용했습니다: 순수 Swift만큼 성능이 뛰어나지는 않고, Capacitor만큼 저렴하지도 않지만, 한 사람이 기존 코드베이스의 대부분을 재사용하면서 30일 안에 출시할 수 있는 유일한 경로였습니다.

AI 지원 변환

웹 앱은 vanilla JS + Vite로 구성되어 있습니다.

React Native로 넘어가기 위해서는 모든 컴포넌트, 모든 훅 (hook), 그리고 모든 비즈니스 로직 조각들이 React Native로 깔끔하게 변환되거나 새로 작성되어야 했습니다. 저는 번역 작업의 대부분에 Claude Code를 사용했습니다 (Codex나 Cursor도 가능합니다. 이미 익숙한 도구를 선택하세요. 중요한 것은 기법입니다). 워크플로우는 다음과 같았습니다: 웹 컴포넌트 전체를 AI에 붙여넣고, "Expo Router + StyleSheet를 사용하는 React Native 대응 코드로 작성해줘"라고 요청합니다. AI는 약 80% 정도 정확한 초안을 반환합니다. 나머지 20%는 수동으로 수정합니다: 네비게이션 패턴 (navigation patterns), 제스처 (gestures), 그리고 AI가 잘못 추측한 플랫폼 특화 사항들입니다. 그다음 컴포넌트로 넘어갑니다.

AI 번역이 잘 처리한 것:

  • 컴포넌트 구조 (함수형 컴포넌트 (functional components), 훅 (hooks), props)
  • 상태 관리 로직 (AI는 useState / useEffect / useMemo를 포팅하는 데 능숙합니다)
  • 모든 순수 비즈니스 로직: 반복 횟수 카운팅 상태 머신 (rep-counting state machines), 칼로리 계산, 매크로 목표 계산 등 — 이들은 TypeScript로 그대로 번역되었습니다.
  • Supabase 클라이언트 호출 (설정 하나만 수정하면 RN에서도 동일한 SDK가 작동합니다)

AI 번역이 잘 처리하지 못한 것:

  • 네비게이션 (Navigation). 웹 라우팅 (Web routing)에서 Expo Router로 넘어가는 것은 사고 모델 (mental model) 자체가 다릅니다. 이 부분은 수동으로 작업해야 했습니다.
  • 카메라 관련 사항. AI는 RN에 존재하지 않는 웹 형태의 API를 만들어내려 시도했습니다. 결국 이를 폐기하고 expo-camera와 네이티브 모듈 브릿지 (native module bridge)를 사용하여 새로 작성해야 했습니다.
  • 작은 화면을 위한 조건부 렌더링 (Conditional rendering). AI는 한 손으로 iPhone을 사용하는 상황을 고려하지 않았습니다. 간격(spacing) 조정에는 수동 작업이 필요했습니다.

결과적으로: AI는 변환 시간의 약 40-50%를 절약해 주었습니다. 마법은 아니지만, 실질적이었습니다. 그리고 순수 로직 번역에 있어서는 예상보다 훨씬 더 정확했습니다.

그대로 유지된 것 (예상보다 더 많이):
모든 백엔드 요소. Supabase는 어떤 클라이언트가 접속하는지 상관하지 않습니다. 동일한 데이터베이스, 동일한 인증 (auth), 동일한 엣지 함수 (edge functions)를 사용합니다. iOS 앱은 웹 앱과 동일한 users, workouts, nutrition_entries 테이블을 사용합니다. iOS에서 가입한 사용자는 웹 앱에 로그인하여 자신의 운동 기록을 끊김 없이 확인할 수 있습니다.

1년 전 Firebase 대신 Supabase를 사용한 단 하나의 가장 큰 이유 — 바로 이런 날이 올 것을 알고 있었다는 점입니다. 데이터 모델 (Data model) 말입니다. 반복 횟수 (Rep counts), 자세 점수 (Form scores), 영양 기록 (Nutrition entries), 매크로 (Macros) — 두 클라이언트 모두에서 동일한 형태를 유지합니다. 마이그레이션도, API 변환 계층 (API translation layer)도 필요 없습니다. 모든 순수 로직 (Pure logic)이 그대로입니다. 칼로리 계산, 매크로 목표, 반복 횟수 카운팅 상태 머신 (State machine) 로직, 자세 점수 휴리스틱 (Heuristics) — 이 모든 것이 TypeScript로 유지되었고 React Native 내부에서 변경 없이 실행되었습니다. 이것이 처음부터 Swift로 개발하는 대신 Expo를 선택했을 때 얻는 큰 보상입니다. 순수 Swift를 사용했다면 약 5,000줄의 비즈니스 로직을 새로운 언어로 다시 작성해야 했을 것입니다. Expo를 사용함으로써 저는 약 0줄을 다시 작성했습니다. pSEO + 마케팅 사이트. 그것은 vanilla JS + Vite + Cloudflare Pages로 유지됩니다. iOS 앱은 코드베이스의 그 부분을 포함하지 않습니다. /ios 경로의 랜딩 페이지는 단순히 App Store로 딥링크 (Deep-link)를 연결할 뿐입니다. 브랜드. 동일한 색상, 동일한 이름, 동일한 포지셔닝 ("AI 반복 횟수 카운팅 + 사진 영양 추적, 이 모든 것이 하나의 앱에"). 웹과 iOS 사이를 전환하는 것은 마치 데스크톱의 Spotify와 휴대폰의 Spotify를 사용하는 것처럼 느껴져야 하며, 두 개의 서로 다른 제품처럼 느껴져서는 안 됩니다.

네이티브로 다시 구축한 것들 (Expo Modules API를 통한 Swift)
AI 피트니스 앱을 위해 Expo를 사용하는 전체적인 매력은, 브릿지 비용 (Bridge cost)이 너무 높을 때 React Native 세계를 떠나지 않고도 네이티브 Swift로 내려갈 수 있다는 점입니다. Expo Modules API는 이를 놀라울 정도로 접근하기 쉽게 만들어 줍니다. 저는 두 가지를 위해 네이티브 Swift 모듈을 작성했습니다:

  1. 포즈 감지 (Pose detection) (가장 레버리지가 높은 네이티브 모듈)
    웹 버전은 MediaPipe Pose를 사용합니다. iOS에서 React Native를 통해 MediaPipe를 사용하는 것도 가능했지만 지연 시간 (Latency)이 추가되었습니다. 네이티브 모듈을 통해 Apple의 Vision framework로 직접 연결하는 것이 더 빨랐고, React Native 래퍼 (Wrapper)가 없는 API들을 사용할 수 있게 해주었습니다.

모듈 구조:

// modules/pose-detection/ios/PoseDetectionModule.swift
import ExpoModulesCore
import Vision
import UIKit

public class PoseDetectionModule : Module {
public func definition () -> ModuleDefinition {
Name ( "PoseDetection" )
AsyncFunction ( "detectFromBuffer" ) {
( imageData : Data ) -> [ String : Any ] in
let handler = VNImageRequestHandler ( data : imageData , options : [:])
let request = VNDetectHumanBodyPoseRequest ()
try handler . perform ([ request ])
guard let observation = request . results ? . first else {
return [ "points" : [], "confidence" : 0 ]
}
return PoseSerializer . serialize ( observation )
}
}
}

requestAuthorization ( toShare : types , read : types ) return true } AsyncFunction ( "saveWorkout" ) { ( params : WorkoutParams ) -> Void in let workout = HKWorkout ( activityType : params . activityType . hkType , start : params . startedAt , end : params . endedAt , duration : params . duration , totalEnergyBurned : HKQuantity ( unit : . kilocalorie (), doubleValue : params . calories ), totalDistance : nil , metadata : [ "FitnitExerciseType" : params . exerciseType ] ) try await store . save ( workout ) } } } 전체 Fitnit 프로젝트의 총 네이티브 iOS 코드는 두 개의 모듈에 걸쳐 약 300줄 정도입니다. 그 외의 모든 것은 TypeScript/React Native입니다.

EAS Build + EAS Submit (배포 이야기)
Expo가 제공하는 기능 중 과소평가된 또 다른 것: 바로 배포 도구입니다. EAS Build는 클라우드에서 iOS 서명 (signing), 아카이브 (archive), 그리고 바이너리 생성을 처리합니다. EAS Submit은 해당 바이너리를 App Store Connect로 전송합니다.

효율적인 개발 루프:
eas build --platform ios --profile production

... 약 15분 후, 클라우드에서 .ipa 파일이 빌드 및 서명됨 ...

eas submit --platform ios --latest

... App Store Connect에 업로드되어 심사 준비 완료 ...

Xcode를 붙잡고 씨름할 필요가 없습니다. UI에서 프로비저닝 프로파일 (provisioning profiles)을 만지작거릴 필요도 없습니다. "이것을 실행하려면 Xcode 버전이 무엇이어야 하지"라고 고민할 필요도 없습니다. 그 시간을 제품에 더 쓰고 싶어 하는 1인 창업자에게 EAS는 첫 번째 빌드만으로도 그 가치를 충분히 증명합니다.

App Store 제출: 실제 경험
무시무시한 이야기들을 듣곤 합니다. 제 경험은 그런 공포 영화는 아니었지만, 그렇다고 결코 사소하지도 않았습니다.

1차 거절 — 가이드라인 2.1 (App Completeness, 앱 완성도): 핵심 기능을 보여주는 더 많은 스크린샷을 요구했습니다. 간단한 해결책으로, 추가 스크린샷을 다시 캡처하고 프레임을 잡아 재제출했습니다.

2차 거절 — 가이드라인 5.1.1 (Data Collection and Storage, 데이터 수집 및 저장): 카메라 사용에 관한 개인정보 보호 명확화를 요구했습니다. fitnitapp.com/privacy의 개인정보 처리방침에는 이미 카메라 프레임이 기기 내에 머문다고 설명되어 있었지만, App Store의 개인정보 보호 영양 성분표 (privacy nutrition labels)를 그에 맞춰 업데이트해야 했습니다.

해결: App Store Connect의 레이블을 강화하고, 기존 개인정보 처리방침 (privacy policy) URL을 다시 연결한 후 재제출했습니다. 3차 시도 만에 승인되었습니다. [실제 경험에 맞춰 라운드 횟수와 타이밍을 조정하세요.] 제가 미리 알았더라면 좋았을 유용한 팁: 바이너리 (binary)를 제출하기 전에 App Store Connect 등록 정보를 미리 준비하세요. 스크린샷, 설명, 키워드, 가격 책정, 개인정보 레이블 — 이 모든 것들을 먼저 제대로 설정한 다음 제출하십시오. 리뷰어들은 등록 정보가 잘 다듬어진 앱에 대해 더 관대한 것처럼 보입니다.

다르게 행동했을 부분:

  1. 25일 차가 아닌 5일 차에 TestFlight로 배포하기. 저는 누군가가 앱을 만지기 전에 iOS 경험이 기능적으로 완벽하기를 원했기 때문에 TestFlight를 미뤘습니다. 실수였습니다. 일주일 일찍 빌드 (build)에 20명의 실제 테스터를 참여시켰다면, App Store 심사에 제출했던 최소 세 개의 버그를 잡아낼 수 있었을 것입니다.

  2. 네이티브 모듈 (native modules)을 먼저 작성한 다음, 이를 바탕으로 RN (React Native) 화면을 구축하기. 저는 반대로 했습니다. RN UI를 먼저 구축한 다음 포즈 감지 (pose detection) 모듈이 필요하다는 것을 깨달았고, 급하게 이를 작성하느라 고생했습니다. 그 과정에서 모듈의 반환 형태 (return shape)가 제가 예상한 UI와 정확히 일치하지 않는다는 사실도 발견했습니다. 네이티브 모듈을 먼저 작성하면 데이터 계약 (data contract)이 정의되며, 그 후 RN 측에서는 단순히 그에 맞춰 구성하기만 하면 됩니다.

  3. 앱이 App Store 심사 중인 동안 iOS 랜딩 페이지 ( /ios ) 구축하기. 저는 승인 후에 페이지를 구축했기 때문에, 얻을 수 있었던 약 10일간의 유기적 발견 (organic discovery) 기회를 놓쳤습니다. App Store 등록 정보가 라이브되는 시점에는 /ios 페이지가 준비되어 인덱싱(indexing)되고 트래픽을 받고 있어야 하며, 그래야 두 요소가 서로를 강화할 수 있습니다.

1인 창업자를 위한 Expo + AI에 대한 솔직한 견해:

순수 Swift (Pure Swift)를 사용하면 약간 더 나은 앱을 만들 수 있습니다. Expo + AI 지원 변환을 사용하면 사용자 측면의 품질을 95% 수준으로 유지하면서도 4배 더 빠른 시간 안에 앱을 출시할 수 있습니다. 대부분의 1인 창업자에게는 두 번째 방식이 올바른 절충안 (tradeoff)입니다. Expo Modules API가 그 핵심 열쇠입니다.

여러분은 "모든 것을 JS로 하겠다"라고 약속하는 것이 아닙니다. "기본적으로는 JS를 사용하되, 중요한 부분에서는 네이티브 (native)를 사용하겠다"라고 약속하는 것입니다. 카메라 파이프라인 (pipeline)이 제품의 전부인 AI 피트니스 앱의 경우, 해당 특정 서브시스템 (subsystem)을 위해 Swift로 탈출구 (escape hatch)를 가질 수 있다는 점이 바로 여러분에게 정확히 필요한 부분입니다. 만약 여러분이 웹 앱을 운영 중이며 네이티브 iOS로의 전환을 고려하고 있다면, 앱이 진정으로 웹사이트 위에 얹혀진 얇은 레이어 (thin layer)가 아닌 이상 Capacitor 방식은 건너뛰겠습니다. Expo와 선택적인 네이티브 모듈 (native modules)의 조합은 제가 아무런 유보 없이 추천하는 경로입니다. 수치상으로는 다음과 같습니다: 웹에서의 30일: 4,779명의 사용자, 여전히 성장 중 / iOS: 406건의 설치, 그 중 [number]명이 Pro로 전환 / App Store 평점: 리뷰 3개 기준 ★★★★★. 만약 여러분이 Expo를 통해 웹 우선 (web-first)에서 네이티브 iOS로 전환했다면, 무엇이 가장 놀라웠는지 듣고 싶습니다. 저는 fitnitapp.com 에 있습니다. 그리고 iOS에서 직접 체험해보고 싶다면: iOS의 Fitnit

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
2

댓글

0