| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- Kubernetes
- JavaScript
- CSS
- API
- 성능
- HTTP
- Debugging
- auth
- observability
- NextJS
- frontend
- database
- react
- Ops
- aws
- reliability
- Git
- SRE
- Security
- Operations
- web
- architecture
- Infra
- Performance
- version-control
- Microservices
- CI
- backend
- DevOps
- 버전관리
- Today
- Total
고민보단 실천을
React에서 S3 Presigned URL로 파일 PUT 업로드하기: CORS와 Content-Type 함정 본문
React에서 S3 Presigned URL로 파일 PUT 업로드하기: CORS와 Content-Type 함정
이 글은 시리즈 3단계(마지막)입니다. 2단계에서 만든 /api/uploads/presign-put API로 presigned URL을 발급받고, React(브라우저)에서 그 URL로 S3에 직접 PUT 업로드합니다.
시리즈: 1단계(s3-presigned-url-what-and-why.json) → 2단계(spring-boot-kotlin-s3-presigned-url-server.json) → 3단계(이 글)
옵션/핵심 요소(3~6개)
| 항목 | 의미 | 언제 쓰는지(실무 상황) |
|---|---|---|
| presign 엔드포인트 | 서버에서 URL을 발급받는 API | 웹에서는 보통 /api/uploads/presign-put로 고정하고 dev 프록시로 연결 |
| Content-Type | PUT 업로드 헤더 | 서명에 포함되면 presign 요청 값과 PUT 헤더 값이 반드시 일치해야 함 |
| PUT 업로드 | 파일 바디를 S3로 직접 전송 | 서버를 거치지 않으므로 대용량 업로드에 유리 |
| 에러 처리 | presign 실패 vs S3 PUT 실패 구분 | 로그/리트라이 정책을 다르게 적용(만료면 재발급) |
| 업로드 완료 통지(선택) | 서버에 key 저장/후처리 트리거 | DB 저장, 썸네일 생성, 바이러스 스캔 같은 후처리가 필요할 때 |
React 예시(단일 컴포넌트)
가장 단순한 형태로 작성했습니다. 핵심은 1) presign 요청 2) presigned URL로 PUT 업로드입니다.
import React, { useMemo, useState } from 'react'
type PresignPutResponse = { key: string; url: string; expiresAt: string }
export function S3PresignedUploadDemo() {
const [file, setFile] = useState<File | null>(null)
const [status, setStatus] = useState('')
const [uploadedKey, setUploadedKey] = useState('')
const contentType = useMemo(() => {
if (!file) return ''
return file.type || 'application/octet-stream'
}, [file])
async function upload() {
if (!file) return
setStatus('Presigning...')
setUploadedKey('')
const presignRes = await fetch('/api/uploads/presign-put', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, contentType }),
})
if (!presignRes.ok) {
throw new Error('Presign failed: ' + presignRes.status)
}
const presigned: PresignPutResponse = await presignRes.json()
setStatus('Uploading to S3...')
const putRes = await fetch(presigned.url, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: file,
})
if (!putRes.ok) {
throw new Error('S3 PUT failed: ' + putRes.status)
}
setStatus('Upload complete')
setUploadedKey(presigned.key)
// 선택: 서버에 업로드 완료 통지(메타데이터 저장/후처리)
// await fetch('/api/uploads/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: presigned.key }) })
}
return (
<div style={{ maxWidth: 720, padding: 16 }}>
<h2>S3 Presigned URL Upload</h2>
<input type='file' onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
<div style={{ marginTop: 12 }}>
<button disabled={!file} onClick={() => upload().catch((e) => setStatus(String(e)))}>Upload</button>
</div>
<div style={{ marginTop: 12 }}><strong>Status:</strong> {status || '-'}</div>
{uploadedKey ? (<div style={{ marginTop: 12 }}><strong>Uploaded key:</strong> <code>{uploadedKey}</code></div>) : null}
</div>
)
}로컬 개발에서 /api가 404일 때
React dev server가 http://localhost:5173, Spring Boot가 http://localhost:8080이면 /api 요청이 프론트 서버로 가서 404가 날 수 있습니다. 보통 Vite proxy로 해결합니다.
// vite.config.ts (예시)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8080',
},
},
})문제 상황(정확히 1개)
상황: PUT 업로드가 브라우저에서 CORS 에러로 막힌다(콘솔에 preflight 실패 메시지).
원인: S3 버킷 CORS 설정에 현재 origin(예: http://localhost:5173) 또는 PUT 메서드가 허용되어 있지 않다. 또는 AllowedHeaders가 너무 좁아서 Content-Type 헤더가 막힌다.
해결: S3 CORS에 AllowedOrigins에 현재 origin을 추가하고, AllowedMethods에 PUT, AllowedHeaders를 충분히 허용한다(2단계의 CORS 예시를 그대로 적용).
예방 팁: 개발/운영 origin을 분리해 명시하고, 배포 환경에서 실제 도메인(origin)이 CORS에 포함되어 있는지 체크리스트로 관리합니다.
