HANS17 AcademyAI · AGENT · LAB
Hans17Lecture NoteRAG LabFAQReference
강사 Agent
📄 M9 · Vercel Blob 영속 저장소 — PDF인쇄 대화상자: 대상=PDF로 저장 · 용지=가로 · 색이 안 나오면 배경 그래픽 켜기 → 다크 16:9 슬라이드
HANS17 ACADEMY · AI Agent 실습 강의

M9 · Vercel Blob 영속 저장소

DB 없이 KV 패턴으로 버틴다

hans17.com · 강의 노트 (16:9 슬라이드)

M9실습 · 45분

Vercel Blob 영속 저장소

DB 없이 KV 패턴으로 버틴다

🎯 Blob 을 KV 처럼 쓰는 wrapper 와 prefix 격리, 대용량 direct upload 를 익힌다.

지금까지 만든 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에 영구 저장한 뒤 목록을 조회합니다.

단계별 안내

  1. 환경 준비

    • vercel env pull .env.local로 BLOB_READ_WRITE_TOKEN 확인
    • pnpm add @vercel/blob 설치
  2. KV wrapper 작성

    • lib/blob-kv.ts에 위의 kvPutText, kvGetJson, kvList, kvDelete 복사
  3. 업로드 토큰 API 작성

    • app/api/upload-token/route.ts 생성
    • onUploadCompleted에서 projects/<projectId>/audio/<filename>_meta.json 키로 메타데이터 저장
  4. 클라이언트 업로드 UI 작성

    • <input type="file" accept="audio/*,application/pdf" />
    • @vercel/blob/client의 upload(pathname, file, { handleUploadUrl: "/api/upload-token" }) 호출
  5. 목록 조회 API 작성

    • GET /api/files?projectId=xxx → kvList("projects/xxx/") 결과 반환
  6. 자동 삭제 검증

    • 업로드된 오디오 URL로 transcribeAndClean() 호출 후 Vercel Dashboard에서 파일이 사라졌는지 확인

체크포인트: 업로드 완료 후 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 하나로 인증되며, 이 토큰은 절대 클라이언트 코드에 노출해선 안 된다.

🤖 Claude Code 프롬프트 (이렇게 시키면 됩니다)

이 모듈은 손코딩이 아니라 Claude Code에 프롬프트로 지시해서 만듭니다.

"@vercel/blob로 KV처럼 쓰는 wrapper(kvPutText·kvGetJson·kvList·kvDelete)를 만들어줘. projects/<id>/ prefix로 격리." "1GB 파일도 되게 클라이언트 direct upload로 해줘."

팁 · "STT 끝나면 원본 음성 자동 삭제"처럼 후처리도 한 문장으로 지시.

🧪 실습(Lab)

파일 업로드 + 메타데이터 영구 저장

Hans17ACADEMY

주중엔 엔지니어, 주말엔 아티스트 — 논리와 감성, 과학과 사운드의 경계를 넘나든다.

COURSE
  • 강사소개
  • Lecture Note
  • Reference
MUSIC
  • TikTok
  • YouTube
  • Apple Music
  • Instagram
  • Facebook
  • SoundCloud
© 2026 Hans17. Weekend Artist · Engineer · DJ.Why 17? — keep asking.
AgentAgent