지금까지 만든 AI Agent는 요청이 끝나면 데이터가 사라집니다. 사용자 파일, STT 원본 오디오, 프로젝트 메타데이터를 영속적으로 저장하려면 Vercel Blob이 필요합니다. 이 모듈에서는 Blob을 KV처럼 추상화하는 wrapper를 직접 만들고, 대용량 파일을 서버를 우회해 안전하게 올리는 방법까지 익힙니다.
@vercel/blob 설치 + Storage Dashboard
pnpm add @vercel/blob
Vercel Dashboard → Storage → Create Database → Blob을 생성하면 BLOB_READ_WRITE_TOKEN 환경변수가 자동으로 프로젝트에 연결됩니다. .env.local에도 동일한 값을 복사해 넣어야 로컬에서 동작합니다.
팁:
vercel env pull .env.local한 줄로 모든 환경변수를 로컬에 내려받을 수 있습니다. 매번 복사할 필요가 없습니다.
Blob은 S3 호환 객체 스토리지입니다. 키는 경로처럼 생긴 문자열(projects/abc/meta.json)이고 값은 바이너리 또는 텍스트입니다. KV 데이터베이스가 아니기 때문에, JSON을 직렬화하고 역직렬화하는 얇은 wrapper가 필요합니다.
KV Wrapper 구현
아래 네 함수(kvPutText, kvGetJson, kvList, kvDelete)가 이 강의 전체에서 사용하는 표준 인터페이스입니다.
// lib/blob-kv.ts
import { put, get, list, del } from "@vercel/blob";
const TOKEN = process.env.BLOB_READ_WRITE_TOKEN!;
/** JSON 객체를 Blob에 저장 */
export async function kvPutText(key: string, value: unknown): Promise<string> {
const body = JSON.stringify(value);
const { url } = await put(key, body, {
access: "public",
token: TOKEN,
contentType: "application/json",
addRandomSuffix: false, // 키를 덮어써야 하므로 반드시 false
});
return url;
}
/** Blob에서 JSON 역직렬화 */
export async function kvGetJson<T = unknown>(key: string): Promise<T | null> {
try {
const res = await get(key, { token: TOKEN });
if (!res) return null;
const text = await res.text();
return JSON.parse(text) as T;
} catch {
return null;
}
}
/** prefix로 시작하는 키 목록 */
export async function kvList(prefix: string) {
const { blobs } = await list({ prefix, token: TOKEN });
return blobs;
}
/** 키 삭제 */
export async function kvDelete(url: string) {
await del(url, { token: TOKEN });
}
주의:
addRandomSuffix: false를 빠뜨리면 같은 키에 업데이트할 때마다 새 URL이 생겨 데이터가 중복 적재됩니다. 반드시 명시하세요.
Prefix 격리 전략
멀티테넌트 서비스에서는 데이터를 테넌트별로 격리해야 합니다. 이 강의는 경로 기반 prefix로 격리합니다.
| 용도 | Blob 키 예시 |
|---|---|
| 프로젝트 메타데이터 | projects/<projectId>/meta.json |
| STT 원본 오디오 | projects/<projectId>/audio/<uploadId>.webm |
| RAG 청크 인덱스 | projects/<projectId>/rag/chunks.json |
| 보고서 | projects/<projectId>/reports/<date>.md |
kvList("projects/abc123/") 한 번으로 특정 프로젝트의 모든 파일을 열거할 수 있습니다. 서로 다른 프로젝트의 데이터는 prefix가 달라 자연스럽게 분리됩니다.
Direct Client Upload (1 GB 서버 우회)
Next.js API Route는 기본 요청 바디 한도가 4 MB입니다. 대용량 오디오나 PDF를 올릴 때 서버를 경유하면 이 한도에 걸립니다. Vercel Blob의 client upload 패턴은 서버가 토큰만 발급하고 클라이언트가 Blob에 직접 업로드합니다.
// app/api/upload-token/route.ts ← 토큰 발급 엔드포인트
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = (await request.json()) as HandleUploadBody;
try {
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => ({
allowedContentTypes: ["audio/webm", "audio/mp4", "application/pdf"],
tokenPayload: JSON.stringify({ pathname }),
maximumSizeInBytes: 1_000_000_000, // 1 GB
}),
onUploadCompleted: async ({ blob, tokenPayload }) => {
// 업로드 완료 후 메타데이터를 KV에 기록
const { pathname } = JSON.parse(tokenPayload ?? "{}");
const metaKey = pathname.replace(/\.[^.]+$/, "_meta.json");
await kvPutText(metaKey, { url: blob.url, uploadedAt: new Date().toISOString() });
},
});
return NextResponse.json(jsonResponse);
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
}
}
클라이언트에서는 @vercel/blob/client의 upload() 함수를 호출합니다. 파일이 서버 메모리를 전혀 거치지 않고 Blob에 직접 전송됩니다.
자동 삭제: STT 완료 후 원본 오디오 제거
원본 오디오는 STT 처리 직후 삭제하는 것이 좋은 관행입니다. 비용 절감과 개인정보 최소화를 동시에 달성합니다.
// STT 처리 후 호출
async function transcribeAndClean(audioUrl: string): Promise<string> {
// ElevenLabs Scribe v2 STT 호출 (강의 M8 참고)
const transcript = await callScribeV2(audioUrl);
// 원본 오디오 즉시 삭제
await kvDelete(audioUrl);
return transcript;
}
kvDelete는 Blob URL을 직접 받으므로, 업로드 완료 콜백에서 받은 blob.url을 그대로 넘기면 됩니다.
실습 (Lab)
목표: PDF 또는 오디오 파일을 direct upload로 올리고, 메타데이터를 KV에 영구 저장한 뒤 목록을 조회합니다.
단계별 안내
-
환경 준비
vercel env pull .env.local로BLOB_READ_WRITE_TOKEN확인pnpm add @vercel/blob설치
-
KV wrapper 작성
lib/blob-kv.ts에 위의kvPutText,kvGetJson,kvList,kvDelete복사
-
업로드 토큰 API 작성
app/api/upload-token/route.ts생성onUploadCompleted에서projects/<projectId>/audio/<filename>_meta.json키로 메타데이터 저장
-
클라이언트 업로드 UI 작성
<input type="file" accept="audio/*,application/pdf" />@vercel/blob/client의upload(pathname, file, { handleUploadUrl: "/api/upload-token" })호출
-
목록 조회 API 작성
GET /api/files?projectId=xxx→kvList("projects/xxx/")결과 반환
-
자동 삭제 검증
- 업로드된 오디오 URL로
transcribeAndClean()호출 후 Vercel Dashboard에서 파일이 사라졌는지 확인
- 업로드된 오디오 URL로
체크포인트: 업로드 완료 후
kvList가 메타데이터 JSON을 반환하면 성공입니다. Blob 원본 오디오는 삭제되고 메타데이터만 남아 있어야 합니다.
핵심 정리
@vercel/blob은 S3 호환 객체 스토리지이며, JSON 직렬화 wrapper(kvPutText/kvGetJson/kvList/kvDelete)로 KV처럼 사용한다.addRandomSuffix: false옵션을 반드시 설정해야 같은 키 덮어쓰기(upsert)가 정상 동작한다.- Prefix 경로(
projects/<id>/...)로 테넌트 또는 프로젝트 단위의 데이터 격리를 구현한다. - Direct client upload는 서버를 우회하므로 4 MB 한도 없이 최대 1 GB까지 업로드 가능하다.
- STT 완료 직후 원본 오디오를
kvDelete로 삭제하면 비용 절감과 개인정보 최소화를 동시에 달성한다. - 모든 Blob 작업은
BLOB_READ_WRITE_TOKEN하나로 인증되며, 이 토큰은 절대 클라이언트 코드에 노출해선 안 된다.
파일 업로드 + 메타데이터 영구 저장