Communitygithub.com

Expo EAS 호스팅 및 웹 배포

Expo 웹사이트와 Expo Router API 라우트를 EAS Hosting에 배포합니다. 웹 번들을 내보내고, 프로덕션 및 PR 미리보기 URL을 위해 eas deploy를 실행하며, 환경 비밀과 커스텀 도메인을 관리하고 Cloudflare Workers 런타임에서 작업합니다. 또한 API 라우트 작성(+api.ts 핸들러, HTTP 메서드, 요청 처리, CORS)을 다룹니다. Expo 웹 앱이나 API 라우트를 배포하거나, EAS Hosting을 설정하거나, 호스팅 환경 및 도메인을 구성할 때 사용하세요. 네이티브 빌드 또는 스토어 출시에는 사용하지 마세요. 해당 작업은 eas-app-stores 스킬을 사용하세요.

Expo EAS 호스팅 및 웹 배포란 무엇인가요?

이 스킬은 Expo 웹 앱과 Expo Router API 라우트를 EAS Hosting(Expo의 관리형 엣지, Cloudflare Workers)에 배포하는 전체 워크플로우를 제공합니다. `npx expo export -p web`으로 웹 번들을 내보내고 `eas deploy` 명령어 하나로 프로덕션 및 PR 미리보기 URL을 생성할 수 있습니다. 주요 기능으로는 API 라우트 작성(+api.ts 파일, HTTP 메서드, 동적 라우팅, 요청 파라미터 및 헤더 처리, CORS, 오류 처리), 환경 변수 및 비밀 관리(`eas env:create`), 커스텀 도메인 구성, EAS Workflows를 통한 CI/CD 자동화 등이 있습니다. EAS Hosting은 Cloudflare Workers 기반이므로 Node.js 파일시스템 접근 불가, 30초 실행 시간 제한, 지속적 연결 불가 등의 제약이 있으며, 대신 Web API와 클라우드 데이터베이스(Cloudflare D1, Turso, PlanetScale 등)를 사용해야 합니다. 이 스킬은 Expo 생태계에서 웹 앱과 서버리스 API를 함께 운영하려는 개발자에게 필수적이며, 무료로 API 라우트를 작성하고 웹 번들을 내보낼 수 있지만 EAS Hosting에 배포할 경우 유료 요금제가 적용됩니다.

지원 대상~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/expo/skills/tree/HEAD/plugins/expo/skills/eas-hosting

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

EAS Hosting

EAS 서비스 - 비용 발생. EAS Hosting은 유료 Expo Application Services 제품으로, 무료 티어 한도가 있습니다. 프로덕션 배포는 요금제의 요청 및 대역폭 허용량을 사용합니다. 자세한 내용은 https://expo.dev/pricing을 참조하세요. API 라우트 작성 및 웹 번들 내보내기는 무료이며 오픈 소스이므로, 내보낸 서버 출력을 EAS Hosting 대신 자체 호스팅할 수도 있습니다.

EAS Hosting은 Expo 웹 앱과 API 라우트를 Expo의 관리형 엣지(Cloudflare Workers)에 배포합니다. npx expo export -p web으로 웹 번들을 내보내고 eas deploy로 배포합니다. 동일한 명령어로 함께 번들링된 모든 Expo Router API 라우트도 배포합니다. 이 스킬은 웹사이트 배포, API 라우트 작성 및 호스팅 런타임을 다룹니다. 배포 워크플로우는 아래 배포 섹션을 참조하세요.

API 라우트 사용 시기

다음과 같은 경우 API 라우트를 사용하세요.

  • 서버 측 비밀 — 클라이언트에 절대 도달해서는 안 되는 API 키, 데이터베이스 자격 증명 또는 토큰
  • 데이터베이스 작업 — 노출되어서는 안 되는 직접 데이터베이스 쿼리
  • 타사 API 프록시 — 외부 서비스(OpenAI, Stripe 등) 호출 시 API 키 숨기기
  • 서버 측 유효성 검사 — 데이터베이스 쓰기 전 데이터 검증
  • 웹훅 엔드포인트 — Stripe 또는 GitHub과 같은 서비스의 콜백 수신
  • 속도 제한 — 서버 수준에서 액세스 제어
  • 무거운 계산 — 모바일에서 느릴 수 있는 처리를 오프로드

API 라우트를 사용하지 말아야 할 경우

다음과 같은 경우 API 라우트를 피하세요.

  • 데이터가 이미 공개된 경우 — 대신 공개 API에 직접 fetch 사용
  • 비밀이 필요 없는 경우 — 정적 데이터 또는 클라이언트-안전 작업
  • 실시간 업데이트가 필요한 경우 — WebSocket 또는 Supabase Realtime과 같은 서비스 사용
  • 간단한 CRUD — 관리형 백엔드를 위해 Firebase, Supabase 또는 Convex 고려
  • 파일 업로드 — 직접-스토리지 업로드 사용(S3 사전 서명된 URL, Cloudflare R2)
  • 인증 전용 — 대신 Clerk, Auth0 또는 Firebase Auth 사용

파일 구조

API 라우트는 app 디렉토리에 +api.ts 접미사로 위치합니다:

app/
  api/
    hello+api.ts          → GET /api/hello
    users+api.ts          → /api/users
    users/[id]+api.ts     → /api/users/:id
  (tabs)/
    index.tsx

기본 API 라우트

// app/api/hello+api.ts
export function GET(request: Request) {
  return Response.json({ message: "Hello from Expo!" });
}

HTTP 메서드

각 HTTP 메서드에 대해 명명된 함수를 내보냅니다:

// app/api/items+api.ts
export function GET(request: Request) {
  return Response.json({ items: [] });
}

export async function POST(request: Request) {
  const body = await request.json();
  return Response.json({ created: body }, { status: 201 });
}

export async function PUT(request: Request) {
  const body = await request.json();
  return Response.json({ updated: body });
}

export async function DELETE(request: Request) {
  return new Response(null, { status: 204 });
}

동적 라우트

// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
  return Response.json({ userId: id });
}

요청 처리

쿼리 파라미터

export function GET(request: Request) {
  const url = new URL(request.url);
  const page = url.searchParams.get("page") ?? "1";
  const limit = url.searchParams.get("limit") ?? "10";

  return Response.json({ page, limit });
}

헤더

export function GET(request: Request) {
  const auth = request.headers.get("Authorization");

  if (!auth) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  return Response.json({ authenticated: true });
}

JSON 본문

export async function POST(request: Request) {
  const { email, password } = await request.json();

  if (!email || !password) {
    return Response.json({ error: "Missing fields" }, { status: 400 });
  }

  return Response.json({ success: true });
}

환경 변수

서버 측 비밀에는 process.env를 사용하세요:

// app/api/ai+api.ts
export async function POST(request: Request) {
  const { prompt } = await request.json();

  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: "gpt-4",
      messages: [{ role: "user", content: prompt }],
    }),
  });

  const data = await response.json();
  return Response.json(data);
}

환경 변수 설정:

  • 로컬: .env 파일 생성 (절대 커밋 금지)
  • EAS Hosting: eas env:create 명령어 또는 Expo 대시보드 사용

CORS 헤더

웹 클라이언트를 위한 CORS 추가:

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export function OPTIONS() {
  return new Response(null, { headers: corsHeaders });
}

export function GET() {
  return Response.json({ data: "value" }, { headers: corsHeaders });
}

오류 처리

export async function POST(request: Request) {
  try {
    const body = await request.json();
    // 처리...
    return Response.json({ success: true });
  } catch (error) {
    console.error("API error:", error);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

로컬 테스트

API 라우트를 포함한 개발 서버 시작:

npx expo serve

이 명령어는 http://localhost:8081에서 전체 API 라우트 지원과 함께 로컬 서버를 시작합니다.

curl로 테스트:

curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'

EAS Hosting에 배포

사전 요구 사항

npm install -g eas-cli
eas login

배포

배포는 웹 번들과 모든 Expo Router API 라우트를 함께 전송합니다. eas deploy가 둘 다 처리합니다. 전체 웹사이트, API 라우트 전용 백엔드 또는 둘 다 있는 경우에도 내보내기가 실행됩니다.

# 웹 번들 내보내기 (모든 API 라우트 포함)
npx expo export -p web

# 미리보기 배포 (PR 스타일 URL)
npx eas-cli@latest deploy

# 프로덕션 배포
npx eas-cli@latest deploy --prod

모든 것이 EAS Hosting(Cloudflare Workers)에 배포됩니다.

프로덕션을 위한 환경 변수

# 비밀 생성
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production

# 또는 Expo 대시보드 사용

커스텀 도메인

eas.json 또는 Expo 대시보드에서 구성합니다.

EAS Workflows로 자동화

type: deploy 워크플로우를 사용하여 main 브랜치에 푸시될 때마다 웹사이트(및 API 라우트)를 배포합니다:

.eas/workflows/deploy.yml

name: Deploy

on:
  push:
    branches:
      - main

# https://docs.expo.dev/eas/workflows/syntax/#deploy
jobs:
  deploy_web:
    type: deploy
    params:
      prod: true

풀 리퀘스트에 대한 미리보기 배포는 동일한 작업 유형을 prod: false로 사용합니다:

name: Web PR Preview

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  preview:
    type: deploy
    params:
      prod: false

이 예제를 넘어 워크플로우 YAML을 작성하거나 검증하려면 eas-workflows 스킬을 사용하세요.

EAS Hosting 런타임 (Cloudflare Workers)

API 라우트는 Cloudflare Workers에서 실행됩니다. 주요 제한 사항:

누락/제한된 API

  • Node.js 파일시스템 없음 — fs 모듈 사용 불가
  • 네이티브 Node 모듈 없음 — Web API 또는 폴리필 사용
  • 제한된 실행 시간 — CPU 집약적 작업에 대해 30초 시간 초과
  • 지속적 연결 없음 — WebSocket은 Durable Objects 필요
  • fetch 사용 가능 — HTTP 요청에 표준 fetch 사용

대신 Web API 사용

// Node crypto 대신 Web Crypto 사용
const hash = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode("data")
);

// node-fetch 대신 fetch 사용
const response = await fetch("https://api.example.com");

// Response/Request 사용 (이미 제공됨)
return new Response(JSON.stringify(data), {
  headers: { "Content-Type": "application/json" },
});

데이터베이스 옵션

파일시스템을 사용할 수 없으므로 클라우드 데이터베이스를 사용하세요:

  • Cloudflare D1 — 엣지에서의 SQLite
  • Turso — 분산 SQLite
  • PlanetScale — 서버리스 MySQL
  • Supabase — REST API가 있는 Postgres
  • Neon — 서버리스 Postgres

Turso 예제:

// app/api/users+api.ts
import { createClient } from "@libsql/client/web";

const db = createClient({
  url: process.env.TURSO_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export async function GET() {
  const result = await db.execute("SELECT * FROM users");
  return Response.json(result.rows);
}

클라이언트에서 API 라우트 호출

// React Native 컴포넌트에서
const response = await fetch("/api/hello");
const data = await response.json();

// 본문 포함
const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John" }),
});

일반적인 패턴

인증 미들웨어

// utils/auth.ts
export async function requireAuth(request: Request) {
  const token = request.headers.get("Authorization")?.replace("Bearer ", "");

  if (!token) {
    throw new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }

  // 토큰 확인...
  return { userId: "123" };
}

// app/api/protected+api.ts
import { requireAuth } from "../../utils/auth";

export async function GET(request: Request) {
  const { userId } = await requireAuth(request);
  return Response.json({ userId });
}

외부 API 프록시

// app/api/weather+api.ts
export async function GET(request: Request) {
  const url = new URL(request.url);
  const city = url.searchParams.get("city");

  const response = await fetch(
    `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`
  );

  return Response.json(await response.json());
}

규칙

  • 클라이언트 코드에 API 키나 비밀을 절대 노출하지 마십시오
  • 항상 사용자 입력을 검증하고 삭제 처리하십시오
  • 적절한 HTTP 상태 코드 사용 (200, 201, 400, 401, 404, 500)
  • try/catch로 오류를 우아하게 처리하십시오
  • API 라우트를 집중적으로 유지하십시오 — 엔드포인트당 하나의 책임
  • 타입 안전성을 위해 TypeScript를 사용하십시오
  • 디버깅을 위해 서버 측에서 오류를 기록하십시오

피드백 제출

이 스킬에서 오류, 오해의 소지가 있거나 오래된 정보를 발견하면 신고하여 Expo가 개선할 수 있도록 하십시오:

npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-hosting" "<실행 가능한 피드백>"

구체적이고 실행 가능한 내용이 있을 때만 제출하십시오. 가능한 많은 관련 컨텍스트를 포함하십시오. 에이전트가 Expo 작업에서 반복적으로 실패하거나 사용자가 개입해야 하는 경우, --category evals와 관련 Expo 패키지 또는 명령어를 --subject로 사용하여 작업, 예상 결과 및 문제점에 대한 간략한 보고서를 제출하십시오.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

expo/add-app-clip

Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app.

expo/building-native-ui

Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.

expo/eas-update-insights

Check the health of published EAS Updates: crash rates, install/launch counts, unique users, payload size, and the split between embedded and OTA users per channel. Use when the user asks how an update is performing, whether a rollout is healthy, how many users are on the embedded build vs OTA, or wants to gate CI on update health.

expo/expo-api-routes

Guidelines for creating API routes in Expo Router with EAS Hosting

expo/expo-brownfield

Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR/XCFramework, or adding Expo to an existing Kotlin/Swift project. Covers both the isolated approach and the integrated approach.

expo/expo-cicd-workflows

Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation.

expo/expo-deployment

Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes

expo/expo-dev-client

Build and distribute Expo development clients locally or via TestFlight

expo/expo-examples

Framework (OSS). Expo's official example projects - the expo/examples repo of ~70 `with-*` integrations (Stripe, Clerk, Supabase, OpenAI, maps, Reanimated, SQLite, Skia, NativeWind, and more). Use when integrating a third-party library or service into an existing Expo app and you want the canonical, version-matched pattern to adapt, or when scaffolding a new project from one with `npx create-expo --example`.

expo/expo-module

Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo.

expo/expo-observe

Use for anything related to EAS Observe — adding `expo-observe` to an Expo project (AppMetricsRoot/ObserveRoot HOC, markInteractive, the useObserve hook, and the Expo Router / React Navigation integrations for per-route metrics), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold/warm launch, TTR, TTI, navigation cold/warm TTR, update download, and the TTI frameRate params for triaging slow startups).

expo/expo-overview

Framework (OSS). Entry point and router for every Expo or EAS task. Load this skill first — before writing code and before choosing another expo-* / eas-* skill — when the request, PRD, or spec mentions Expo, EAS, Expo Go, or an expo-* package, or the project has an `expo` dependency in `package.json`. Within that gate it also covers app specs and designs to implement (tabs, stacks, maps, lists, navigation, building from a screenshot), and phrasings like 'implement a mobile app', 'make my app look native', 'add navigation', 'fetch some data', 'upgrade my SDK', 'add Expo to my existing native app', 'ship to the App Store', or 'I'm new to Expo, where do I start'. A fully specified request (SDK pinned, libraries named, layout given) still routes through here — the shared setup rules still apply. Do NOT load it when neither signal is present: a bare React Native project with no `expo` dependency is not Expo work. Detects the real goal, routes to the right expo-* / eas-* skill, and owns the shared setup rules.

expo/expo-router

Framework (OSS). Navigation and routing for Expo Router. Covers file-based routes, groups and dynamic routes, folder organization, Link with previews and context menus, native Stack, page titles, modals and form sheets, NativeTabs, headers and toolbars, and header search bars.

expo/expo-skill-eval

Evaluate Expo skills in this repo end-to-end - trigger accuracy, generated code quality, and runtime screenshots on iOS simulator and Android emulator via Expo Go (web optional). Use when the user wants to eval an Expo skill, test that a skill produces working code, benchmark a skill with device screenshots, or verify a skill's output renders correctly.

expo/expo-tailwind-setup

Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling

expo/expo-ui

Framework (OSS). Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching.

expo/expo-ui-jetpack-compose

`@expo/ui/jetpack-compose` package lets you use Jetpack Compose Views and modifiers in your app.

expo/expo-ui-swiftui

`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.

expo/expo-upgrade

Framework (OSS). Guidelines for upgrading Expo SDK versions and fixing dependency issues

expo/native-data-fetching

Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).

관련 스킬