목표와 한 줄 답
서버를 거치지 않고 브라우저에서 Cloudflare R2로 파일을 직접 업로드하려면 백엔드에서 Presigned URL을 생성해 클라이언트에 전달하고, 클라이언트는 해당 URL로 PUT 요청을 보내면 된다. 이 방식은 서버 대역폭을 절약하고 업로드 속도를 높이며, URL에 만료 시간과 권한을 제한해 보안을 유지할 수 있다.
핵심 항목
1. 백엔드 Presigned URL 생성
준비
- R2 버킷과 API 토큰(Admin Read & Write 또는 Object Read & Write 권한)
- AWS SDK(예:
@aws-sdk/client-s3,@aws-sdk/s3-request-presigner) 또는 호환 라이브러리 - 백엔드 엔드포인트(Node.js, Python 등)
단계
- S3Client 인스턴스 생성 시 endpoint를
https://<ACCOUNT_ID>.r2.cloudflarestorage.com으로 설정 - PutObjectCommand로 업로드할 객체 키와 메타데이터 지정
- getSignedUrl 함수로 만료 시간(expiresIn, 초 단위)을 설정해 Presigned URL 생성
- 생성된 URL을 JSON 응답으로 클라이언트에 반환
주의점
- expiresIn을 너무 길게 설정하면 URL이 유출됐을 때 위험하므로 업로드 예상 시간보다 약간 여유만 둔다(예: 300~3600초)
- Presigned URL 생성 시 지정한 Content-Type과 클라이언트 업로드 시 헤더가 일치해야 한다
완료 기준
- 백엔드 엔드포인트 호출 시 유효한 Presigned URL이 반환되고, URL에 X-Amz-Signature와 X-Amz-Expires 쿼리 파라미터가 포함된다
2. R2 버킷 CORS 설정
준비
- Cloudflare 대시보드 접근 권한 또는 Wrangler CLI
- 클라이언트가 요청할 도메인 목록
단계
- Cloudflare 대시보드에서 R2 버킷 선택 후 Settings 탭 이동
- CORS Policy 섹션에서 Add CORS Policy 클릭
- AllowedOrigins에 클라이언트 도메인 입력(예:
https://example.com, 개발 시http://localhost:3000) - AllowedMethods에 PUT 추가
- AllowedHeaders에
Content-Type,Content-Length등 업로드 시 사용할 헤더 추가 - ExposeHeaders에
ETag추가(업로드 완료 후 응답 헤더 읽기 위해)
주의점
- AllowedOrigins에
*를 사용하면 모든 도메인에서 접근 가능하므로 프로덕션에서는 구체적인 도메인만 명시한다 - Presigned URL은 CORS 정책과 독립적이지만, 브라우저가 preflight OPTIONS 요청을 보낼 수 있으므로 CORS 설정이 필수다
완료 기준
- 브라우저 개발자 도구 네트워크 탭에서 OPTIONS preflight 요청이 200 응답을 받고, Access-Control-Allow-Origin 헤더가 올바르게 반환된다
3. 클라이언트 업로드 구현
준비
- 백엔드로부터 받은 Presigned URL
- 업로드할 File 객체(input type="file" 또는 Blob)
- fetch 또는 XMLHttpRequest API
단계
- 백엔드 엔드포인트를 호출해 Presigned URL 요청
- 응답으로 받은 URL에 PUT 메서드로 파일 데이터를 body에 담아 요청
- Content-Type 헤더를 Presigned URL 생성 시 지정한 값과 동일하게 설정
- 응답 상태 코드가 200이면 업로드 성공
주의점
- PUT 요청 시 body에는 File 객체를 직접 전달하고, FormData로 감싸지 않는다(Presigned URL은 multipart/form-data가 아닌 바이너리 직접 업로드를 가정)
- Content-Type이 일치하지 않으면 403 SignatureDoesNotMatch 오류가 발생한다
완료 기준
- PUT 요청 응답이 200이고, ETag 헤더가 반환되며, R2 버킷에서 업로드된 객체를 확인할 수 있다
전체 실행 순서
- R2 버킷 생성 및 API 토큰 발급
- 백엔드에 AWS SDK 설치 및 S3Client 설정
- Presigned URL 생성 엔드포인트 구현
- R2 버킷에 CORS 정책 추가
- 클라이언트에서 파일 선택 UI 구현
- 파일 선택 시 백엔드로 URL 요청
- 받은 Presigned URL로 PUT 요청 전송
- 업로드 완료 후 응답 상태 확인
주의점과 실패하기 쉬운 지점
Presigned URL 만료
URL 생성 후 expiresIn 시간이 지나면 403 AccessDenied 오류가 발생한다. 대용량 파일이나 느린 네트워크 환경을 고려해 충분한 여유를 두되, 보안을 위해 필요 이상으로 길게 설정하지 않는다.
Content-Type 불일치
Presigned URL 생성 시 PutObjectCommand에 ContentType을 지정했다면, 클라이언트 PUT 요청의 Content-Type 헤더가 정확히 일치해야 한다. 불일치 시 SignatureDoesNotMatch 오류가 반환된다.
CORS preflight 실패
AllowedHeaders에 클라이언트가 전송하는 모든 커스텀 헤더를 포함하지 않으면 preflight OPTIONS 요청이 실패하고 브라우저가 실제 PUT 요청을 차단한다. Content-Type, Content-Length, Authorization 등 필요한 헤더를 모두 명시한다.
FormData 사용 오류
Presigned URL은 바이너리 직접 업로드를 전제로 하므로, body에 File 객체를 그대로 전달해야 한다. FormData로 감싸면 multipart/form-data 형식이 되어 서명 불일치로 업로드가 실패한다.
권한 부족
API 토큰에 Object Read & Write 권한이 없으면 Presigned URL 생성은 성공하지만 실제 업로드 시 403 오류가 발생할 수 있다. 토큰 권한을 확인하고 필요 시 재발급한다.
완료 확인 체크리스트
- [ ] 백엔드 엔드포인트가 유효한 Presigned URL을 반환한다
- [ ] URL에 X-Amz-Signature, X-Amz-Expires 파라미터가 포함되어 있다
- [ ] R2 버킷 CORS 정책에 클라이언트 도메인과 PUT 메서드가 추가되어 있다
- [ ] 브라우저 개발자 도구에서 OPTIONS preflight 요청이 200 응답을 받는다
- [ ] 클라이언트 PUT 요청의 Content-Type이 Presigned URL 생성 시 지정한 값과 일치한다
- [ ] PUT 요청 응답 상태 코드가 200이고 ETag 헤더가 반환된다
- [ ] R2 버킷 대시보드 또는 API로 업로드된 객체를 확인할 수 있다
- [ ] 만료 시간 이후 동일 URL로 업로드 시도 시 403 오류가 발생한다
