소개
지원 애플리케이션에서 문서를 받아 스토리지 버킷을 공개하지 않은 채 다시 전송해야 합니다. 비공개 R2 버킷을 Worker 에 연결하고, 크기가 제한된 업로드를 구현한 다음, 호출자에게 다운로드 내용을 스트리밍합니다. 스트림은 다운로드 전체를 먼저 메모리에 모으지 않고 준비되는 청크를 즉시 전달합니다.
먼저 문서 버킷 구성과 Workers 구성/시크릿 학습을 완료합니다. 이 새 VM 에는 Node.js 22.22.0, Wrangler 4.131.1, 테스트용 문서 및 제공된 인증 모듈이 준비되어 있습니다. 이 모듈은 일회용 토큰으로 데모 엔드포인트를 보호하므로, 스토리지 학습 과정에서 제한 없는 업로드 서비스가 노출되지 않습니다. 애플리케이션 인증을 수정하는 방법은 이 과정의 뒷부분에서 배웁니다.
시작하기 전에 학습 계정에 활성 R2 구독이 있고 새 버킷과 Worker 를 관리할 권한이 있어야 합니다. R2 가격을 확인합니다. 스토리지/작업 사용량과 Worker 사용량은 별도로 과금됩니다. 구매한 도메인은 필요하지 않습니다. 테스트용 파일만 사용하고, 실습이 끝나면 이 실습의 Worker, 객체 및 버킷을 삭제합니다. 각 VM 은 자체 인증이 필요하며 이전 VM 의 리소스를 재사용하지 않습니다.
애플리케이션 버킷 연결하기
이 단계에서는 이 VM 을 인증하고 애플리케이션용 독립적인 비공개 버킷을 만듭니다. 디바이스 인증은 학습 계정을 확인합니다. R2 버킷 관리는 해당 계정으로 제한된 별도의 API 토큰을 사용합니다.
아래 명령 구문에 맞게 Bash 를 시작한 다음, 준비된 프로젝트로 이동하여 도구를 확인합니다. 리소스 이름 변수의 값이 유지되도록 같은 터미널을 계속 사용합니다.
bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version
표시된 디바이스 코드를 본인 브라우저에서 승인합니다. 동의하기 전에 학습 계정과 요청된 account 및 user 읽기 범위를 확인합니다.
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json
loggedIn: true인지 확인합니다. 계정이 하나만 표시되더라도 계정 이름을 읽습니다. 아래의 YOUR_ACCOUNT_ID를 해당 계정의 실제 32 자 ID 로 바꿉니다. openssl rand -hex 6은 12 개의 무작위 16 진수 문자를 생성하므로 이전 실행과 리소스 이름이 충돌하지 않습니다. 이 here-document 는 표준 구성 파일을 작성하며, 셸이 변수 값을 파일에 삽입합니다.
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r02-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON
버킷을 관리하려면 Cloudflare 프로필의 API Tokens 페이지를 열고 이 실습의 이름을 딴 사용자 지정 토큰을 만듭니다. Account → Workers R2 Storage → Edit 권한을 부여하고, Account Resources를 저장한 학습 계정으로 제한합니다. 만료 기간은 짧게 설정합니다. 다른 계정이나 관련 없는 권한은 포함하지 않습니다. 이 관리 토큰은 생성과 삭제를 포함한 버킷 관리에 사용합니다. 이 실습에서 Worker 는 DOCUMENTS 바인딩을 통해 R2 객체에 접근합니다.
토큰을 한 번만 복사하여 이 숨겨진 VM 프롬프트에 입력합니다. umask 077은 파일 접근 권한을 사용자로 제한하고, read -s는 입력 내용을 화면에 표시하지 않습니다. 이 파일은 Wrangler 의 표준 토큰 변수를 사용하며 Git 에서 제외됩니다.
umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN
R2 관리 명령에만 --env-file=.env.management를 사용합니다. 일반적인 whoami 명령은 계속 VM 의 디바이스 인증을 확인합니다.
--env-file을 각 Wrangler 명령의 끝에 배치하여 파일 인수 목록에 명령 이름까지 포함되지 않게 합니다. 각 버킷을 만든 후 Wrangler 가 구성에 바인딩을 추가할지 물으면 n을 입력하고 Enter 를 누릅니다. 필요한 바인딩은 이미 구성에 포함되어 있습니다.
npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management
버킷 목록을 표시하고 방금 생성한 정확한 이름을 찾습니다. 다른 버킷은 다른 작업에 속하므로 수정하지 않습니다.
npx wrangler r2 bucket list --env-file=.env.management
Dashboard 에서 Storage & databases → R2 → Overview를 열고, 방금 생성한 정확한 버킷을 선택한 다음 비어 있는 객체 목록을 확인합니다. 버킷 설정에서 공개 개발 URL 과 사용자 지정 도메인은 비활성화된 상태로 둡니다. Dashboard 에 표시된 버킷 이름은 버킷의 식별 정보를 확인해 줍니다. 저장된 바이트가 정확한지는 이후 다운로드 확인으로 검증합니다.
Worker 스크립트 권한은 배포에 사용됩니다. KV 권한은 Wrangler 의 삭제 기록 관리에 사용되지만, 이 실습에서는 KV 네임스페이스를 만들지 않습니다. R2 관리 토큰은 계정 범위가 지정된 별도의 자격 증명으로 유지됩니다.
크기가 제한된 업로드와 스트리밍 다운로드 구현하기
이 단계에서는 DOCUMENTS 구성 바인딩을 객체 작업에 사용합니다. **바인딩 (binding)**은 Cloudflare 가 Worker 에 제공하는 런타임 객체입니다. env.DOCUMENTS는 이름으로 구성된 비공개 버킷을 가리키며, Worker 가 버킷을 사용하기 위해 S3 시크릿을 별도로 필요로 하지 않습니다.
제공된 src/auth.js는 일회용 bearer 토큰을 확인합니다. 이 라우트는 단순한 .txt 문서 이름만 허용합니다. PUT은 선택한 키의 바이트를 교체합니다. 이 예제는 길이 헤더를 생략한 클라이언트를 포함하여 최대 1MiB(1,048,576 바이트) 까지 허용합니다. 업로드 청크는 이 제한 범위까지만 수집하므로 R2 에 길이가 알려진 본문을 전달할 수 있습니다. 다운로드는 object.body를 응답에 직접 전달하며 스트리밍 상태를 유지합니다.
다음 here-document 로 핸들러를 작성합니다.
cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
const MAX_BYTES = 1024 * 1024;
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path === "/health" && request.method === "GET") return new Response("ok");
if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
if (!/^\/documents\/[a-z0-9-]+\.txt$/.test(path)) return new Response("Not found", { status: 404 });
const key = path.slice(1);
if (request.method === "PUT") {
if (Number(request.headers.get("Content-Length")) > MAX_BYTES)
return new Response("Too large", { status: 413 });
// Count actual bytes too: a request may omit Content-Length.
const reader = request.body?.getReader();
if (!reader) return new Response("Body required", { status: 400 });
const chunks = [];
let total = 0;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > MAX_BYTES) {
await reader.cancel();
return new Response("Too large", { status: 413 });
}
chunks.push(value);
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
await env.DOCUMENTS.put(key, bytes, { httpMetadata: { contentType: "text/plain" } });
return new Response("Stored", { status: 201 });
}
if (request.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, PUT" } });
const object = await env.DOCUMENTS.get(key);
if (object === null) return new Response("Not found", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("ETag", object.httpEtag);
headers.set("Cache-Control", "private, no-store");
return new Response(object.body, { headers });
}
};
JS
get()은 키가 없으면 null을 반환하므로, 본문을 읽기 전에 이를 처리해야 합니다. writeHttpMetadata는 저장된 콘텐츠 유형을 복원하고, httpEtag는 이미 올바른 따옴표 형식으로 제공됩니다. private, no-store 설정은 보호된 문서가 공유 캐시에 저장되지 않도록 합니다.
Wrangler 가 로컬 개발에 불러올 애플리케이션 토큰을 .dev.vars에 생성합니다. 이 토큰은 테스트용으로 만든 자격 증명이며 Cloudflare 계정 자격 증명과 별개입니다.
umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars
배포하지 않고 Wrangler 가 코드를 번들링할 수 있는지 확인합니다. 플랫폼 확인은 새로운 테스트 데이터를 사용하는 별도의 임시 로컬 런타임에서 실행되며, 정확한 바이트, 두 가지 크기 제한 경로 및 초과 크기 객체가 생성되지 않는지를 검증합니다.
npx wrangler deploy --dry-run
로컬 스토리지 경계 테스트하기
이 단계에서는 로컬 R2 스토리지에서 Worker 를 실행합니다. 기본적으로 wrangler dev는 로컬 시뮬레이션을 사용하므로 이 요청으로 클라우드 객체가 생성되지 않습니다. 개발 서버를 백그라운드에서 실행합니다. $!은 정리할 때 사용할 이 작업의 프로세스 ID 를 기록합니다.
npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!
dev.log에 서버가 준비되었다는 내용이 표시될 때까지 기다린 다음, 이 터미널에 일회용 애플리케이션 토큰을 불러옵니다. 토큰을 출력하지 않습니다.
cat dev.log
set -a
source .dev.vars
set +a
준비된 파일을 업로드하고 다운로드합니다. --data-binary는 파일의 바이트를 그대로 유지하고, -o는 다운로드 결과를 파일에 저장합니다.
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt http://127.0.0.1:8787/documents/report.txt
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o local-download.txt
cmp document.txt local-download.txt
업로드 응답이 201 Stored인지 확인하고, 바이트 비교 명령이 아무 출력 없이 성공하는지 확인합니다. 존재하지 않는 키와 제한보다 1 바이트 큰 업로드도 테스트합니다. Python 은 크기가 제한된 테스트 파일만 생성합니다.
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/missing.txt
python3 -c "open('oversized.txt','wb').write(b'x' * (1024 * 1024 + 1))"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt http://127.0.0.1:8787/documents/large.txt
404 Not found와 413 Too large가 반환되는지 확인합니다. 예상한 HTTP 오류가 그대로 표시되도록 이 curl 호출에서는 의도적으로 --fail을 생략했습니다. 프록시에서 반환한 오류 HTML 페이지는 애플리케이션의 응답이 아닙니다. 로컬 서버를 중지하기 전에 플랫폼 확인을 실행합니다.
배포하고 비공개 버킷 통합 확인하기
이 단계에서는 실제 R2 에서 문서 작업을 반복합니다. 로컬에서 성공했다고 해서 원격 바인딩이나 계정 소유권이 확인되는 것은 아닙니다.
개발 서버를 중지하고 Worker 를 배포합니다.
kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler deploy
표준 일괄 명령으로 애플리케이션 시크릿을 업로드합니다. .dev.vars는 배포 시 자동으로 업로드되지 않습니다.
npx wrangler secret bulk .dev.vars
배포 출력에 표시된 정확한 HTTPS workers.dev URL 을 후행 슬래시 없이 BASE_URL에 입력합니다. /health가 ok를 반환할 때까지 기다립니다. 배포 전파가 아직 진행 중이면 최대 1 분 동안 읽기 요청을 반복합니다.
BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"
원격 버킷에 보고서를 업로드하고 다운로드한 뒤 두 파일을 비교합니다.
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt "$BASE_URL/documents/report.txt"
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote-download.txt
cmp document.txt remote-download.txt
응답이 201 Stored이고 두 파일의 바이트가 동일한지 확인합니다. 공개 엔드포인트를 대상으로 부정적인 테스트도 반복합니다.
curl -i "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/missing.txt"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt "$BASE_URL/documents/large.txt"
401 Unauthorized, 404 Not found, 413 Too large가 반환되는지 확인합니다. Dashboard 에서 이 Worker 를 열고 R2 바인딩을 확인한 다음, 정확한 버킷을 열어 documents/report.txt를 찾습니다. 공개 개발 URL 과 사용자 지정 도메인은 계속 비활성화된 상태로 둡니다. Worker 가 접근 경로를 제공한다고 해서 버킷의 비공개 설정만으로 모든 Worker 라우트가 자동으로 안전해지는 것은 아닙니다.

DOCUMENTS 행은 이 Worker 와 정확한 대상 버킷의 연결을 보여 줍니다. 예시에서 생성된 리소스 이름은 사용자 이름과 다릅니다.

객체 행은 report.txt, text/plain, 41 B 를 표시하며 Public Access 는 Disabled 상태입니다. 이름과 날짜는 예시입니다. Bucket Size 는 갱신이 지연되어 0 B 로 표시될 수 있습니다. 객체 행과 성공한 다운로드가 보고서의 존재를 입증합니다.
원격 애플리케이션과 버킷 삭제하기
이 단계에서는 인증이 유지된 상태에서 이 실습의 Worker 와 객체만 삭제합니다. Worker 를 삭제해도 비공개 버킷은 자동으로 삭제되지 않습니다.
npx wrangler delete
생성된 정확한 Worker 이름을 확인합니다. 업로드한 객체 하나를 명시적으로 삭제한 다음 버킷을 삭제합니다.
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
크기 제한을 초과한 요청으로 documents/large.txt가 생성되어서는 안 됩니다. 버킷이 예상과 달리 비어 있지 않다면 이 버킷만 검사하고, 크기 계약이 실패한 원인을 진단한 후 정확한 테스트용 키를 삭제합니다. 이러한 수정이 필요했다면 앞선 기능 확인은 통과한 것이 아닙니다.
Dashboard 에서 Worker 목록과 버킷 목록을 새로 고치고 플랫폼 정리 확인을 실행합니다. 인증 또는 네트워크 오류가 발생한 경우 삭제가 성공했다고 판단할 수 없습니다.
남은 자격 증명 정리하기
이 단계에서는 프로필의 API Tokens 페이지에서 이 실습의 관리 토큰을 폐기하고, 로컬 애플리케이션 시크릿을 삭제한 다음 VM 인증을 종료합니다. 이전 정리 확인이 통과한 후에만 수행합니다.
rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
loggedIn: false인지 확인합니다. 관리 토큰 폐기는 Dashboard 에서 별도로 수행해야 합니다. 로컬 파일만 삭제해도 토큰이 폐기되지는 않습니다. 일반적인 Dashboard 로그인과 다른 실습의 토큰은 그대로 둡니다.
요약
비공개 R2 스토리지를 Worker 에 연결하고, 크기가 제한된 업로드를 수락하며, 정확한 문서 바이트를 스트리밍하고, 오류를 처리한 후 소유한 클라우드 리소스를 삭제합니다.



