본문으로 건너뛰기

빠른 시작: React Native

React Native 또는 Expo 앱에 제공업체 SDK나 API 키를 네이티브 번들에 포함하지 않고 스트리밍 AI 채팅을 추가하려고 합니다. 이 가이드를 마치면 앱이 @tanstack/ai-reactuseChat으로 서버가 소유한 Hono 라우트를 호출하고, 모바일 호환 전송으로 응답을 스트리밍하며, OPENAI_API_KEY / OPENAI_MODEL을 서버에 유지하게 됩니다.

웹 빠른 시작에서 넘어왔나요? 훅은 같지만 URL과 전송 방식은 다릅니다. React Native에는 /api/chat이 아닌 절대 백엔드 URL이 필요하며, 대부분의 Expo 런타임에서는 xhrHttpStream()으로 시작해야 합니다.

1. 패키지 설치

처음부터 시작한다면 먼저 Expo 앱을 만듭니다.

npx create-expo-app@latest my-ai-chat

TanStack AI, React 훅 패키지, 서버용 OpenAI 어댑터, 예제 백엔드용 Hono를 설치합니다.

pnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openai hono @hono/node-server zod

Expo 앱이 워크스페이스에 있다면 앱 패키지에서 명령을 실행하거나 워크스페이스 필터를 사용합니다.

2. 서버에 OpenAI 유지

모델, API 키, 응답 형식을 관리하는 Hono 라우트를 만듭니다. 네이티브 앱은 이 라우트로 채팅 메시지를 보내며, @tanstack/ai-openai를 가져오지도 않고 OPENAI_API_KEY를 받지도 않습니다.

// server.ts
import { serve } from '@hono/node-server'
import { chat, toHttpResponse, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { Hono } from 'hono'
import { model } from './config'

const app = new Hono()

function requireOpenAIKey() {
if (!process.env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY is not configured on the server')
}
}

app.get('/health', (c) => c.json({ ok: true }))

app.post('/chat/http', async (c) => {
requireOpenAIKey()
const body = await c.req.json()
const stream = chat({
adapter: openaiText(model),
messages: body.messages,
})

return toHttpResponse(stream, {
headers: {
'Content-Type': 'application/x-ndjson',
'Cache-Control': 'no-cache',
},
})
})

app.post('/chat/sse', async (c) => {
requireOpenAIKey()
const body = await c.req.json()
const stream = chat({
adapter: openaiText(model),
messages: body.messages,
})

return toServerSentEventsResponse(stream)
})

serve({
fetch: app.fetch,
hostname: '0.0.0.0',
port: Number(process.env.PORT ?? 8787),
})

Hono 프로세스가 실행되는 곳에 서버 전용 환경 변수를 설정합니다.

OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5.2

네이티브 앱을 시작하기 전에 Hono 서버를 실행합니다. TypeScript 전용 예제라면 tsx를 설치하고 스크립트를 추가합니다.

pnpm add -D tsx
pnpm pkg set scripts.dev:server="tsx server.ts"
pnpm dev:server

라우트 조합이 중요합니다: xhrHttpStream()fetchHttpStream()toHttpResponse()의 줄바꿈으로 구분된 JSON 응답을 기대합니다. xhrServerSentEvents()toServerSentEventsResponse()text/event-stream 응답을 기대합니다.

3. 네이티브에서 접근 가능한 URL 구성

React Native은 백엔드 origin에서 제공되지 않으므로 /api/chat을 기본값으로 사용할 수 없습니다. 공개 변수로 백엔드 URL을 Expo에 노출합니다.

EXPO_PUBLIC_TANSTACK_AI_BASE_URL=http://192.168.1.10:8787

기기에서 접근할 수 있는 주소를 사용합니다.

  • iOS 시뮬레이터: http://127.0.0.1:8787이 자주 작동합니다.
  • Android 에뮬레이터: http://10.0.2.2:8787을 사용합니다.
  • 실제 기기: 컴퓨터의 LAN IP(예: http://192.168.1.10:8787) 또는 터널링된 HTTPS URL을 사용합니다.

EXPO_PUBLIC_* 값만 앱에 번들됩니다. 제공업체 키는 OPENAI_API_KEY와 같은 일반 서버 변수로 유지합니다.

4. 네이티브 화면에서 useChat 사용

Expo와 React Native에서는 xhrHttpStream()으로 시작합니다. 이는 toHttpResponse()가 생성한 동일한 줄바꿈 구분 JSON을 읽고 XHR 진행 이벤트를 사용하며, 일반적으로 휴대폰 런타임에서 스트리밍 fetch보다 안정적입니다.

// ChatScreen.tsx
import { useState } from 'react'
import { Button, ScrollView, Text, TextInput, View } from 'react-native'
import { useChat, xhrHttpStream } from '@tanstack/ai-react'

const baseUrl =
process.env.EXPO_PUBLIC_TANSTACK_AI_BASE_URL ?? 'http://127.0.0.1:8787'

export function ChatScreen() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading, error } = useChat({
connection: xhrHttpStream(`${baseUrl}/chat/http`),
})

async function send() {
const text = input.trim()
if (!text || isLoading) return
setInput('')
await sendMessage(text)
}

return (
<View style={{ flex: 1, padding: 24, gap: 12 }}>
<ScrollView style={{ flex: 1 }}>
{messages.map((message) => (
<View key={message.id} style={{ marginBottom: 16 }}>
<Text style={{ fontWeight: '700' }}>{message.role}</Text>
{message.parts.map((part, index) =>
part.type === 'text' ? (
<Text key={index}>{part.content}</Text>
) : null,
)}
</View>
))}
</ScrollView>

{error ? <Text style={{ color: 'crimson' }}>{error.message}</Text> : null}

<TextInput
value={input}
onChangeText={setInput}
editable={!isLoading}
placeholder="Ask for a recipe..."
style={{ borderWidth: 1, borderRadius: 8, padding: 12 }}
/>
<Button title={isLoading ? 'Streaming...' : 'Send'} onPress={send} />
</View>
)
}

이제 서버 엔드포인트를 호출하고 어시스턴트 텍스트를 스트리밍하며 제공업체 자격 증명을 앱 외부에 유지하는 네이티브 채팅 화면이 완성되었습니다.

5. 전송 방식 선택

서버 라우트와 런타임에 맞는 전송 방식을 사용합니다.

네이티브 런타임클라이언트 어댑터서버 응답
대부분의 Expo / React Native 앱xhrHttpStream(url)/chat/httptoHttpResponse(stream)
SSE 호환 네이티브 런타임 또는 프록시 경로xhrServerSentEvents(url)/chat/ssetoServerSentEventsResponse(stream)
스트리밍 fetch을 지원하는 런타임fetchHttpStream(url)/chat/httptoHttpResponse(stream)

정확한 런타임이 다음을 모두 지원하는 경우에만 fetchHttpStream()을 사용합니다.

  • Response.body
  • Response.body.getReader()
  • TextDecoder

하나라도 없으면 어댑터가 UnsupportedResponseStreamError를 발생시킵니다. 전체 응답을 버퍼링하는 polyfilled fetch만으로는 충분하지 않습니다. 모델이 스트리밍하는 동안 채팅을 업데이트하려면 TanStack AI에 응답 바이트가 점진적으로 필요합니다.

헤더, 자격 증명, withCredentials, 동적 URL과 같은 어댑터 옵션은 Connection Adapters를 참고합니다.

6. Expo 레시피 예제 사용

React Native 지원을 평가한다면 포함된 Expo 앱을 사용합니다. 로컬 Hono/OpenAI 서버를 실행하고 전송 선택기를 표시하며 구조화된 레시피 카드를 스트리밍하므로 네이티브 채팅과 구조화된 출력 동작을 함께 확인할 수 있습니다.

Create examples/ts-react-native-chat/.env:

OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5.2

예제를 실행합니다.

pnpm --filter ts-react-native-chat dev

이 명령은 다음을 시작합니다.

  • 0.0.0.0:8787 에서 Hono
  • LAN 모드인 Expo/Metro
  • EXPO_PUBLIC_TANSTACK_AI_BASE_URL=http://<lan-ip>:8787 는 LAN 주소를 감지할 때

같은 Wi-Fi 네트워크에 연결된 휴대폰에서 Expo Go QR 코드를 스캔합니다. 앱의 Testing mode 패널에서 Fetch HTTP, XHR HTTP, XHR SSE 사이를 전환합니다. 기본 레시피 카드는 후속 프롬프트에 걸쳐 title, ingredients, steps, tips, warnings, revision과 같은 구조화된 필드를 스트리밍합니다.

예제 전용 명령과 네트워크 재정의는 examples/ts-react-native-chat/README.md.

문제 해결

http://localhost:8081에 JSON이 표시됨

정상적인 동작입니다. 포트 8081은 웹 UI가 아니라 Metro의 매니페스트 및 번들 서버입니다. 대신 Expo Go, Android 에뮬레이터 또는 iOS 시뮬레이터에서 앱을 실행합니다.

실제 기기에서 백엔드에 접근할 수 없음

휴대폰 브라우저에서 http://<lan-ip>:8787/health를 엽니다. {"ok":true}를 반환하지 않으면 휴대폰과 컴퓨터가 같은 Wi-Fi 네트워크에 있는지, 클라이언트 격리가 비활성화되어 있는지, 방화벽이 Hono 포트와 Metro 포트 8081에서 Node.js를 허용하는지 확인합니다.

Android 에뮬레이터에서 127.0.0.1에 접근할 수 없음

EXPO_PUBLIC_TANSTACK_AI_BASE_URLhttp://10.0.2.2:8787을 사용합니다. Android 에뮬레이터에서 10.0.2.2는 호스트 머신으로 매핑됩니다.

Expo에서 Android SDK 또는 adb 경고가 표시됨

이는 TanStack AI 전송 문제가 아니라 Android 도구 문제입니다. Android Studio가 SDK를 설치했는지, Device Manager에 에뮬레이터가 있는지, adbPATH에 있는지 확인합니다. Windows에서는 %LOCALAPPDATA%\Android\Sdk\platform-tools\adb.exe.

휴대폰에 UnsupportedResponseStreamError가 기록됨

런타임에서 스트리밍 fetch, Response.body.getReader() 또는 TextDecoder를 노출하지 않습니다. fetchHttpStream()에서 xhrHttpStream() 또는 xhrServerSentEvents()로 전환합니다. 실제 점진적 읽기 가능 스트림을 제공하지 않는 fetch polyfill에 의존하지 마세요.

XHR에서 서버 오류를 보고함

먼저 Hono 서버 터미널을 확인합니다. 일반적인 원인은 OPENAI_API_KEY 누락, 지원되지 않는 OPENAI_MODEL, 또는 xhrServerSentEvents()/chat/sse 대신 /chat/http를 가리키는 경우(또는 그 반대)입니다.

이제 서버가 관리하는 제공업체 경계, 네이티브에서 접근 가능한 URL, 모바일 호환 전송, 실제 기기에서 설정을 검증하는 Expo 예제로 구성된 전체 React Native 경로를 갖추었습니다.