대용량 파일은 왜 서버를 거치지 않고 R2나 S3로 올려야 할까?

브라우저가 파일을 애플리케이션 서버에 보내고, 서버가 다시 객체 저장소로 전달하는 방식은 이해하기 쉽다. 하지만 파일이 커지면 서버는 같은 데이터를 두 번 전송하고, 메모리와 실행 시간을 오래 점유한다. 서버리스 환경의 본문 크기 제한에도 걸리기 쉽다.

Vercel Functions는 요청과 응답 본문을 각각 4.5MB로 제한한다. 이보다 큰 파일을 함수가 중계하는 구조는 적합하지 않다. Vercel Functions 제한

서명된 URL을 이용한 직접 업로드

권장 흐름은 다음과 같다.

사용자가 파일 이름, 형식, 크기를 API에 알린다.

API가 로그인, 업무 권한, 허용 형식과 용량을 확인한다.

서버가 짧게 유효한 업로드 URL과 서버가 정한 객체 키를 발급한다.

브라우저가 R2나 S3로 파일을 직접 전송한다.

업로드가 끝나면 애플리케이션에 완료를 알린다.

서버가 객체의 존재, 크기, 형식과 무결성을 다시 확인한다.

검사가 끝난 파일만 업무 데이터에 연결한다.

서명된 URL에는 저장소 비밀 키가 들어가지 않는다. 특정 객체와 작업, 만료 시간을 서명한 임시 권한이다. 다만 URL을 가진 사람은 만료 전까지 허용된 작업을 할 수 있으므로 짧게 발급하고 로그나 분석 도구에 남기지 않는다.

Cloudflare R2의 사전 서명 URL은 GET, HEAD, PUT, DELETE를 지원하고 만료 시간은 1초부터 7일까지 설정할 수 있다. Content-Type을 서명에 포함해 다른 형식의 업로드를 거부할 수도 있다. 브라우저에서 쓸 때는 허용한 출처만 접근하도록 CORS를 설정해야 한다. R2 사전 서명 URL

애플리케이션이 객체 키를 정해야 한다

사용자가 보낸 원본 파일명을 그대로 저장 경로로 쓰지 않는다. 조직 ID, 업무 ID, 무작위 ID를 조합한 키를 서버가 만들고, 원본 이름은 DB 메타데이터로 보존한다. 경로 충돌, 덮어쓰기, 다른 조직 파일 접근을 막기 쉽다.

권한도 읽기와 쓰기를 나눈다. 업로드 URL은 한 객체에 대한 PUT만 허용하고, 내려받기는 별도의 짧은 GET URL이나 인증 프록시를 사용한다. 공개 파일만 custom domain과 캐시를 사용한다.

업로드 완료를 그대로 신뢰하지 않는다

브라우저가 보낸 파일명과 MIME 형식은 조작할 수 있다. 서버나 후속 워커가 실제 크기, 파일 서명, 확장자, 악성 코드 여부를 확인한다. 검사 전 파일은 격리 경로에 두고, 통과한 뒤 공개 또는 업무 경로로 옮긴다.

DB에는 업로드 요청, 업로드됨, 검사 중, 사용 가능, 거부됨, 삭제됨 같은 상태를 둔다. 객체는 올라갔지만 완료 알림이 오지 않은 경우를 찾아 정리하는 작업도 필요하다.

큰 파일은 멀티파트로 나눈다

수백 MB 이상의 파일은 여러 조각으로 올리면 실패한 조각만 다시 보낼 수 있다. R2의 멀티파트 업로드는 마지막 조각을 제외하면 최소 5MiB, 조각당 최대 5GiB, 최대 10,000개 조각을 지원한다. 완료되지 않은 업로드는 기본 7일 뒤 중단되며 수명 주기로 기간을 바꿀 수 있다. R2 객체 업로드

R2와 S3 중 선택

기준 | R2 | S3
인터넷 전송 비용 | R2에서 나가는 전송료가 없음 | 리전과 전송 경로에 따라 과금
생태계 | S3 호환 API와 Cloudflare 연동 | AWS 서비스와 가장 폭넓게 연동
호환성 | 일부 S3 기능과 헤더는 미지원 | S3 기능의 기준
잘 맞는 경우 | 다운로드가 많고 Cloudflare를 쓰는 서비스 | AWS 내부 처리, 규정, 고급 저장 기능 연동

R2는 S3 API와 폭넓게 호환되지만 ACL, KMS 헤더, 일부 Object Lock 관련 기능처럼 지원하지 않는 항목이 있다. 기존 S3 도구를 그대로 옮기기 전에 실제 사용하는 API를 확인한다. R2 S3 API 호환성

직접 업로드의 목적은 서버 비용만 줄이는 것이 아니다. 큰 파일 전송을 객체 저장소에 맡기고, 애플리케이션은 권한, 상태, 검사와 업무 연결에 집중하게 만드는 것이다.