



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









結論から言うと、Vercel AI SDKを使えばChatGPTのようなストリーミングUIを数十行のコードで実装できます。useChat HookにSSEのパース・状態管理・エラーハンドリングがすべて抽象化されており、OpenAI・Anthropic・Amazon Bedrockなど複数プロバイダーにも対応しています。Claude Sonnet 4のような高性能モデルで10秒以上かかる応答も、ストリーミング実装で「最初の文字が0.3〜1秒で表示される」体験に変わります。
Vercel AI SDKは、LLMを活用したWebアプリケーションを簡単に構築するためのオープンソースライブラリです。Server-Sent Events(SSE)を使ったストリーミング処理・React Hooksによる状態管理・複数AIプロバイダーへの対応を提供します。Vercel社が2023年に公開し、2024〜2026年にかけて急速に普及し、Next.js公式ドキュメントでも推奨されています。
従来はストリーミングUIの実装に「SSEのレスポンスを手動でパース」「テキストの増分をstateで管理」「エラー時のリトライロジックを自前で実装」など多くのボイラープレートコードが必要でした。Vercel AI SDKはこれらをすべて内部に抽象化し、useChatという一つのHookで完全なチャットUI機能を提供します。
>【Vercel AI SDKの3つの特徴】
✅ 簡単なストリーミング実装
→ streamText() + toDataStreamResponse() の2行でストリーミング対応完了
✅ 抽象化されたフロントエンド
→ useChat() Hook で送信・受信・履歴管理をすべて管理
→ 自前で状態管理を書く必要がほぼない
✅ マルチプロバイダー対応
→ OpenAI, Anthropic, Google AI, Amazon Bedrock, Mistral etc.
→ プロバイダーを変更してもUIコードはそのまま| 観点 | ノンストリーミング | ストリーミング |
|---|---|---|
| 体感速度 | 全テキスト生成まで待機(10〜30秒) | 生成開始と同時に表示(0.3〜1秒で最初の文字) |
| ユーザー体験 | 「固まったのか?」という不安感 | AIが考えていることがリアルタイムでわかる |
| 実装複雑度 | シンプル(fetch→JSON解析) | Vercel AI SDK使用でシンプルに実現 |
| サーバーリソース | 応答完了までメモリに保持が必要 | チャンクを順次送信してメモリ効率が高い |
| エラーハンドリング | 一度にエラーが判明 | ストリーム途中でエラーになる場合がある |
Vercel AI SDKのストリーミングはHTTPの「Server-Sent Events(SSE)」を使っています。SSEはHTTP接続を維持したままサーバーからクライアントへ一方向にデータをプッシュできるWebAPIです。WebSocketと比べてシンプルで、HTTPの標準機能のみで動作するためCDN・プロキシ・ファイアウォール環境でも安定しています。
>【LLMがストリーミングで送るデータの実際の形式】
f:{"messageId":"msg-OuigLUo9Lxefa0EudVrcsnaz"}
0:"Vercel AI"
0:" SDKは、"
0:"AI機能を"
0:"Webアプリ"
0:"ケーションに"
0:"簡単に統"
0:"合するための"
0:"オープンソース"
0:"ライブラリです"
...
e:{"finishReason":"stop","usage":{"promptTokens":468,"completionTokens":570}}
d:{"finishReason":"stop"}
→ テキストが生成されるたびに小さなチャンクがリアルタイムで送信される
→ ユーザーは生成の過程を文字単位でリアルタイムに確認できる
→ HTTP接続を維持しながらサーバーからクライアントへ一方向に送信(SSE)
→ 先頭の数字はデータの種類を示すプレフィックス(0=テキスト、f=メタデータ、e=完了情報)# Next.js プロジェクトを作成
npx create-next-app@latest ai-streaming-app --typescript --tailwind --app
# Vercel AI SDK と使用するプロバイダーのパッケージをインストール
# OpenAI を使う場合
pnpm add ai @ai-sdk/openai
# Amazon Bedrock を使う場合
pnpm add ai @ai-sdk/amazon-bedrock @aws-sdk/credential-providers
# Anthropic を使う場合
pnpm add ai @ai-sdk/anthropic# .env.local に APIキーを設定(Gitに含めないこと)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxx
# Amazon Bedrock を使う場合
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_REGION=ap-northeast-1>// src/app/api/chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
// ストリーミング応答を最大30秒まで許可する(Vercel Edge Functionのタイムアウト)
export const maxDuration = 30
export async function POST(req: Request) {
const { messages } = await req.json()
// ストリーミングでテキストを生成
const result = streamText({
model: openai('gpt-4o-mini'),
system: 'あなたは親切なAIアシスタントです。日本語で回答してください。',
messages,
})
// データストリームレスポンスとして返却
// → Server-Sent Events 形式でクライアントに送信される
return result.toDataStreamResponse()
}>// src/app/page.tsx
'use client'
import { useChat } from '@ai-sdk/react'
export default function Chat() {
// useChat Hook にストリーミング処理が完全に抽象化されている
const {
messages, // チャット履歴({ id, role, content, parts }[])
input, // テキスト入力の現在値
handleInputChange, // input の onChange ハンドラ
handleSubmit, // フォームの onSubmit ハンドラ
isLoading, // 応答待ち中は true
stop, // ストリーミングを中断する関数
reload, // 最後のメッセージを再生成する関数
error, // エラーがあれば Error オブジェクト
} = useChat({
api: '/api/chat', // デフォルト値なので省略可
onFinish: (message) => {
console.log('生成完了:', message.content.length, '文字')
},
onError: (error) => {
console.error('エラー発生:', error.message)
}
})
return (
<div className="flex flex-col w-full max-w-2xl mx-auto h-screen py-8">
{/* チャット履歴エリア */}
<div className="flex-1 overflow-y-auto space-y-4 mb-4 px-4">
{messages.length === 0 && (
<p className="text-center text-gray-400 mt-20">
質問を入力してください
</p>
)}
{messages.map(message => (
<div
key={message.id}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[80%] rounded-2xl px-4 py-3 ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-900'
}`}>
{/* テキストパーツを表示(ストリーミング中も随時更新される) */}
{message.parts?.map((part, i) => {
if (part.type === 'text') {
return (
<p key={i} className="whitespace-pre-wrap text-sm">
{part.text}
</p>
)
}
return null
})}
</div>
</div>
))}
{/* ローディングインジケーター */}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-2xl px-4 py-3">
<span className="inline-block animate-pulse text-gray-500">▍</span>
</div>
</div>
)}
</div>
{/* エラー表示 */}
{error && (
<div className="mx-4 mb-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{error.message}</p>
<button
onClick={reload}
className="mt-2 text-xs text-red-500 underline"
>
再試行する
</button>
</div>
)}
{/* 入力フォーム */}
<form onSubmit={handleSubmit} className="flex gap-2 px-4">
<input
value={input}
onChange={handleInputChange}
placeholder="メッセージを入力..."
disabled={isLoading}
className="flex-1 border border-gray-300 rounded-xl px-4 py-3 text-sm
focus:outline-none focus:ring-2 focus:ring-blue-500
disabled:opacity-50"
/>
{isLoading ? (
<button
type="button"
onClick={stop}
className="bg-red-500 text-white rounded-xl px-5 py-3 text-sm font-medium
hover:bg-red-600"
>
停止
</button>
) : (
<button
type="submit"
disabled={!input.trim()}
className="bg-blue-500 text-white rounded-xl px-5 py-3 text-sm font-medium
hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
送信
</button>
)}
</form>
</div>
)
}Vercel AI SDKのもっとも強力な機能の一つが「ツール呼び出し」です。LLMが自律的に外部APIや関数を呼び出して情報を取得し、その結果をもとに回答を生成できます。ChatGPTのプラグイン機能に相当する仕組みです。
>// src/app/api/chat/route.ts
// ツール呼び出しを使った例:天気情報を取得して回答する
import { streamText, tool } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
export const maxDuration = 30
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
system: 'あなたはアシスタントです。天気について聞かれたら必ず getWeather ツールを使って回答してください。',
messages,
tools: {
// ツール名: tool() で定義
getWeather: tool({
description: '指定した都市の現在の天気情報を取得する',
parameters: z.object({
city: z.string().describe('都市名(例: Tokyo, Osaka)'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
// execute 関数で実際の処理を実装(非同期OK)
execute: async ({ city, unit }) => {
// 実際はAPIを叩く(ここではモックデータ)
const mockData = {
tokyo: { temp: 18, condition: '晴れ', humidity: 55 },
osaka: { temp: 20, condition: '曇り', humidity: 60 },
}
const data = mockData[city.toLowerCase()] ?? { temp: 15, condition: '不明', humidity: 50 }
return {
city,
temperature: unit === 'celsius' ? data.temp : data.temp * 9/5 + 32,
unit,
condition: data.condition,
humidity: data.humidity,
}
},
}),
// 複数のツールを定義できる
searchWeb: tool({
description: 'Webを検索して最新情報を取得する',
parameters: z.object({
query: z.string().describe('検索クエリ'),
}),
execute: async ({ query }) => {
// 実際はBing Search API等を呼び出す
return { results: `「${query}」の検索結果...` }
},
}),
},
maxSteps: 5, // ツール呼び出しの最大ステップ数(連鎖的な呼び出しに対応)
})
return result.toDataStreamResponse()
}>// src/app/page.tsx
// ツール呼び出しの結果を表示する
'use client'
import { useChat } from '@ai-sdk/react'
export default function ChatWithTools() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div className="max-w-2xl mx-auto p-4">
<div className="space-y-4 mb-4">
{messages.map(message => (
<div key={message.id} className="space-y-2">
<div className={`font-semibold text-sm ${
message.role === 'user' ? 'text-blue-600' : 'text-gray-600'
}`}>
{message.role === 'user' ? 'あなた' : 'AI'}
</div>
{/* メッセージのパーツを種類ごとに表示 */}
{message.parts?.map((part, i) => {
switch (part.type) {
case 'text':
return <p key={i} className="text-gray-800">{part.text}</p>
case 'tool-invocation':
// ツール呼び出し中 / 完了を表示
return (
<div key={i} className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-xs font-medium text-blue-700 mb-1">
🔧 ツール: {part.toolInvocation.toolName}
</div>
{part.toolInvocation.state === 'call' && (
<p className="text-xs text-blue-500">実行中...</p>
)}
{part.toolInvocation.state === 'result' && (
<pre className="text-xs text-blue-800 overflow-auto">
{JSON.stringify(part.toolInvocation.result, null, 2)}
</pre>
)}
</div>
)
default:
return null
}
})}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="「東京の天気は?」など試してみてください"
className="flex-1 border rounded-lg px-4 py-2"
/>
<button type="submit" disabled={isLoading} className="bg-blue-500 text-white px-4 py-2 rounded-lg">
送信
</button>
</form>
</div>
)
}GPT-4o・Claude 3.5 Sonnet・Google Geminiなどのマルチモーダルモデルに画像を渡して分析させることも、Vercel AI SDKを使えば簡単に実装できます。
>// src/app/api/vision/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export const maxDuration = 30
export async function POST(req: Request) {
const formData = await req.formData()
const image = formData.get('image') as File
const question = formData.get('question') as string
// File を ArrayBuffer に変換
const imageBytes = await image.arrayBuffer()
const imageBase64 = Buffer.from(imageBytes).toString('base64')
const result = streamText({
model: openai('gpt-4o'), // Vision対応モデルを使用
messages: [
{
role: 'user',
content: [
{
type: 'image',
image: imageBase64, // Base64エンコードされた画像
mimeType: image.type as 'image/jpeg' | 'image/png' | 'image/webp',
},
{
type: 'text',
text: question || 'この画像について詳しく説明してください。',
},
],
},
],
})
return result.toDataStreamResponse()
}>// src/app/vision/page.tsx
'use client'
import { useState, useRef } from 'react'
import { useCompletion } from '@ai-sdk/react'
export default function VisionPage() {
const [imagePreview, setImagePreview] = useState<string | null>(null)
const [question, setQuestion] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
const { completion, isLoading, complete } = useCompletion({
api: '/api/vision',
})
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => setImagePreview(reader.result as string)
reader.readAsDataURL(file)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const file = fileInputRef.current?.files?.[0]
if (!file) return
const formData = new FormData()
formData.append('image', file)
formData.append('question', question)
await complete('', {
body: formData,
headers: {}, // Content-TypeはブラウザがFormDataを自動設定
})
}
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">画像分析AI</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="border-2 border-dashed border-gray-300 rounded-xl p-6 text-center">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageChange}
className="hidden"
id="image-upload"
/>
<label htmlFor="image-upload" className="cursor-pointer">
{imagePreview ? (
<img src={imagePreview} alt="アップロード画像" className="max-h-48 mx-auto rounded-lg" />
) : (
<div className="text-gray-400">
<p className="text-4xl mb-2">📷</p>
<p>クリックして画像を選択</p>
</div>
)}
</label>
</div>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="画像について質問してください(例:この料理のカロリーは?)"
className="w-full border rounded-xl px-4 py-3"
/>
<button
type="submit"
disabled={!imagePreview || isLoading}
className="w-full bg-blue-500 text-white rounded-xl py-3 font-medium disabled:opacity-50"
>
{isLoading ? '分析中...' : '画像を分析する'}
</button>
</form>
{completion && (
<div className="mt-6 p-4 bg-gray-50 rounded-xl">
<h2 className="font-semibold mb-2">分析結果:</h2>
<p className="whitespace-pre-wrap text-gray-700">{completion}</p>
</div>
)}
</div>
)
}generateObjectを使うと、LLMの出力をZodスキーマで型定義したJSONとして受け取れます。「記事のメタデータ生成」「フォームの自動入力」「データ抽出」などのユースケースで特に有効です。
>// 出力結果の構造をZodで定義してJSON形式で取得できる
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const result = await generateObject({
model: openai('gpt-4o-mini'),
schema: z.object({
title: z.string().describe('記事タイトル'),
summary: z.string().describe('100文字以内の要約'),
tags: z.array(z.string()).max(5).describe('関連タグ(最大5つ)'),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']).describe('難易度'),
readingTime: z.number().describe('読了時間(分)'),
}),
prompt: '次の技術記事についてメタデータを生成してください:「TypeScriptの型推論入門」',
})
console.log(result.object)
// → { title: 'TypeScriptの型推論入門', summary: '...', tags: ['TypeScript', ...], difficulty: 'beginner', readingTime: 8 }>// LLMの処理は様々な理由で失敗することがある
// maxRetries でリトライ回数を設定できる
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
const result = await generateText({
model: openai('gpt-4o-mini'),
maxRetries: 3, // 失敗時に最大3回自動リトライ(指数バックオフ)
prompt: '日本の首都はどこですか?',
})
console.log(result.text)
// → リトライ時のログ: Retrying after 1s / 2s / 4s...
// → 最終的な成功結果を返す>// プロバイダーを変えてもUIコードはそのまま
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { anthropic } from '@ai-sdk/anthropic'
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'
// 環境変数でプロバイダーを切り替える
const getModel = () => {
switch (process.env.AI_PROVIDER) {
case 'anthropic':
return anthropic('claude-3-5-sonnet-20241022')
case 'bedrock':
const bedrock = createAmazonBedrock({ region: 'ap-northeast-1' })
return bedrock('apac.anthropic.claude-sonnet-4-20250514-v1:0')
default:
return openai('gpt-4o-mini')
}
}
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({ model: getModel(), messages })
return result.toDataStreamResponse()
}Vercel Edge Functionsを使うことで、ユーザーの地理的に最も近いサーバーでAI APIリクエストを処理できます。日本ユーザーが東京リージョンのEdgeから呼び出すことで、米国経由のリクエストと比べてレイテンシを30〜50%削減できる場合があります。
>// src/app/api/chat/route.ts
// Edge Runtimeを有効にする
export const runtime = 'edge'
export const maxDuration = 30
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
})
return result.toDataStreamResponse()
}
// ⚠️ Edge Runtimeではfetchなど一部のNode.js APIが使えないので注意
// ⚠️ データベースアクセスにはEdge対応のクライアント(Neon HTTP driver等)が必要システムプロンプトは毎回のリクエストでトークンを消費します。長すぎるシステムプロンプトはコストと遅延の両方に影響します。一般的なベストプラクティスとして、200〜500トークン(日本語で400〜1000文字)程度に収めることを推奨します。
>// システムプロンプトの最適化例
// ❌ 悪い例:長すぎて毎回トークンを無駄遣い
const badSystem = `
あなたはWithCodeというプログラミングスクールのAIアシスタントです。
WithCodeは2018年に設立され、東京を拠点に...(200行続く)
`
// ✅ 良い例:要点を絞った簡潔なプロンプト
const goodSystem = `
あなたはWithCode(プログラミングスクール)のAIアシスタントです。
主な業務:受講相談、カリキュラム説明、技術的なQ&A対応。
回答は200文字以内で簡潔に。専門用語には必ず説明を付ける。
`チャットの会話が長くなるにつれ、渡すメッセージ履歴のトークン数が増加します。コストと速度の観点から、一定件数以上の古いメッセージを切り捨てる「スライディングウィンドウ」戦略が有効です。
>// メッセージ履歴のトークン数を制限するユーティリティ
function trimMessageHistory(
messages: Message[],
maxMessages: number = 20
): Message[] {
if (messages.length <= maxMessages) return messages
// 最初のシステムメッセージは常に保持
const systemMessages = messages.filter(m => m.role === 'system')
const conversationMessages = messages.filter(m => m.role !== 'system')
// 最新N件のみ保持
const trimmed = conversationMessages.slice(-maxMessages)
return [...systemMessages, ...trimmed]
}
// APIルートで使用
export async function POST(req: Request) {
const { messages } = await req.json()
const trimmedMessages = trimMessageHistory(messages, 20)
const result = streamText({
model: openai('gpt-4o-mini'),
messages: trimmedMessages,
})
return result.toDataStreamResponse()
}# Vercel CLIでデプロイ
npm i -g vercel
vercel
# 環境変数を設定(Vercelダッシュボード or CLIで)
vercel env add OPENAI_API_KEY production
# プレビューデプロイ(develop/featureブランチ)
vercel --env OPENAI_API_KEY=sk-xxx
# 本番デプロイ
vercel --prod本番環境では悪意あるユーザーによる大量リクエスト(DoS攻撃・コスト爆発)を防ぐレート制限が必須です。Vercel KV(Redis互換)を使ったシンプルな実装例を示します。
>// src/app/api/chat/route.ts
// レート制限付きAPIルート
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { kv } from '@vercel/kv'
import { headers } from 'next/headers'
const RATE_LIMIT = 20 // 1分あたりの最大リクエスト数
const RATE_LIMIT_WINDOW = 60 // ウィンドウサイズ(秒)
export async function POST(req: Request) {
// IPアドレスでレート制限
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') ?? 'unknown'
const key = `rate_limit:${ip}`
const count = await kv.incr(key)
if (count === 1) {
await kv.expire(key, RATE_LIMIT_WINDOW)
}
if (count > RATE_LIMIT) {
return new Response(
JSON.stringify({ error: 'Too Many Requests' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
'Retry-After': String(RATE_LIMIT_WINDOW),
},
}
)
}
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
})
return result.toDataStreamResponse()
}useChatのerrorプロパティにエラーオブジェクトが格納されます。onErrorコールバックを設定することでエラー発生時のログ送信・ユーザーへの通知処理を記述できます。maxRetriesで自動リトライを設定することで一時的なネットワーク障害への耐性を高められます。reload()関数で最後のメッセージを再送信することも可能です。
できます。Vercel AI SDKはVercelプラットフォームに依存していません。ただしexport const maxDuration = 30はVercel Edge Functionの設定であり、他の環境ではサーバーのタイムアウト設定を別途調整する必要があります。AWSのApp Runner・ECS・LambdaやGCPのCloud Runでも問題なく動作します。
useChatのonFinishコールバックで会話終了時にAPIを叩いてDBに保存するのが標準的なパターンです。initialMessagesプロパティでDBから取得した過去の履歴を初期値として渡すことで会話履歴の復元も可能です。
>const { messages } = useChat({
initialMessages: await fetchMessagesFromDB(sessionId), // 過去の履歴を初期値として渡す
onFinish: async (message) => {
// 会話完了時にDBへ保存
await fetch('/api/save-message', {
method: 'POST',
body: JSON.stringify({ sessionId, message }),
})
},
})useChatはチャット形式(複数ターンのやり取り・会話履歴管理)に特化したHookです。useCompletionは単発のテキスト生成(1つのプロンプトに対して1つの補完)に特化しています。「翻訳ツール」「要約ツール」「コード生成」などシングルターンのユースケースにはuseCompletionが適しています。
できます。useChatが返すstop()関数を呼ぶことでストリーミングをいつでも中断できます。中断した場合それまでに受信したテキストはmessagesに残ります。isLoadingに応じて「送信ボタン」と「停止ボタン」を切り替えるUIパターンが一般的です。
Vercel AI SDKを使えば、LLMの応答待ち体験を根本から改善するストリーミングUIが数十行のコードで実装できます。ツール呼び出し・マルチモーダル・マルチプロバイダーなど高度な機能も標準で提供されており、Next.jsとTypeScriptの基礎があれば今日から始められます。

副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
公式サイト より
今すぐ
無料カウンセリング
を予約!