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로 사용하여 작업, 예상 결과 및 문제점에 대한 간략한 보고서를 제출하십시오.