> ## Documentation Index
> Fetch the complete documentation index at: https://docs.replit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 네이티브 모바일 앱

> Agent로 네이티브 모바일 앱을 만들고, 전화기에서 테스트하고, App Store 게시를 위한 App Review에 제출하는 방법을 알아보세요.

export const MobileArchitectureDiagram = () => {
  if (typeof document !== 'undefined' && !document.getElementById('mobile-arch-styles-v3')) {
    const style = document.createElement('style');
    style.id = 'mobile-arch-styles-v3';
    style.textContent = `
      .mobile-arch-v3 {
        --arch-bg: var(--replit-docs-bg, #F6F6F4);
        --arch-surface: var(--replit-docs-bg-elevated, #F1F1EE);
        --arch-border: var(--replit-docs-border, #DEDAD5);
        --arch-text: var(--replit-docs-text, #1D1D1D);
        --arch-muted: var(--replit-docs-text-subtle, #858585);
        --arch-accent: #F26522;
        --arch-accent-soft: #FEF3EE;
      }
      
      .dark .mobile-arch-v3,
      html.dark .mobile-arch-v3,
      [data-theme="dark"] .mobile-arch-v3 {
        --arch-bg: var(--replit-docs-bg, #1E1E1F);
        --arch-surface: var(--replit-docs-bg-elevated, #222223);
        --arch-border: var(--replit-docs-border, #39393D);
        --arch-text: var(--replit-docs-text, #F5F5F5);
        --arch-muted: var(--replit-docs-text-subtle, #8E8F97);
        --arch-accent: #F26522;
        --arch-accent-soft: #1F1A17;
      }

      .arch-panel {
        transition: all 0.25s ease;
      }
      .arch-panel:hover {
        border-color: var(--arch-accent) !important;
      }
    `;
    document.head.appendChild(style);
  }
  const serverItems = [{
    label: 'Database',
    detail: 'PostgreSQL for structured data'
  }, {
    label: 'Object Storage',
    detail: 'Files, images, and media'
  }, {
    label: 'Secrets',
    detail: 'API keys stored securely'
  }, {
    label: 'API routes',
    detail: 'Server-side logic and endpoints'
  }];
  const clientItems = [{
    label: 'Native UI',
    detail: 'Real iOS and Android components'
  }, {
    label: 'Local state',
    detail: 'Fast, offline-capable data'
  }, {
    label: 'Device APIs',
    detail: 'Camera, location, notifications'
  }, {
    label: 'Live reload',
    detail: 'Instant preview via Expo Go'
  }];
  return <div className="mobile-arch-v3" style={{
    padding: '24px',
    borderRadius: '8px',
    border: '1px solid var(--arch-border)',
    background: 'var(--arch-bg)'
  }}>
      {}
      <div style={{
    marginBottom: '20px'
  }}>
        <div style={{
    fontSize: '18px',
    fontWeight: '600',
    color: 'var(--arch-text)'
  }}>
          Architecture
        </div>
        <div style={{
    fontSize: '14px',
    color: 'var(--arch-muted)',
    marginTop: '4px'
  }}>
          Your server runs on Replit. The app runs natively on devices.
        </div>
      </div>

      {}
      <div style={{
    display: 'flex',
    alignItems: 'stretch',
    gap: '16px'
  }}>
        
        {}
        <div className="arch-panel" style={{
    flex: 1,
    padding: '20px',
    borderRadius: '8px',
    border: '1px solid var(--arch-border)',
    background: 'var(--arch-surface)'
  }}>
          <div style={{
    fontSize: '13px',
    color: 'var(--arch-accent)',
    fontWeight: '600',
    marginBottom: '6px'
  }}>
            Replit Cloud
          </div>
          <div style={{
    fontSize: '16px',
    fontWeight: '600',
    color: 'var(--arch-text)',
    marginBottom: '16px'
  }}>
            Server
          </div>
          
          <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
            {serverItems.map(item => <div key={item.label}>
                <div style={{
    fontSize: '14px',
    fontWeight: '500',
    color: 'var(--arch-text)'
  }}>
                  {item.label}
                </div>
                <div style={{
    fontSize: '13px',
    color: 'var(--arch-muted)',
    marginTop: '2px'
  }}>
                  {item.detail}
                </div>
              </div>)}
          </div>
        </div>

        {}
        <div className="arch-panel" style={{
    flex: 1,
    padding: '20px',
    borderRadius: '8px',
    border: '1px solid var(--arch-border)',
    background: 'var(--arch-surface)'
  }}>
          <div style={{
    fontSize: '13px',
    color: 'var(--arch-accent)',
    fontWeight: '600',
    marginBottom: '6px'
  }}>
            User device
          </div>
          <div style={{
    fontSize: '16px',
    fontWeight: '600',
    color: 'var(--arch-text)',
    marginBottom: '16px'
  }}>
            Native app
          </div>
          
          <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
            {clientItems.map(item => <div key={item.label}>
                <div style={{
    fontSize: '14px',
    fontWeight: '500',
    color: 'var(--arch-text)'
  }}>
                  {item.label}
                </div>
                <div style={{
    fontSize: '13px',
    color: 'var(--arch-muted)',
    marginTop: '2px'
  }}>
                  {item.detail}
                </div>
              </div>)}
          </div>
        </div>
      </div>
    </div>;
};

export const MobileStackDiagram = () => {
  if (typeof document !== 'undefined' && !document.getElementById('mobile-stack-styles-v3')) {
    const style = document.createElement('style');
    style.id = 'mobile-stack-styles-v3';
    style.textContent = `
      .mobile-stack-v3 {
        --stack-bg: var(--replit-docs-bg, #F6F6F4);
        --stack-surface: var(--replit-docs-bg-elevated, #F1F1EE);
        --stack-border: var(--replit-docs-border, #DEDAD5);
        --stack-text: var(--replit-docs-text, #1D1D1D);
        --stack-muted: var(--replit-docs-text-subtle, #858585);
        --stack-accent: #F26522;
        --stack-accent-soft: #FEF3EE;
      }
      
      .dark .mobile-stack-v3,
      html.dark .mobile-stack-v3,
      [data-theme="dark"] .mobile-stack-v3 {
        --stack-bg: var(--replit-docs-bg, #1E1E1F);
        --stack-surface: var(--replit-docs-bg-elevated, #222223);
        --stack-border: var(--replit-docs-border, #39393D);
        --stack-text: var(--replit-docs-text, #F5F5F5);
        --stack-muted: var(--replit-docs-text-subtle, #8E8F97);
        --stack-accent: #F26522;
        --stack-accent-soft: #1F1A17;
      }

      .stack-layer {
        transition: all 0.25s ease;
        cursor: pointer;
      }
      .stack-layer:hover,
      .stack-layer.active {
        background: var(--stack-accent-soft) !important;
        border-color: var(--stack-accent) !important;
      }
      .stack-layer .layer-detail {
        max-height: 0;
        overflow: hidden;
        opacity: 0;
        transition: max-height 0.3s ease, opacity 0.2s ease, margin 0.3s ease;
        margin-top: 0;
      }
      .stack-layer.active .layer-detail {
        max-height: 80px;
        opacity: 1;
        margin-top: 8px;
      }
      
      .platform-item {
        transition: all 0.2s ease;
      }
      .platform-item:hover {
        border-color: var(--stack-accent) !important;
        background: var(--stack-accent-soft) !important;
      }
    `;
    document.head.appendChild(style);
  }
  const handleLayerClick = e => {
    const layer = e.currentTarget;
    const allLayers = document.querySelectorAll('.stack-layer');
    const wasActive = layer.classList.contains('active');
    allLayers.forEach(l => l.classList.remove('active'));
    if (!wasActive) {
      layer.classList.add('active');
    }
  };
  const runAnimation = () => {
    const btn = document.getElementById('stack-animate-btn');
    if (!btn || btn.dataset.running === 'true') return;
    btn.dataset.running = 'true';
    btn.textContent = 'Running...';
    btn.style.opacity = '0.6';
    const layers = document.querySelectorAll('.stack-layer');
    const platforms = document.getElementById('stack-platforms-container');
    let step = 0;
    const totalSteps = layers.length + 1;
    const animate = () => {
      layers.forEach(l => l.classList.remove('active'));
      if (platforms) platforms.style.borderColor = 'var(--stack-border)';
      if (step < layers.length) {
        layers[step].classList.add('active');
        step++;
        setTimeout(animate, 4000);
      } else if (step === layers.length) {
        if (platforms) platforms.style.borderColor = 'var(--stack-accent)';
        step++;
        setTimeout(animate, 4000);
      } else {
        layers.forEach(l => l.classList.remove('active'));
        if (platforms) platforms.style.borderColor = 'var(--stack-border)';
        btn.dataset.running = 'false';
        btn.textContent = 'See the flow';
        btn.style.opacity = '1';
      }
    };
    animate();
  };
  const layers = [{
    title: 'Replit Agent',
    summary: 'Builds your app from a prompt',
    detail: 'Describe what you want in natural language. Agent writes TypeScript code, configures dependencies, and sets up your project.',
    verb: 'writes'
  }, {
    title: 'Your code',
    summary: 'TypeScript + React components',
    detail: 'Your app is standard React code that you own and can customize. No vendor lock-in.',
    verb: 'using'
  }, {
    title: 'Expo',
    summary: 'Development framework',
    detail: 'Expo simplifies React Native development with managed builds, over-the-air updates, and easy access to device APIs.',
    verb: 'simplifies'
  }, {
    title: 'React Native',
    summary: 'Cross-platform UI',
    detail: 'React Native compiles your JavaScript to real native code, not a web view. True native performance.',
    verb: 'compiles to'
  }];
  const platforms = ['iOS', 'Android', 'Web'];
  return <div className="mobile-stack-v3" style={{
    padding: '24px',
    borderRadius: '8px',
    border: '1px solid var(--stack-border)',
    background: 'var(--stack-bg)'
  }}>
      {}
      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'flex-start',
    marginBottom: '20px'
  }}>
        <div>
          <div style={{
    fontSize: '18px',
    fontWeight: '600',
    color: 'var(--stack-text)'
  }}>
            Technology stack
          </div>
          <div style={{
    fontSize: '14px',
    color: 'var(--stack-muted)',
    marginTop: '4px'
  }}>
            Tap any layer to learn more
          </div>
        </div>
        <button id="stack-animate-btn" onClick={runAnimation} data-running="false" style={{
    padding: '8px 14px',
    fontSize: '13px',
    color: 'var(--stack-accent)',
    background: 'transparent',
    border: '1px solid var(--stack-accent)',
    borderRadius: '4px',
    cursor: 'pointer'
  }}>
          See the flow
        </button>
      </div>

      {}
      <div style={{
    display: 'flex',
    flexDirection: 'column',
    gap: '8px'
  }}>
        {layers.map(layer => <div key={layer.title}>
            <div className="stack-layer" onClick={handleLayerClick} style={{
    padding: '16px',
    borderRadius: '8px',
    border: '1px solid var(--stack-border)',
    background: 'var(--stack-surface)'
  }}>
              <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center'
  }}>
                <div>
                  <div style={{
    fontSize: '16px',
    fontWeight: '500',
    color: 'var(--stack-text)'
  }}>
                    {layer.title}
                  </div>
                  <div style={{
    fontSize: '14px',
    color: 'var(--stack-muted)',
    marginTop: '4px'
  }}>
                    {layer.summary}
                  </div>
                </div>
                <div style={{
    fontSize: '14px',
    color: 'var(--stack-accent)',
    fontWeight: '500'
  }}>
                  {layer.verb}
                </div>
              </div>
              <div className="layer-detail" style={{
    fontSize: '14px',
    color: 'var(--stack-muted)',
    lineHeight: '1.5'
  }}>
                {layer.detail}
              </div>
            </div>
          </div>)}
      </div>

      {}
      <div id="stack-platforms-container" style={{
    display: 'flex',
    gap: '12px',
    padding: '12px',
    marginTop: '8px',
    borderRadius: '8px',
    border: '1px solid var(--stack-border)',
    background: 'var(--stack-surface)',
    transition: 'border-color 0.25s ease'
  }}>
        {platforms.map(platform => <div key={platform} className="platform-item" style={{
    flex: 1,
    padding: '12px',
    borderRadius: '6px',
    border: '1px solid var(--stack-border)',
    background: 'var(--stack-bg)',
    textAlign: 'center',
    fontSize: '15px',
    fontWeight: '500',
    color: 'var(--stack-text)'
  }}>
            {platform}
          </div>)}
      </div>
    </div>;
};

<Frame>
  <img src="https://mintcdn.com/replit/TlSUj3SmUsRG399T/images/native-mobile-apps/mobile-screen.png?fit=max&auto=format&n=TlSUj3SmUsRG399T&q=85&s=d512e8c7bf0a86ecacd92fbef1f52749" alt="미리보기 기기 패널과 QR 코드가 있는 Replit 프로젝트 편집기 미리보기에서 실행 중인 모바일 앱" width="1440" height="900" data-path="images/native-mobile-apps/mobile-screen.png" />
</Frame>

Agent로 iOS 및 Android 앱을 빌드하고, 전화기에서 미리보기하고, 안내된 흐름을 통해 빌드를 준비하세요.

<Note>
  네이티브 iOS 앱을 빌드하거나 미리보기하려면 [replit.com](https://replit.com)의 프로젝트 편집기를 사용하세요. 네이티브 모바일 앱 작업은 지원되는 경우 Replit Android 앱에서도 가능합니다.
</Note>

## 시작하기

몇 단계만으로 작동하는 모바일 앱을 만들 수 있습니다:

<Steps>
  <Step title="모바일 앱 만들기">
    Replit 홈 화면에서 앱 아이디어를 설명하고 앱 유형으로 **모바일 앱**을 선택하세요.

    <Frame>
      <img src="https://mintcdn.com/replit/TlSUj3SmUsRG399T/images/native-mobile-apps/prompt.png?fit=max&auto=format&n=TlSUj3SmUsRG399T&q=85&s=21bba53b72382bf6fb0dc37a168de9e8" alt="모바일 앱이 앱 유형으로 선택된 Replit 홈 화면 프롬프트" width="1440" height="900" data-path="images/native-mobile-apps/prompt.png" />
    </Frame>
  </Step>

  <Step title="앱 테스트하기">
    빌드하는 동안 모바일 앱을 미리보기하는 방법이 두 가지 있습니다:

    * **프로젝트 편집기에서**: 미리보기 패널의 기기 선택기에서 **iOS 시뮬레이터** 또는 **Android 에뮬레이터**를 선택하세요. 실제 시뮬레이터가 프로젝트 편집기로 스트리밍되어 Replit을 벗어나지 않고 탭할 수 있습니다. Xcode나 Android Studio가 필요하지 않습니다.
    * **Expo Go로 실제 전화기에서**: iPhone 또는 Android 기기에 [Expo Go](https://expo.dev/go)를 설치하세요. 프로젝트 편집기의 미리보기 패널에서 **Expo Go로 열기**를 선택하고 QR 코드를 스캔하세요.

    <Frame>
      <img src="https://mintcdn.com/replit/TlSUj3SmUsRG399T/images/native-mobile-apps/workspace.png?fit=max&auto=format&n=TlSUj3SmUsRG399T&q=85&s=6508e73ef3c21dcee8221e884164a23c" alt="앱 미리보기와 모바일 기기에서 미리보기 옵션이 표시된 Replit 프로젝트 편집기" width="1440" height="900" data-path="images/native-mobile-apps/workspace.png" />
    </Frame>
  </Step>

  <Step title="Agent로 반복하기">
    Agent에게 기능 추가, 데이터 소스 연결, 서비스 연동을 요청하세요. 반복하면서 시뮬레이터 또는 전화기에서 계속 테스트하세요.
  </Step>
</Steps>

<Tip>
  프로젝트 편집기의 **iOS 시뮬레이터**와 **Android 에뮬레이터**는 플랫폼별 스타일링과 대부분의 네이티브 컴포넌트를 포함하여 앱의 네이티브 버전을 보여줍니다. 햅틱, 카메라, 푸시 알림, 위치 등 실제 하드웨어에 의존하는 기능은 Expo Go로 실제 전화기에서 테스트하세요.
</Tip>

## 시뮬레이터 또는 에뮬레이터에서 미리보기

Replit은 Expo를 사용하여 전화기에서 모바일 앱을 실행하고 테스트할 수 있게 해줍니다. 또는 Replit이 실제 **iOS 시뮬레이터** 또는 **Android 에뮬레이터**를 미리보기 패널로 직접 스트리밍합니다. Agent로 변경 사항을 만들면 실제 기기에서처럼 시뮬레이터에서 핫 리로드됩니다.

<Note>
  iOS 시뮬레이터와 Android 에뮬레이터 미리보기는 Core, Pro, Enterprise 플랜의 빌더에게 제공됩니다. 모바일 앱 전용으로, 웹 전용 프로젝트에서는 옵션이 숨겨집니다.
</Note>

### 시뮬레이터 열기

<Steps>
  <Step title="모바일 프로젝트 열기">
    모바일 아티팩트가 있는 프로젝트를 열세요.
  </Step>

  <Step title="기기 선택하기">
    상단의 미리보기 패널 기기 선택기에서 **iOS 시뮬레이터** 또는 **Android 에뮬레이터**를 선택하세요. 시뮬레이터가 웹 미리보기 대신 부팅됩니다.
  </Step>

  <Step title="실제 기기처럼 상호작용하기">
    마우스와 키보드를 사용하여 탭하고, 스와이프하고, 입력하고, 탐색하세요. 툴바에서 **재시작**을 선택하면 시뮬레이터를 재시작하지 않고 앱을 다시 로드할 수 있습니다.
  </Step>
</Steps>

<Tip>
  시뮬레이터는 클라우드에서 실행되어 브라우저로 스트리밍되므로 다운로드나 설정이 필요하지 않습니다. 프로젝트 파일은 Replit에 유지됩니다.
</Tip>

<Frame>
  <video autoPlay muted loop playsInline src="https://cdn.replit.com/sanity/ios-emulator.mp4" />
</Frame>

### 브라우저 지원

* **Chrome**, **Safari**, Chromium 기반 브라우저가 완전히 지원됩니다.
* **Firefox**는 지원되지 않습니다. Firefox에서 Replit을 열면 iOS 및 Android 옵션이 비활성화되고 "Firefox는 지원되지 않음" 메모가 표시됩니다. 이는 시뮬레이터를 구동하는 스트리밍 기술의 제한입니다.

### 대신 Expo Go를 사용해야 하는 경우

실제 하드웨어에 의존하는 기능을 테스트해야 하거나 컴퓨터 앞에 없는 사람과 미리보기를 공유하고 싶을 때 실제 전화기에서 Expo Go를 사용하세요:

* 카메라, 마이크, 햅틱, 기타 네이티브 센서
* 푸시 알림
* 실제 GPS 위치
* 팀원, 테스터, 또는 투자자와 개발 미리보기 공유

Expo Go에서 앱을 열려면 [replit.com](https://replit.com)에서 프로젝트를 열고, 프로젝트 편집기의 미리보기 패널에서 **Expo Go로 열기**를 선택한 다음 전화기의 [Expo Go](https://expo.dev/go) 앱으로 QR 코드를 스캔하세요.

### 전체 화면 시뮬레이터

모바일 아티팩트 카드에서 **추가 액션** → **iOS 시뮬레이터에서 열기** 또는 **Android 에뮬레이터에서 열기**를 선택하면 더 큰 뷰포트로 자체 탭에서 시뮬레이터를 열 수 있습니다.

## 모바일 앱을 빌드해야 하는 이유

다음이 필요할 때 모바일 앱을 빌드하세요:

* **네이티브 경험**: 빠른 성능, 부드러운 상호작용, 플랫폼 네이티브 UI.
* **기기 기능**: 카메라, 푸시 알림, 위치 등.
* **App Store 배포**: Apple의 App Review 프로세스 후 사람들이 발견하고 설치할 수 있는 공유 가능한 리스팅.

## 주요 기능

* **AI 우선 생성**: 앱을 설명하면 Agent가 작동하는 모바일 앱을 스캐폴딩합니다.
* **프로젝트 편집기 미리보기**: Replit을 벗어나지 않고 iOS 시뮬레이터나 Android 에뮬레이터에서 테스트하거나, Expo Go로 전화기에서 미리보기하세요.
* **기본적으로 풀스택**: 앱이 성장함에 따라 서버 경로, 데이터베이스, 앱 스토리지, 커넥터, AI 연동을 추가하세요.
* **안내된 제출**: 로컬 iOS 툴체인을 관리하지 않고 TestFlight 및 App Store 제출을 위한 빌드를 준비하세요.

## 개발 워크플로우

앱에 접근하는 세 가지 단계가 있으며, 각각 다른 대상과 기능을 가집니다:

| 단계            | 접근 가능한 사람       | 접근 방법                             | 최적 용도               |
| ------------- | --------------- | --------------------------------- | ------------------- |
| **개발**        | 본인              | 프로젝트 편집기 시뮬레이터, 또는 Expo Go용 QR 코드 | 빌드 및 반복             |
| **배포**        | Expo Go가 있는 테스터 | QR 코드가 있는 공개 URL                  | 개발 미리보기, 프로토타이핑, 데모 |
| **App Store** | 누구나             | App Review 후 App Store에서 다운로드     | 프로덕션 출시             |

**개발**: 앱을 시작하면 프로젝트 편집기의 **iOS 시뮬레이터** 또는 **Android 에뮬레이터**에서 미리보기하거나, 미리보기 패널의 QR 코드를 스캔하여 전화기의 Expo Go에서 열 수 있습니다.

**배포**: **게시**를 클릭하면 Replit이 Expo Go용 QR 코드가 있는 공개 URL을 만듭니다. 이는 개발 미리보기와 프로토타이핑에 이상적입니다. App Store 프로세스를 시작하기 전에 투자자에게 보여주거나, 피드백을 받거나, 친구들과 테스트하세요.

**App Store**: App Store에 제출하면 Apple이 앱을 검토합니다. 승인 후 사람들이 App Store에서 다운로드하고 설치할 수 있습니다. Apple 개발자 계정이 필요합니다.

## 게시 개요

iOS 릴리스를 준비할 때 일반적인 흐름은 다음과 같습니다:

* 프로젝트 편집기에서 게시
* TestFlight에 빌드 제출
* App Store Connect에서 TestFlight 빌드를 App Store로 승격

<Note>
  TestFlight 및 App Store에 빌드를 제출하려면 Apple Developer Program 멤버십이 필요합니다.
</Note>

전체 안내는 [모바일 앱 빌드 및 출시](/build/mobile-app) 튜토리얼을 참조하세요.

## 네이티브 모바일 앱을 빌드하는 위치

Replit 환경은 로컬 머신이 아닌 클라우드에서 실행됩니다. 네이티브 모바일 앱을 빌드하려면 [replit.com](https://replit.com)의 프로젝트 편집기를 사용하세요. 모바일 앱 워크플로우(Agent 스캐폴딩, Expo Go 미리보기, 시뮬레이터 및 에뮬레이터 테스트, TestFlight 빌드, App Store 제출)는 프로젝트 편집기에서 실행됩니다.

네이티브 모바일 앱 작업은 지원되는 경우 [Replit Android 앱](/references/platforms/mobile-app)에서도 가능합니다. Replit iOS 앱을 사용 중이라면 [replit.com](https://replit.com)에서 프로젝트를 열어 네이티브 모바일 앱을 만들고, 미리보기하고, 빌드하거나 제출하세요.

## 기술 작동 방식

모바일 앱은 함께 작동하는 기술 스택으로 빌드됩니다. 이 섹션에서는 앱을 구동하는 것과 각 요소가 어떻게 맞물리는지 설명합니다.

### 기술 스택

<MobileStackDiagram />

* **React Native**는 하나의 코드베이스를 작성하여 iOS, Android, 웹에 컴파일할 수 있는 오픈 소스 프레임워크입니다. 웹뷰가 아닌 플랫폼 네이티브 UI 컴포넌트를 렌더링합니다.
* **Expo**는 빌드를 처리하고, 네이티브 모듈을 관리하고, 미리보기를 위한 Expo Go 같은 도구를 제공하여 React Native 개발을 단순화합니다.
* **Expo Go**는 전화기에 설치하는 무료 앱입니다. 전체 네이티브 바이너리를 빌드하지 않고 실제 기기에서 테스트할 수 있도록 개발 미리보기를 실행합니다.

앱을 실행하면 Metro 번들러가 코드를 컴파일하여 기기로 푸시합니다. 첫 번째 빌드는 캐시가 없기 때문에 시간이 더 걸립니다. 이후 빌드는 더 빠릅니다.

### 아키텍처: 서버와 클라이언트

모바일 앱을 미리보기나 배포용으로 준비할 때 두 가지를 다룹니다:

1. **서버**: 클라우드의 Replit에서 실행됩니다. 데이터베이스, API 경로, AI 연동, 백엔드 로직을 처리합니다.
2. **클라이언트 앱**: 사람의 전화기에서 실행됩니다. 개발 중에는 Expo Go에서 미리보기되거나, 검토 후 앱 스토어를 통해 배포되는 네이티브 앱입니다.

<MobileArchitectureDiagram />

이 분리를 통해 유연성이 제공됩니다. 서버에서 복잡한 로직을 실행하고(Replit의 데이터베이스, 오브젝트 스토리지, 커넥터에 접근 가능) 클라이언트를 가볍게 유지할 수 있습니다. 빌드하면서 전화기에서 일어나야 할 일과 클라우드에서 일어나야 할 일을 생각해보세요.

## 고려 사항

* **게시 요구 사항**: Apple이 TestFlight 및 App Store에 대한 요구 사항을 설정하며, Apple이 배포 전에 iOS 앱을 검토합니다.
* **Android 게시**: iOS 및 Android용 크로스 플랫폼 앱을 빌드할 수 있습니다. 안내된 경험을 통한 Google Play 게시는 아직 지원되지 않지만 수동으로 완료할 수 있습니다.
* **네이티브 변경**: 앱 아이콘이나 권한 같은 변경은 일반적으로 새 스토어 빌드가 필요합니다.

## 문제 해결

모바일 앱 개발 중 문제가 발생하면 일반적인 문제와 해결 방법에 대해 [모바일 앱 문제 해결](/references/troubleshooting/mobile-app)을 참조하세요.

## 다음 단계

* Agent 작동 방식 알아보기: [Agent](/references/agent/overview)
* 연동 탐색: [연동](/references/integrations/overview)
* 모바일에서 Replit 사용: [Replit 모바일 앱](/references/platforms/mobile-app)
* Expo에 대해 자세히 읽기: [Expo](https://expo.dev/)
* TestFlight 및 제출 관리: [App Store Connect](https://appstoreconnect.apple.com/)

## FAQ

<AccordionGroup>
  <Accordion title="Expo란 무엇인가요?">
    Expo는 Agent가 Replit에서 모바일 앱을 빌드하는 데 사용하는 도구입니다. React Native로 크로스 플랫폼 네이티브 앱을 빌드, 실행, 배포하기 위한 오픈 소스 플랫폼 및 툴체인입니다. [https://expo.dev에서](https://expo.dev에서) 자세히 알아보세요.
  </Accordion>

  <Accordion title="React Native란 무엇인가요?">
    React Native는 Meta에서 만든 오픈 소스 프레임워크로, React와 JavaScript 또는 TypeScript를 사용하여 네이티브 iOS 및 Android 앱을 빌드합니다. 웹뷰가 아닌 플랫폼 네이티브 UI 컴포넌트를 렌더링하므로 앱이 네이티브처럼 보이고 느껴집니다.
  </Accordion>

  <Accordion title="Expo Go란 무엇인가요?">
    Expo Go는 App Store 또는 Google Play에서 전화기에 설치하는 무료 앱입니다. 전체 네이티브 바이너리를 빌드하지 않고 개발 중에 모바일 앱을 미리보기할 수 있게 해줍니다. 프로젝트 편집기에서 QR 코드를 스캔하면 Expo Go가 앱 코드를 다운로드하여 실행합니다.
  </Accordion>

  <Accordion title="Expo Go와 개발 빌드의 차이점은 무엇인가요?">
    **Expo Go**는 코드를 실행하는 미리 빌드된 앱입니다. 설정이 빠르지만 Expo SDK에 포함된 모듈만 지원합니다.

    **개발 빌드**는 모든 네이티브 모듈을 포함할 수 있는 커스텀 네이티브 바이너리입니다. 더 많은 설정이 필요하지만(EAS Build 또는 로컬 Xcode/Android Studio) 더 많은 유연성을 제공합니다.

    Replit은 개발 미리보기에 Expo Go를 사용합니다. Expo Go에서 지원하지 않는 네이티브 모듈이 필요하다면 [Expo 문서](https://docs.expo.dev/develop/development-builds/introduction/)를 통해 개발 빌드를 탐색해야 할 수 있습니다.
  </Accordion>

  <Accordion title="모바일 반응형 웹 앱과 어떻게 다른가요?">
    모바일 반응형 웹 앱은 브라우저에서 레이아웃을 적응시키는 웹사이트입니다. React Native 앱은 기기에 설치된 네이티브 애플리케이션으로, 플랫폼 API(카메라, 햅틱, 푸시 알림)를 사용하고, 하드웨어 및 오프라인 기능에 더 잘 접근하며, 앱 스토어를 통해 배포됩니다. 반응형 웹은 도달 범위와 설치 불필요에 적합하고, 기기 기능, 성능 또는 App Store 배포가 필요할 때는 네이티브가 최적입니다.
  </Accordion>

  <Accordion title="Mac이나 Xcode가 필요한가요?">
    아니요. Replit과 Expo가 클라우드에서 빌드 프로세스를 관리합니다.
  </Accordion>

  <Accordion title="Apple 개발자 계정 없이 미리보기할 수 있나요?">
    네. 프로젝트 편집기의 iOS 시뮬레이터 또는 전화기의 Expo Go로 미리보기할 수 있습니다. TestFlight 또는 App Store에 제출할 준비가 되었을 때만 Apple 개발자 계정이 필요합니다.
  </Accordion>

  <Accordion title="Android가 지원되나요?">
    네. 동일한 코드베이스에서 iOS 및 Android용 크로스 플랫폼 앱을 빌드할 수 있습니다. 프로젝트 편집기의 Android 에뮬레이터 또는 Expo Go가 있는 Android 기기에서 미리보기하세요. Google Play 게시는 수동으로 완료할 수 있습니다.
  </Accordion>

  <Accordion title="서버와 데이터베이스는 어떻게 되나요?">
    Replit의 내장 PostgreSQL, 오브젝트 스토리지, 커넥터, AI 연동을 사용하세요. 별도의 인프라가 필요하지 않습니다. 서버는 Replit에서 실행되고 모바일 앱이 연결됩니다.
  </Accordion>

  <Accordion title="프로젝트 편집기 시뮬레이터와 실제 전화기 중 어디서 테스트해야 하나요?">
    각각 다른 시점에 두 가지 모두 사용하세요. 프로젝트 편집기의 **iOS 시뮬레이터**와 **Android 에뮬레이터**는 앱의 네이티브 버전을 보여주며 레이아웃, 탐색, 네이티브 컴포넌트, 플랫폼 스타일링 등 대부분의 일상적인 테스트를 다룹니다. 카메라, 햅틱, 푸시 알림, GPS 위치 등 실제 하드웨어에 의존하는 기능을 테스트해야 할 때는 **Expo Go**가 있는 실제 전화기로 전환하세요.
  </Accordion>
</AccordionGroup>
