콘텐츠로 건너뛰기

Netlify Functions에서 파일 업로드하는 방법|제한・Large Media・회피 전략

카테고리: Netlify·배포 설정
이 기사는 현재 일본어로만 제공됩니다. 번역본은 순차적으로 공개될 예정입니다.

Netlify Functions(AWS Lambda 기반 서버리스 함수)로 파일 업로드를 구현할 때 요청 본문에는 크기 제한이 있습니다. 기본 6MB 제한을 모르고 막히는 경우가 많습니다. 이 문서는 Netlify Functions의 본문 크기 제한, <code>netlify.toml</code> 설정, <code>multipart/form-data</code> 파싱 방법, 그리고 제한을 초과하는 파일을 다루는 대체 방안을 설명합니다.

Functions 6 MB request body 10s timeout Background Fn 6 MB request body 15min timeout Edge Functions 20 MB request body streaming OK
그림: Netlify 함수 유형별 본문 크기 제한

Netlify Functions 본문 크기 제한

Netlify Functions는 AWS Lambda 기반이며 요청 본문 크기에 고정 상한이 있습니다.

Function의 종류 본문 크기 제한 변경 가능 여부 비고
Netlify Functions(동기) 6 MB 불가능 AWS Lambda의 제한
Netlify Functions(Background) 6 MB 불가능 처리 시간은 최대 15분
Netlify Edge Functions 제한 있음 불가능 Deno 기반 런타임

Netlify Functions에 6MB를 초과하는 요청을 보내면 HTTP 413 또는 함수 타임아웃이 발생합니다. Vercel(4.5MB)보다는 약간 크지만, 이미지나 동영상 같은 대용량 파일은 여전히 직접 받을 수 없습니다.

netlify.toml의 설정

<code>netlify.toml</code>에서는 Functions의 런타임, 타임아웃, 메모리, 리다이렉트, 헤더 등을 설정할 수 있습니다. 바디 크기 자체는 변경할 수 없지만 관련 설정을 이해하는 것이 중요합니다.

# netlify.toml

[build]
  command = "npm run build"
  publish = ".next"
  functions = "netlify/functions"  # Functions のディレクトリ

# Node.js バンドラーの設定
[functions]
  node_bundler = "esbuild"

# 特定の Function の設定
[functions."upload"]
  included_files = ["uploads/**"]

# リダイレクト設定(Next.js など SPA との組み合わせ)
[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

# ヘッダーの設定(CORS など)
[[headers]]
  for = "/.netlify/functions/*"
  [headers.values]
    Access-Control-Allow-Origin = "*"
    Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"
    Access-Control-Allow-Headers = "Content-Type, Authorization"

# 環境変数(本番用。機密情報は Netlify UI で設定すること)
[context.production.environment]
  NODE_ENV = "production"

[context.deploy-preview.environment]
  NODE_ENV = "development"

multipart/form-data 파싱 구현

Netlify Functions에서는 요청 본문이 기본적으로 Base64 인코딩될 수 있습니다. <code>busboy</code> 나 <code>formidable</code> 을 사용한 멀티파트 파싱 방법을 설명합니다.

// netlify/functions/upload.js(CommonJS)
const busboy = require('busboy');

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  return new Promise((resolve, reject) => {
    const contentType = event.headers['content-type'] || event.headers['Content-Type'];

    const bb = busboy({ headers: { 'content-type': contentType } });
    const files = [];
    const fields = {};

    bb.on('file', (name, file, info) => {
      const { filename, encoding, mimeType } = info;
      const chunks = [];

      file.on('data', (data) => chunks.push(data));
      file.on('end', () => {
        const buffer = Buffer.concat(chunks);

        // ファイルサイズの検証(6MB 上限の手前で確認)
        const MAX_SIZE = 5 * 1024 * 1024; // 5MB(余裕を持たせる)
        if (buffer.length > MAX_SIZE) {
          resolve({
            statusCode: 413,
            body: JSON.stringify({ error: 'ファイルが大きすぎます(上限5MB)。' }),
          });
          return;
        }

        files.push({ name, filename, mimeType, buffer, size: buffer.length });
      });
    });

    bb.on('field', (name, value) => {
      fields[name] = value;
    });

    bb.on('finish', () => {
      if (files.length === 0) {
        resolve({
          statusCode: 400,
          body: JSON.stringify({ error: 'ファイルが見つかりません。' }),
        });
        return;
      }

      const file = files[0];
      // ここでファイルを S3 などに保存する処理を行う

      resolve({
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          success: true,
          filename: file.filename,
          size: file.size,
          mimeType: file.mimeType,
          fields,
        }),
      });
    });

    bb.on('error', (err) => {
      resolve({
        statusCode: 500,
        body: JSON.stringify({ error: err.message }),
      });
    });

    // Netlify Functions ではボディが Base64 エンコードされる場合がある
    const body = event.isBase64Encoded
      ? Buffer.from(event.body, 'base64')
      : event.body;

    bb.end(body);
  });
};
// netlify/functions/upload-s3.js(S3 に転送する例)
const busboy = require('busboy');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

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

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  return new Promise((resolve) => {
    const bb = busboy({
      headers: { 'content-type': event.headers['content-type'] },
      limits: { fileSize: 5 * 1024 * 1024 }, // 5MB で切り捨て
    });

    bb.on('file', async (fieldName, stream, { filename, mimeType }) => {
      const chunks = [];
      let truncated = false;

      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('limit', () => { truncated = true; });

      stream.on('end', async () => {
        if (truncated) {
          return resolve({
            statusCode: 413,
            body: JSON.stringify({ error: 'ファイルが5MBを超えています。' }),
          });
        }

        const buffer = Buffer.concat(chunks);
        const key = `uploads/${Date.now()}-${filename}`;

        try {
          await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_S3_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: mimeType,
          }));

          resolve({
            statusCode: 200,
            body: JSON.stringify({
              success: true,
              key,
              url: `https://${process.env.AWS_S3_BUCKET}.s3.amazonaws.com/${key}`,
            }),
          });
        } catch (err) {
          resolve({ statusCode: 500, body: JSON.stringify({ error: err.message }) });
        }
      });
    });

    const body = event.isBase64Encoded
      ? Buffer.from(event.body, 'base64')
      : event.body;

    bb.end(body);
  });
};

Netlify Large Media(폐지 예정)와 대체안

Netlify Large Media는 Git LFS 기반으로 대용량 파일을 관리하는 서비스였지만, 현재 신규 가입이 제한되어 있으며 대체 수단으로의 이전이 권장됩니다.

# Netlify Large Media(非推奨・新規利用不可)
# .lfsconfig
[lfs]
    url = https://large-media.netlify.com/<repo-id>

대체 방안으로 권장되는 구성은 다음과 같습니다.

// Presigned URL 経由で S3 に直接アップロードする Netlify Function
// netlify/functions/get-upload-url.js
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

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

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  const { filename, contentType, size } = JSON.parse(event.body);

  // サーバー側でのバリデーション
  const MAX_SIZE = 100 * 1024 * 1024; // 100MB
  if (size > MAX_SIZE) {
    return { statusCode: 413, body: JSON.stringify({ error: 'ファイルが大きすぎます。' }) };
  }

  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!allowedTypes.includes(contentType)) {
    return { statusCode: 415, body: JSON.stringify({ error: '許可されていない形式です。' }) };
  }

  const key = `uploads/${Date.now()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,
    Key: key,
    ContentType: contentType,
    ContentLength: size,
  });

  // 署名付き URL を生成(このリクエストはファイル本体を含まないため制限外)
  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 900 }); // 15分

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ presignedUrl, key }),
  };
};
// クライアント側:Netlify Function から Presigned URL を取得して S3 に直接アップロード
async function uploadFile(file) {
  // 1. Netlify Function から Presigned URL を取得
  const res = await fetch('/.netlify/functions/get-upload-url', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      size: file.size,
    }),
  });

  const { presignedUrl, key } = await res.json();

  // 2. S3 に直接 PUT(Netlify Function を経由しないため容量制限なし)
  const uploadRes = await fetch(presignedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file,
  });

  if (!uploadRes.ok) throw new Error('アップロードに失敗しました');

  return key;
}

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

  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG 테스트 이미지 (1MB)</a> — Netlify Functions 경유 업로드 동작 확인용
  • → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF 테스트 파일(1MB)</a> — 6MB 제한 이내의 파일 업로드 테스트용
  • → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">ZIP 테스트 파일 (1MB)</a> — Presigned URL을 통한 S3 직접 업로드 테스트용

관련 기사

  • → <a href="/ja/blog/vercel-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">Vercel 파일 업로드 설정과 제한 | Vercel Blob・API 제한・회피 방법</a>
  • → <a href="/ja/blog/render-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">Render에서 PHP/Node.js 파일 업로드를 설정하는 방법 | Disk・환경 변수・S3 연동</a>
  • → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">AWS S3・CloudFront 파일 업로드 제한 정리</a>