콘텐츠로 건너뛰기

Vercel 파일 업로드 설정 및 제한 | Vercel Blob, API 제한, 회피 방법

카테고리: Vercel·배포 설정

Vercel에 배포된 앱에서 파일 업로드를 구현할 때, Serverless Function의 요청 본문에는 제한이 있습니다. 기본 4.5MB의 이 제한은 종종 문제가 됩니다. 본 문서는 Vercel Function 본문 크기 제한, vercel.json에서의 설정 방법, Vercel Blob 사용 방법, 그리고 제한을 우회하여 대용량 파일을 처리하는 방법(S3으로의 직접 업로드)을 자세히 설명합니다.

Serverless Function 4.5 MB request body 10s ~ 900s timeout Edge Function 4 MB request body 25s timeout / streaming Vercel Blob 5 GB via client upload bypasses fn limit
그림: Vercel 함수 크기 제한과 Blob을 통한 우회

Vercel Function 본문 크기 제한

Vercel의 Serverless Function(API Routes, Route Handlers, Edge Functions)에는 요청 크기 제한이 있습니다. 이 제한을 초과하는 요청은 413 오류를 발생시킵니다.

Function의 종류 기본 제한 최대 설정값 비고
Serverless Function(Node.js) 4.5 MB 4.5 MB vercel.json 에서 변경 불가
Edge Function 4 MB 4 MB 변경 불가
Next.js Server Actions 1 MB 설정 가능 next.config.js에서의 변경

중요한 점은 Vercel의 Serverless Function 본문 크기 제한이 인프라 수준의 제한이며 애플리케이션 코드 설정 변경으로는 우회할 수 없다는 것입니다. 4.5MB를 초과하는 파일을 업로드하려면 아키텍처 수준의 우회 전략이 필요합니다.

vercel.json 에서의 설정

vercel.json에서는 Function의 실행 시간(maxDuration)과 메모리(memory)를 설정할 수 있지만 본문 크기 상한은 변경할 수 없습니다. 다만 Next.js의 Pages Router의 경우 responseLimit 등 일부 설정이 유효합니다.

// vercel.json
{
  "functions": {
    "app/api/upload/route.ts": {
      "maxDuration": 60,
      "memory": 1024
    },
    "pages/api/upload.ts": {
      "maxDuration": 30
    }
  },
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        {
          "key": "Access-Control-Allow-Origin",
          "value": "*"
        }
      ]
    }
  ]
}
// next.config.js(Next.js の Server Actions のボディサイズを変更)
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: {
      // Server Actions のボディサイズ上限を変更(Vercel インフラの制限は超えられない)
      bodySizeLimit: '4mb',
    },
  },
};

module.exports = nextConfig;
// Pages Router の API Route でのボディパーサー無効化
// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export const config = {
  api: {
    // bodyParser を無効にしてストリームとして受け取る
    bodyParser: false,
    // responseLimit: false, // レスポンスサイズ制限を無効化
  },
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  // formidable などでマルチパートを解析
  const { IncomingForm } = await import('formidable');
  const form = new IncomingForm({ maxFileSize: 4 * 1024 * 1024 }); // 4MB 上限

  form.parse(req, (err, fields, files) => {
    if (err) {
      return res.status(400).json({ error: err.message });
    }
    const file = Array.isArray(files.file) ? files.file[0] : files.file;
    if (!file) return res.status(400).json({ error: 'ファイルが見つかりません。' });
    res.json({ filename: file.originalFilename, size: file.size });
  });
}

Vercel Blob 사용 방법

Vercel Blob은 Vercel이 제공하는 객체 스토리지 서비스입니다. @vercel/blob 패키지를 사용하면 S3 설정 없이 Vercel의 앱에서 파일을 저장하고 제공할 수 있습니다.

npm install @vercel/blob
// サーバーサイドアップロード(4.5MB 以下のファイル向け)
// app/api/upload/route.ts
import { put, del, list } from '@vercel/blob';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const form = await request.formData();
  const file = form.get('file') as File;

  if (!file) {
    return NextResponse.json({ error: 'ファイルが見つかりません。' }, { status: 400 });
  }

  // Vercel Blob にアップロード
  // 環境変数 BLOB_READ_WRITE_TOKEN が必要
  const blob = await put(file.name, file, {
    access: 'public',        // 'public'(誰でも閲覧可)または 'private'
    addRandomSuffix: true,   // ファイル名にランダムなサフィックスを追加
    contentType: file.type,  // MIMEタイプを明示
  });

  return NextResponse.json({
    url: blob.url,           // 配信 URL
    downloadUrl: blob.downloadUrl, // ダウンロード URL
    pathname: blob.pathname, // パス名
    size: blob.size,         // ファイルサイズ(バイト)
    contentType: blob.contentType,
  });
}

// ファイル一覧の取得
export async function GET() {
  const { blobs } = await list({ prefix: 'uploads/' });
  return NextResponse.json(blobs);
}

// ファイルの削除
export async function DELETE(request: NextRequest) {
  const { url } = await request.json();
  await del(url);
  return NextResponse.json({ deleted: true });
}
// クライアントアップロード(4.5MB 超のファイル向け)
// Vercel Blob がトークンを発行してクライアントから直接アップロード
// app/api/upload/route.ts
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest): Promise<NextResponse> {
  const body = (await request.json()) as HandleUploadBody;

  try {
    const jsonResponse = await handleUpload({
      body,
      request,
      onBeforeGenerateToken: async (pathname, clientPayload) => {
        // ここでユーザー認証やバリデーションを行う
        return {
          allowedContentTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'],
          maximumSizeInBytes: 100 * 1024 * 1024, // 100MB
          tokenPayload: JSON.stringify({ userId: 'user-123' }),
        };
      },
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        // アップロード完了後の処理(DB への保存など)
        const { userId } = JSON.parse(tokenPayload ?? '{}');
        console.log(`ユーザー ${userId} が ${blob.url} をアップロードしました`);
        // await db.files.create({ userId, url: blob.url });
      },
    });

    return NextResponse.json(jsonResponse);
  } catch (error) {
    return NextResponse.json({ error: String(error) }, { status: 400 });
  }
}
// クライアントコンポーネントでのクライアントアップロード
// components/BlobUpload.tsx
'use client';

import { upload } from '@vercel/blob/client';
import { useState } from 'react';

export default function BlobUpload() {
  const [url, setUrl] = useState('');
  const [progress, setProgress] = useState(0);

  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const blob = await upload(file.name, file, {
      access: 'public',
      handleUploadUrl: '/api/upload', // トークン発行エンドポイント
      onUploadProgress: ({ percentage }) => {
        setProgress(Math.round(percentage));
      },
    });

    setUrl(blob.url);
  };

  return (
    <div>
      <input type="file" onChange={handleUpload} />
      {progress > 0 && progress < 100 && <p>{progress}% アップロード中...</p>}
      {url && <a href={url}>アップロードされたファイル</a>}
    </div>
  );
}

대용량 파일의 우회 방법: S3로의 직접 업로드

Vercel Blob을 사용하지 않거나 S3를 직접 사용하고 싶다면, Presigned URL을 통해 클라이언트에서 S3로 직접 업로드하는 방법이 가장 확실합니다. Vercel Function은 토큰 생성만 수행하므로 본문 크기 제한의 영향을 받지 않습니다.

// app/api/presigned-url/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { NextRequest, NextResponse } from 'next/server';

const s3 = new S3Client({ region: process.env.AWS_REGION! });

export async function POST(request: NextRequest) {
  // このリクエストはファイル本体を含まないため、Vercel の上限に引っかからない
  const { filename, contentType, size } = await request.json();

  const key = `uploads/${Date.now()}-${filename}`;
  const command = new PutObjectCommand({
    Bucket: process.env.AWS_BUCKET_NAME!,
    Key: key,
    ContentType: contentType,
  });

  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });

  return NextResponse.json({ presignedUrl, key });
}

// クライアントコードでは:
// 1. /api/presigned-url に POST して URL を取得
// 2. 取得した URL に XHR で PUT リクエストを送信(進捗バー付き)
// → Vercel Function を経由しないため容量制限なし

❓ 자주 묻는 질문

Vercel에서 파일 업로드 상한을 올리는 방법이 있나요?
Vercel Serverless Function의 바디 크기 상한(4.5MB)은 인프라 수준의 제한이며 vercel.json이나 애플리케이션 코드로는 변경할 수 없습니다. 4.5MB를 넘는 파일은 Vercel Blob의 클라이언트 업로드 기능을 쓰거나 S3 Presigned URL을 통해 클라이언트가 S3에 직접 업로드합니다. 둘 다 파일 본체가 Function을 거치지 않으므로 제한의 영향을 받지 않습니다.
Vercel Blob과 AWS S3 중 무엇을 쓰면 되나요?
Vercel Blob은 Vercel과의 통합이 간단하고 설정이 최소한으로 끝나지만 스토리지 비용은 S3보다 비쌉니다. 소~중규모 프로젝트나 Vercel 생태계 안에 머물고 싶은 경우에 적합합니다. 대용량 스토리지가 필요하거나 비용을 중시하거나 다른 서비스(Lambda, ECS 등)와 공유하는 경우에는 S3가 적합합니다.
Vercel 무료 플랜에서 파일 업로드를 쓸 수 있나요?
Vercel Blob은 Hobby 플랜(무료)에서 월 500MB의 무료 사용량이 있습니다. Serverless Function을 경유하는 업로드는 4.5MB 상한 범위 안에서 무료 플랜에서도 이용할 수 있습니다. 다만 Function의 실행 시간 제한(Hobby: 10초)에 주의가 필요합니다. 대용량 파일 처리에는 Pro 플랜 이상을 권장합니다.

이 기사에서 사용할 수 있는 테스트 파일 (무료)