소개
일반적인 Cloudflare Worker는 많은 요청에 응답할 수 있지만, 다음 요청이 동일한 실행 중인 JavaScript 인스턴스에 도달한다고 가정할 수는 없습니다. 이러한 무상태 설계는 서로 독립적인 작업에 적합합니다. 하지만 지원 대기열에 있는 사람 수처럼 여러 요청이 하나의 변경 가능한 값에 동의해야 할 때는 다루기 어렵습니다.
Durable Object는 애플리케이션에 주소로 지정할 수 있는 하나의 조정 단위를 제공합니다. 이 실습에서는 각 카운터 이름이 서로 다른 객체를 선택합니다. support에 대한 요청은 계속해서 동일한 논리적 카운터에 도달하고, billing에 대한 요청은 별도의 상태를 가진 다른 카운터에 도달합니다. Cloudflare가 기본 런타임을 이동하거나 다시 시작하더라도 안정적인 객체 식별자와 SQLite 기반 상태는 애플리케이션의 계약으로 유지됩니다.
다음 네 가지 개념을 연결해 봅니다.
- 클래스는 하나의 카운터 객체가 수행할 수 있는 작업을 정의합니다.
- 네임스페이스는 해당 클래스를 기반으로 하는 객체들의 모음입니다.
- 바인딩은 프런트 도어 Worker가 네임스페이스에 접근할 수 있도록 합니다.
getByName()은 동일하게 검증된 이름을 동일한 객체 참조로 변환하고, RPC 메서드는 해당 객체의 코드를 호출합니다.
애플리케이션을 구축하고, 로컬에서 이름 기반 라우팅을 검증한 다음, 자신의 Cloudflare 학습 계정에 배포합니다. 터미널의 실행 결과를 Dashboard와 연결해 확인하고, 완료 후 클래스 네임스페이스와 Worker를 모두 삭제합니다.
이 과정을 시작하기 전에 LabEx를 Cloudflare 계정에 연결하기를 완료하세요. 이 과정에서는 LabEx VM 터미널, Wrangler 장치 인증, 계정 확인 및 계정 ID 설정을 다룹니다. 작은 JavaScript Worker가 HTTP 요청을 처리하는 방법을 이미 알고 있어야 합니다. Durable Objects에 대한 사전 지식은 필요하지 않습니다.
현재 공식 문서에서는 SQLite 기반 Durable Objects를 Workers Free에서 사용할 수 있습니다. 이 실습에서는 삭제 가능한 클래스 네임스페이스 하나와 작은 객체 몇 개, 제한된 요청만 생성합니다. Workers Paid는 필요하지 않습니다. 설정 과정에서 /home/labex/project/named-counters에 Node.js 22.22.0과 프로젝트 로컬 Wrangler 4.132.0을 설치하지만, 로그인하거나 클라우드 상태를 생성하거나 코드를 배포하거나 학습자의 구현을 완료하지는 않습니다.
VM 인증 및 애플리케이션 이름 지정
이 단계에서는 새 LabEx VM을 Cloudflare 학습 계정에 연결하고 고유한 애플리케이션 구성을 만듭니다. 브라우저에서 Dashboard에 로그인한 상태라고 해서 새 VM 내부의 명령이 자동으로 인증되는 것은 아닙니다.
준비된 프로젝트 디렉터리로 이동하고 고정된 Wrangler 버전을 확인합니다.
cd /home/labex/project/named-counters
npx wrangler --version
4.132.0이 출력되어야 합니다. Wrangler의 장치 인증 흐름을 시작합니다.
npx wrangler login --device --browser=false
Wrangler가 URL과 짧은 장치 코드를 출력합니다. 브라우저에서 URL을 열고 코드를 입력한 다음, 선택된 계정이 전용 학습 계정인지 확인하고 인증 전에 요청된 권한을 검토하세요. 터미널로 돌아온 뒤에도 Wrangler가 계속 작업해야 하므로 백그라운드 액세스 권한이 표시될 수 있습니다. 터미널을 통해 비밀번호나 토큰을 절대 보내지 마세요.
브라우저에 성공 메시지가 표시되면 터미널로 돌아와 Wrangler가 완료될 때까지 기다립니다. 구조화된 계정 정보를 요청합니다.
npx wrangler whoami --json
loggedIn: true인지 확인한 다음, 의도한 계정을 식별합니다. 계정이 하나만 표시되더라도 확인해야 합니다. 계정 이름은 사람이 확인하기 위한 값이고, ID는 터미널에 출력하지 않아도 되는 안정적인 구성 값입니다.
구조화된 결과를 저장하고, 민감하지 않은 계정 이름만 표시한 다음 LabEx Learning에 해당하는 ID를 선택합니다.
WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"
$(...)는 명령 출력을 셸 변수에 저장합니다. jq는 먼저 확인을 위해 계정 이름만 표시한 다음 연결된 ID를 내부적으로 선택합니다. test -n은 선택한 값이 비어 있지 않을 때만 성공합니다. 전용 학습 계정의 표시 이름이 다르면 해당 이름을 확인한 후 선택 표현식의 LabEx Learning을 실제 이름으로 바꾸세요.
고유한 Worker 이름을 생성합니다. openssl rand -hex 6은 12개의 무작위 16진수 문자를 만들고, $(...)는 이를 셸 변수에 삽입합니다.
RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"
wrangler.jsonc를 만듭니다. 구성 파일은 Wrangler에게 배포할 코드와 런타임에 연결할 Cloudflare 기능을 알려 줍니다. 따옴표가 없는 JSON 표시는 $RUN과 $ACCOUNT_ID를 확장할 수 있게 하며, 백슬래시는 $schema 키를 문자 그대로 유지합니다.
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
"durable_objects": {
"bindings": [
{ "name": "COUNTERS", "class_name": "Counter" }
]
},
"exports": {
"Counter": { "type": "durable-object", "storage": "sqlite" }
}
}
JSON
이 파일은 애플리케이션을 설명하지만 아직 Cloudflare에 아무것도 생성하지 않습니다. observability는 이후 Dashboard 확인 단계에서 사용할 요청 및 애플리케이션 로그를 보관합니다. Durable Object 관련 필드는 다음 단계에서 의미를 갖습니다.
네임스페이스, 바인딩 및 클래스 연결
이 단계에서는 Durable Objects 구성을 요청이 하나의 상태 저장 객체에 도달하는 경로로 이해한 다음, 바인딩을 코드에 노출하는 런타임 타입을 생성합니다.
Durable Object 클래스는 하나의 객체를 정의하는 JavaScript 설계도입니다. 이후 작성할 Counter 클래스는 값을 증가시키고 읽는 등의 작업을 정의합니다.
네임스페이스는 해당 클래스를 기반으로 하는 모든 객체의 모음입니다. 하나의 네임스페이스에는 support, billing 및 여러 이름 지정 카운터가 포함될 수 있습니다. 네임스페이스가 카운터들이 하나의 값을 공유한다는 뜻은 아닙니다. 각각의 안정적인 객체 식별자가 별도의 저장 공간을 소유합니다.
바인딩은 프런트 도어 Worker가 해당 네임스페이스에 접근할 때 사용하는 이름입니다. 이 구성은 COUNTERS라는 이름을 Counter 클래스에 연결합니다. 따라서 코드에서는 env.COUNTERS를 사용합니다.
exports 항목은 클래스의 현재 수명 주기 상태를 선언합니다. 첫 번째 배포 시 Cloudflare가 SQLite 저장소 백엔드를 사용하는 Counter를 생성하도록 지정합니다. SQLite는 새 클래스에 권장되는 백엔드이며 Workers Free에서 사용할 수 있습니다. 이 실습의 작은 테이블은 각 객체 내부에 정수 하나만 저장합니다.
구성에서 타입 설명을 생성합니다.
npx wrangler types
생성된 파일에서 COUNTERS를 검색합니다.
grep -n 'COUNTERS' worker-configuration.d.ts
다음과 비슷한 줄이 표시됩니다.
COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;
생성된 텍스트의 정확한 주변 내용은 달라질 수 있지만, 세 가지가 중요합니다. 바인딩 이름은 COUNTERS이고, 타입은 DurableObjectNamespace이며, 내보낸 Counter 클래스에 연결되어 있습니다. 바인딩을 변경할 때마다 타입을 다시 생성하여 구성과 코드가 조용히 어긋나지 않도록 하세요.
이름이 지정된 카운터 구축
이 단계에서는 Counter 클래스와 프런트 도어 Worker를 구현하여 검증된 URL 이름을 하나의 객체로 라우팅합니다.
모든 Durable Object에는 전용 저장소가 있습니다. 생성자는 counter_state라는 한 행짜리 테이블을 만들고, 해당 행이 아직 없을 때만 초기 값을 삽입합니다. blockConcurrencyWhile()는 이 짧은 초기화가 끝날 때까지 객체 요청을 지연합니다. 스키마 설정에는 적합하지만 모든 요청이나 외부 네트워크 작업을 이 함수로 감싸서는 안 됩니다.
공개 메서드인 increment()와 getCount()는 RPC 메서드입니다. RPC는 remote procedure call의 약자로, Worker가 비동기 JavaScript 객체를 다루듯 Durable Object 스텁의 메서드를 호출할 수 있게 합니다. Cloudflare가 호출을 선택된 객체로 전달합니다.
Worker 진입점을 만듭니다.
cat > src/index.js <<'JS'
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS counter_state (
key INTEGER PRIMARY KEY CHECK (key = 1),
value INTEGER NOT NULL
)
`);
this.ctx.storage.sql.exec(
"INSERT OR IGNORE INTO counter_state (key, value) VALUES (1, 0)"
);
});
}
increment() {
return this.ctx.storage.sql
.exec("UPDATE counter_state SET value = value + 1 WHERE key = 1 RETURNING value")
.one().value;
}
getCount() {
return this.ctx.storage.sql
.exec("SELECT value FROM counter_state WHERE key = 1")
.one().value;
}
}
function json(data, status = 200) {
return Response.json(data, { status });
}
function counterName(pathname) {
const match = pathname.match(/^\/counters\/([^/]+)$/);
if (!match) return { error: "not_found", status: 404 };
let name;
try {
name = decodeURIComponent(match[1]);
} catch {
return { error: "invalid_counter_name", status: 400 };
}
if (!/^[a-z][a-z0-9-]{0,31}$/.test(name)) {
return { error: "invalid_counter_name", status: 400 };
}
return { name };
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "GET" && url.pathname === "/health") {
return json({ status: "ok" });
}
const parsed = counterName(url.pathname);
if (parsed.error) return json({ error: parsed.error }, parsed.status);
if (request.method !== "GET" && request.method !== "POST") {
return json({ error: "method_not_allowed" }, 405);
}
const name = parsed.name;
const stub = env.COUNTERS.getByName(name);
const count = request.method === "POST"
? await stub.increment()
: await stub.getCount();
console.log(JSON.stringify({
event: request.method === "POST" ? "counter_incremented" : "counter_read",
name,
count
}));
return json({ name, count });
}
};
JS
라우팅 줄인 getByName(name)은 식별자가 결정되는 경계입니다. 동일하게 검증된 문자열은 결정적으로 동일한 논리적 객체를 선택하고, 다른 문자열은 다른 객체를 선택합니다. 스텁은 참조일 뿐입니다. RPC 호출이 실제로 객체에 도달할 때 객체가 지연 생성됩니다.
제공된 결정적 테스트를 실행합니다. 테스트는 작은 네임스페이스 픽스처를 사용하므로 클라우드에 요청을 보내지 않습니다.
NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs
간단한 로더는 Node가 모듈을 가져올 수 있도록 cloudflare:workers 기본 클래스의 로컬 대체 구현만 제공합니다. 네임스페이스 픽스처가 테스트되는 모든 호출을 계속 제어하므로 Cloudflare API에는 연결하지 않습니다. 세 개의 테스트가 통과해야 합니다. 그런 다음 배포하지 않고 Wrangler에게 Worker를 빌드하도록 요청합니다.
npx wrangler deploy --dry-run
테스트는 HTTP 라우팅 계약을 검증하고, dry run은 Wrangler가 실제 Durable Object 클래스를 번들링할 수 있는지 검증합니다. 어느 작업도 원격 네임스페이스를 생성하지 않습니다.
로컬에서 안정적인 이름 검증
이 단계에서는 로컬 Workers 런타임에서 애플리케이션을 실행하고, 클라우드 리소스를 생성하기 전에 두 개의 이름을 사용하여 라우팅 규칙을 확인합니다.
백그라운드에서 포트 8787로 Wrangler를 시작합니다. >는 로그를 저장하고, 2>&1은 오류를 일반 출력과 합치며, &는 터미널 프롬프트를 즉시 반환합니다. $!는 방금 시작한 명령의 프로세스 ID입니다.
npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
health 경로가 응답할 때까지 기다립니다. 이 반복문은 1초마다 한 번씩 시도하고 Worker가 응답하면 즉시 중지합니다.
for attempt in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:8787/health; then
break
fi
sleep 1
done
{"status":"ok"}가 출력되어야 합니다. support 카운터를 두 번 증가시킵니다.
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
응답에서 support가 1에서 2로 증가한 것을 확인할 수 있습니다.
{
"name": "support",
"count": 2
}
이제 billing을 한 번 증가시킵니다.
curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq
값은 3이 아니라 1입니다. 네임스페이스는 모음이고, 각 이름은 그 모음 안에서 격리된 객체 하나를 선택합니다.
두 객체를 변경하지 않고 읽습니다.
curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq
카운트는 계속 2와 1이어야 합니다. 마지막으로 getByName()이 객체를 선택하기 전에 잘못된 입력이 거부되는지 확인합니다.
curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
http://127.0.0.1:8787/counters/Not_Allowed
{"error":"invalid_counter_name"}과 HTTP 400이 출력되어야 합니다. 밑줄과 대문자는 문서에 정의된 이름 규칙에 포함되지 않습니다.
네임스페이스 배포 및 확인
이 단계에서는 로컬 런타임을 중지하고, 동일한 애플리케이션을 Cloudflare에 배포한 다음 API 동작을 Dashboard에서 확인할 수 있는 네임스페이스, 바인딩, 메트릭 및 로그와 연결합니다.
저장한 ID의 개발 프로세스만 중지합니다.
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
선언된 SQLite 기반 Counter 클래스를 포함하여 Worker를 배포합니다.
npx wrangler deploy
Wrangler가 공개 workers.dev URL과 클래스 조정 결과를 출력합니다. 예시 값을 자신의 실제 URL로 바꾸어 정확한 URL을 저장합니다.
WORKER_URL="https://YOUR_WORKER_URL"
엣지 라우트가 준비되기까지 잠시 걸릴 수 있습니다. Durable Object에 접근하지 않는 health 경로만 폴링합니다.
for attempt in $(seq 1 30); do
if curl --silent --fail "$WORKER_URL/health"; then
break
fi
sleep 2
done
support에 두 번, billing에 한 번 요청합니다.
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/billing" | jq
값을 읽습니다.
curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq
원격 애플리케이션에서도 로컬 런타임과 동일한 식별자 계약이 나타나야 합니다. support는 2, billing은 1이어야 합니다.
Cloudflare Dashboard에서 Workers & Pages를 엽니다. 고유하게 생성된 Worker가 애플리케이션 목록에 표시됩니다. 다음 스크린샷의 Worker 이름, 타임스탬프 및 계정 전체 사용량은 테스트 실행의 예시입니다. 터미널에서 생성된 자신의 labex-c10-o01-... 이름을 찾으세요.

Cloudflare Dashboard에서 Workers & Pages → Overview → 자신의 labex-c10-o01-... Worker → Settings → Bindings로 이동합니다. COUNTERS라는 Durable Object 바인딩과 해당 Counter 클래스를 찾습니다. Worker는 바인딩 이름을 알고 있으며, Cloudflare는 이를 클래스 export로 선언된 네임스페이스에 연결합니다.
바인딩 다이어그램에는 Worker가 COUNTERS를 통해 Durable Object에 연결된 모습이 표시되어야 합니다. 이 스크린샷의 Worker와 네임스페이스 이름은 특정 실행에서 사용된 예시입니다. 중요한 것은 바인딩 이름과 연결 관계입니다.

그다음 Developer Platform 탐색 메뉴에서 Durable Objects를 엽니다. 삭제할 Worker가 소유한 네임스페이스를 선택합니다. SQLite 저장소를 사용하며 클래스가 Counter인지 확인합니다. 네임스페이스는 클래스 수준의 모음이고, support와 billing은 그 안에 포함된 객체를 식별하는 이름입니다.
네임스페이스 개요에 Storage: SQL이 표시됩니다. 네임스페이스 이름과 ID는 삭제 가능한 테스트 실행에 속하므로 자신의 값과 다릅니다.

네임스페이스의 Metrics 보기를 엽니다. 최근 요청이 표시되기까지 시간이 걸릴 수 있으므로, 차트가 일시적으로 비어 있다고 해서 문제가 있다고 단정할 수 없습니다. 그래프를 강제로 생성하기 위해 대량의 요청 반복문을 실행하지 마세요.
예시 네임스페이스 스크린샷에는 런타임 요청이 성공했는데도 최근 호출 수가 0으로 표시됩니다. 이는 Dashboard 메트릭이 지연될 수 있으므로 기능 확인의 권위 있는 근거가 아니라 보조 정보라는 점을 보여 줍니다.
Worker로 돌아가 Observability → Logs를 엽니다. 최근 counter_incremented 또는 counter_read 이벤트를 찾습니다. 구조화된 로그에는 합성 카운터 이름과 카운트가 포함되지만 계정 식별자나 자격 증명은 포함되지 않습니다. 위에서 실행한 제한된 요청 중 하나와 일치하는지 확인합니다.
일치하는 이벤트 하나를 펼칩니다. 테스트 실행에서는 검증기가 생성한 이름의 카운트가 2로 끝났고, 이벤트 차트에는 성공한 요청과 오류 0건이 표시되었습니다. 자신의 합성 이름과 합계는 달라집니다.

Worker 이름, 객체 ID, 타임스탬프 및 요청 수와 같은 Dashboard 값은 자신의 실행에 따라 달라집니다. CLI/API/런타임 확인이 계속해서 권위 있는 근거이며, Dashboard 보기는 동일한 관계가 어디에 표시되는지 이해하기 위한 자료입니다.
네임스페이스 삭제 및 로그아웃
이 단계에서는 의도적으로 Counter 클래스를 폐기하고, 해당 네임스페이스와 저장된 데이터를 삭제한 다음 Worker를 제거하고 이 VM의 Wrangler 세션을 취소합니다.
Worker 스크립트만 삭제한다고 해서 저장된 Durable Object 데이터까지 삭제하겠다는 뜻은 아닙니다. exports 수명 주기는 삭제 tombstone을 사용합니다. 이는 하나의 클래스 네임스페이스를 영구적으로 삭제하도록 Cloudflare에 알리는 짧은 수명의 구성 항목입니다. 이 작업에는 휴지통이 없으므로 클래스와 Worker 이름이 이 실습에 속하는지 확인하세요.
Counter export가 없는 최소 정리 진입점을 만듭니다.
cat > src/cleanup.js <<'JS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
JS
정리 구성을 만듭니다. 동일한 Worker 이름과 계정을 유지하고, 바인딩을 제거하며, Counter만 삭제 상태로 표시합니다.
ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/cleanup.js",
"compatibility_date": "2026-09-18",
"workers_dev": true,
"preview_urls": false,
"exports": {
"Counter": { "type": "durable-object", "state": "deleted" }
}
}
JSON
tombstone을 배포합니다.
npx wrangler deploy --config wrangler.cleanup.jsonc
Wrangler의 조정 결과를 주의 깊게 읽습니다. Counter가 삭제되었다고 표시되어야 합니다. 이 작업은 클래스 네임스페이스와 support, billing, 독립 검증기가 저장한 작은 값들을 영구적으로 삭제합니다.
이제 남아 있는 무상태 정리 Worker를 삭제합니다.
npx wrangler delete --config wrangler.cleanup.jsonc
정확히 labex-c10-o01-... 애플리케이션만 확인합니다. Dashboard에서 해당 Worker가 사라졌는지, 해당 Worker가 소유하던 네임스페이스가 더 이상 표시되지 않는지 확인합니다. 과거 메트릭이나 로그는 일시적으로 남을 수 있으며 활성 리소스가 아닙니다.
인증된 삭제 확인을 실행한 후 인증을 제거합니다.
python3 .labex/verify.py deleted
PASS: deleted가 출력된 후에만 로그아웃합니다.
npx wrangler logout
npx wrangler whoami --json
최종 출력에 loggedIn: false가 명시적으로 표시되어야 합니다. 네트워크 오류는 로그아웃의 증거가 아닙니다.
요약
첫 번째 Durable Objects 애플리케이션을 구축하고 운영했습니다. 클래스가 하나의 객체 동작을 정의하고, 네임스페이스가 해당 클래스의 객체들을 그룹화하며, 바인딩이 네임스페이스를 Worker에 노출하고, getByName()이 하나의 논리적 객체를 결정적으로 선택한다는 점을 배웠습니다. RPC 메서드로 SQLite 기반 상태를 변경하고 읽었으며, 같은 이름은 카운트를 공유하고 다른 이름은 서로 격리되었고, 잘못된 이름은 객체를 선택하기 전에 거부되었습니다.
또한 런타임 동작을 Cloudflare Dashboard와 연결해 확인한 뒤, 선언적 클래스 tombstone을 사용하여 Worker를 삭제하고 로그아웃하기 전에 네임스페이스와 해당 데이터를 제거했습니다. 다음 실습에서는 이 식별자 모델을 바탕으로 SQLite를 활동 로그로 다루고, 내구성 있는 저장소가 임시 메모리 상태와 어떻게 다른지 확인합니다.



