OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 33 / 90
stack-analysis
CHAPTER 33 / 90
읽기 약 2
FUNCTION

파일 업로드: Multer + S3 + presigned URL


핵심 개념

multipart 처리·S3 직접 업로드·presigned URL — 대용량 파일 + 보안.

본문

Multer 기본 (서버 경유)

TYPESCRIPT📋 코드 (30줄)
import multer from 'multer';
import path from 'path';

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `${Date.now()}-${crypto.randomUUID()}${ext}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 10 * 1024 * 1024 },  // 10MB
  fileFilter: (req, file, cb) => {
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) cb(null, true);
    else cb(new Error('Invalid file type'));
  },
});


// 사용
app.post('/upload', authenticate, upload.single('file'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file' });
  res.json({
    url: `/uploads/${req.file.filename}`,
    size: req.file.size,
  });
});

S3 Presigned URL (권장)

TYPESCRIPT📋 코드 (32줄)
// 서버 — 업로드 URL 발급만
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'ap-northeast-2' });
const BUCKET = process.env.S3_BUCKET!;

app.post('/upload-url', authenticate, async (req, res) => {
  const { filename, contentType, size } = req.body;

  if (size > 50 * 1024 * 1024) {
    return res.status(400).json({ error: 'File too large' });
  }

  const key = `users/${req.user!.id}/${Date.now()}-${crypto.randomUUID()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    ContentType: contentType,
    ContentLength: size,
    Metadata: { uploaderId: req.user!.id },
  });

  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 60 * 5 });

  res.json({
    uploadUrl,
    key,
    publicUrl: `https://${BUCKET}.s3.amazonaws.com/${key}`,
  });
});

클라이언트 (브라우저 → S3 직접)

TYPESCRIPT📋 코드 (27줄)
async function uploadFile(file: File) {
  // 1. 서버에서 presigned URL 받기
  const { uploadUrl, publicUrl } = await fetch('/upload-url', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      size: file.size,
    }),
  }).then(r => r.json());

  // 2. S3에 직접 PUT (서버 경유 X)
  await fetch(uploadUrl, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type },
  });

  // 3. 서버에 메타데이터 저장
  await fetch('/files', {
    method: 'POST',
    body: JSON.stringify({ url: publicUrl, name: file.name }),
  });

  return publicUrl;
}

진행률 + 멀티파트 업로드 (큰 파일)

TYPESCRIPT📋 코드 (17줄)
// XMLHttpRequest로 progress 추적
function uploadWithProgress(file: File, url: string, onProgress: (n: number) => void) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', url);
    xhr.setRequestHeader('Content-Type', file.type);

    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        onProgress((e.loaded / e.total) * 100);
      }
    };
    xhr.onload = () => xhr.status === 200 ? resolve(null) : reject();
    xhr.onerror = reject;
    xhr.send(file);
  });
}

이미지 처리 (Sharp + Lambda)

TYPESCRIPT📋 코드 (23줄)
// 업로드 후 — 자동 썸네일 생성 (S3 트리거 → Lambda)
import sharp from 'sharp';

export const handler = async (event: any) => {
  const { Records } = event;
  for (const record of Records) {
    const key = record.s3.object.key;
    if (!key.startsWith('users/') || key.includes('/thumb/')) continue;

    const original = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
    const buffer = Buffer.from(await original.Body!.transformToByteArray());

    const thumbnail = await sharp(buffer)
      .resize(200, 200, { fit: 'cover' })
      .webp({ quality: 80 })
      .toBuffer();

    const thumbKey = key.replace(/^users\//, 'users/').replace(/^([^/]+\/)/, '$1thumb/');
    await s3.send(new PutObjectCommand({
      Bucket: BUCKET, Key: thumbKey, Body: thumbnail, ContentType: 'image/webp',
    }));
  }
};

다음 챕터

CH.34 "실시간: Socket.io 프로덕션 패턴".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 코드의 파일 업로드 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

파일 업로드 관련 인기 라이브러리/패턴 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 프로젝트 전체에서 파일 업로드
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 백엔드 시장의
파일 업로드 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
파일 업로드: Multer + S3 + presigned URL 이 3가지만 확실히 잡으세요
1.Presigned URL = 브라우저 → S3 직접 업로드 — 서버 트래픽 90% 절감
2.서버는 URL 발급 + 검증만, 실제 파일은 S3가 받음
3.이미지 후처리는 S3 이벤트 + Lambda — 비동기 + 자동


공유하기
진행도 33 / 90