소개
지원 API 에서 열린 티켓을 찾고 개별 레코드를 안전하게 생성, 수정, 삭제해야 합니다. Worker 를 D1 에 연결하고, 매개변수화된 SQL 액세스를 구현하며, API 가 누락된 레코드와 잘못된 입력을 처리하는 방식을 테스트합니다.
HTTP 라우터는 이미 제공되므로 주요 작업은 데이터베이스 통합입니다. 이 독립 실습에서는 삭제 가능한 Worker 하나와 D1 데이터베이스 하나를 사용하고, 로컬 데이터는 별도로 관리합니다.
개인 학습 계정과 새 VM 을 사용합니다. 먼저 설정 과정에서 Node.js 22.22.0 을 준비한 다음 /home/labex/project/ticket-database에서 프로젝트 전용 Wrangler 4.131.1 과 평가에 필요한 종속 항목을 npm install로 설치합니다. 직접 지정된 종속 항목 버전은 고정되어 있으며, 설치 과정에서 자체 lockfile 이 생성됩니다. 설정 과정에서는 클라우드 로그인이나 평가 대상 데이터베이스 작업을 수행하지 않습니다. 개인 컴퓨터에서는 프로젝트에서 npm install --save-dev wrangler@4.131.1을 실행해 같은 Wrangler 버전을 설치합니다.
이 실습에서는 D1 Free allowances 범위 안에서 소규모의 합성 레코드를 사용합니다. 기존 계정 사용량도 해당 한도에 포함됩니다. 구매한 도메인은 필요하지 않습니다. 리소스 삭제와 로그아웃을 모두 확인할 때까지 이 VM 을 유지합니다.
이 VM 을 인증하고 계정 선택
이 단계에서는 새 터미널을 개인 학습 계정에 연결합니다. Dashboard 에 로그인하는 것만으로는 VM 이 인증되지 않습니다. D1 권한은 데이터베이스 생성, SQL 변경 및 삭제를 허용하고, Workers 권한은 배포를 허용하며, KV 권한은 Wrangler 정리 작업의 인벤토리를 지원합니다. 인증하기 전에 Background Access 를 포함한 실제 동의 페이지의 내용을 확인합니다.
준비된 프로젝트로 이동한 후 고정된 CLI 버전을 확인합니다.
cd /home/labex/project/ticket-database
npx wrangler --version
4.131.1이 표시되어야 합니다. 디바이스 인증을 시작합니다. --device는 브라우저 코드를 표시하고, --browser=false는 브라우저를 직접 선택하도록 합니다.
npx wrangler login --device --browser=false --scopes account:read user:read d1:write workers_scripts:write workers_kv:write
브라우저에서 표시된 URL 을 열고 현재 코드를 입력합니다. 학습 계정과 권한을 확인한 후 인증합니다. 터미널에 성공 메시지가 표시될 때까지 기다립니다. 비밀번호나 토큰을 프로젝트 파일에 붙여 넣지 마십시오.
npx wrangler whoami --json
loggedIn: true인지 확인한 다음, 계정이 하나만 표시되더라도 계정의 name과 id를 읽습니다. 사용할 계정의 ID 를 아래 구성에 복사합니다. 다음 셸 변수는 6 개의 무작위 바이트 (16 진수 12 자) 를 사용해 다른 학습자의 이름과 충돌하지 않도록 합니다. here-document 는 JSON으로 표시된 두 줄 사이의 JSON 을 기록하며, 내부의 $RUN은 확장됩니다.
$schema 앞의 백슬래시는 이 JSON 키를 그대로 유지하며, $RUN 은 이번 실행의 고유 이름으로 확장됩니다.
RUN=labex-c04-d02-$(openssl rand -hex 6)
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "YOUR_ACCOUNT_ID",
"main": "src/index.js",
"compatibility_date": "2026-09-15",
"workers_dev": true,
"preview_urls": false
}
JSON
블록을 실행하기 전에 YOUR_ACCOUNT_ID를 실제 계정 ID 로 바꿉니다. RUN이 계속 유지되도록 이 터미널을 열어 둡니다. name은 이번 실행을 식별하고, account_id는 클라우드 작업에 사용할 계정을 선택합니다. 이 파일은 일반 JSON 이며 유효한 JSONC 이기도 합니다. 이 파일을 작성하는 것만으로는 Worker 가 배포되지 않습니다.
독립적인 로컬 데이터와 원격 데이터 준비
이 단계에서는 D1 연결을 만들고 익숙한 티켓 테이블에 초기 데이터를 넣습니다. 설정에서 src/index.js에 HTTP 라우팅과 입력 검증을 제공하며, 누락된 저장소 함수는 src/store.js에 있습니다. 따라서 작업의 초점을 SQL 액세스에 맞출 수 있습니다.
삭제 가능한 클라우드 데이터베이스를 만듭니다. --binding DB는 애플리케이션 코드에서 사용할 짧은 이름을 지정하고, --update-config는 실제 이름과 UUID 를 wrangler.jsonc에 기록하며, --use-remote=false는 개발 작업을 로컬에서 수행하도록 합니다.
npx wrangler d1 create "$RUN-db" --binding DB --update-config --use-remote=false
생성된 이름과 ID 를 읽은 다음 저장된 바인딩을 확인합니다.
cat wrangler.jsonc
DB 항목에는 이번 실행에서 만든 데이터베이스의 이름이 있어야 합니다. 바인딩은 코드와 리소스를 연결하도록 구성된 연결입니다. UUID 는 클라우드 데이터베이스를 식별하고, --local은 이 VM 의 별도 SQLite 데이터베이스를 사용합니다. SQL 명령에는 항상 --local 또는 --remote 중 하나를 포함합니다.
cat schema.sql
npx wrangler d1 execute DB --local --file schema.sql
npx wrangler d1 execute DB --remote --file schema.sql
이제 두 대상에 동일한 초기 티켓 두 개가 들어 있습니다. DB는 제공된 핸들러가 env.DB로 받는 이름이며, 구성의 바인딩과 일치해야 합니다.
매개변수화된 CRUD 구현
이 단계에서는 CRUD, 즉 생성 (create), 조회 (read), 수정 (update), 삭제 (delete) 를 구현합니다. 준비된 명령문은 SQL 구조와 입력값을 분리합니다. 각 ?는 매개변수 자리 표시자이며, .bind(...)가 순서대로 값을 제공합니다. 사용자 입력이 무해해 보이더라도 SQL 문자열에 직접 연결하지 마십시오.
WHERE는 영향을 받는 레코드를 제한합니다. .all()은 results 필드에 행 배열을 포함한 결과 객체를 반환합니다. .first()는 한 행 또는 null을 반환합니다. SQLite 의 RETURNING 절을 사용하면 별도의 조회 없이 변경된 행을 반환할 수 있습니다. 삭제 작업에서는 .run()이 meta.changes를 제공하며, 이를 통해 실제로 레코드가 존재했는지 라우터가 판단할 수 있습니다.
저장소 모듈을 작성합니다.
cat > src/store.js <<'JS'
export async function list(db, status) {
const query = status === null
? db.prepare('SELECT id, subject, status, source FROM tickets ORDER BY id')
: db.prepare('SELECT id, subject, status, source FROM tickets WHERE status = ? ORDER BY id').bind(status);
const { results } = await query.all();
return results;
}
export async function get(db, id) {
return db.prepare('SELECT id, subject, status, source FROM tickets WHERE id = ?').bind(id).first();
}
export async function create(db, subject) {
return db.prepare("INSERT INTO tickets (subject, source) VALUES (?, 'api') RETURNING id, subject, status, source").bind(subject).first();
}
export async function update(db, id, status) {
return db.prepare('UPDATE tickets SET status = ? WHERE id = ? RETURNING id, subject, status, source').bind(status, id).first();
}
export async function remove(db, id) {
const result = await db.prepare('DELETE FROM tickets WHERE id = ?').bind(id).run();
return result.meta.changes === 1;
}
JS
제공된 라우팅이 이 함수들을 어떻게 사용하는지 src/index.js에서 확인합니다. 누락된 레코드는 제어된 404 응답이 되고, 잘못된 형식의 입력은 400 응답이 되며, 데이터베이스 오류가 발생하면 SQL 내부 정보를 노출하지 않고 503 응답이 됩니다.
터미널을 계속 사용할 수 있도록 로컬 서버를 백그라운드 작업으로 시작합니다.
npx wrangler dev --ip 0.0.0.0 > dev.log 2>&1 &
시작 로그를 읽고 수신 대기 메시지가 표시될 때까지 기다립니다.
cat dev.log
curl -i http://localhost:8787/tickets?status=open
HTTP 200 응답과 티켓 1 만 표시되어야 합니다. 서버는 로컬 데이터베이스를 사용합니다. 정리 작업에 필요하므로 터미널에 표시된 작업 번호를 기억해 둡니다.
로컬에서 쓰기 작업과 거부되는 입력 테스트
이 단계에서는 성공적인 조회 외의 동작도 테스트합니다. curl -i는 HTTP 상태와 헤더를 표시하고, -H는 JSON 콘텐츠 유형을 지정하며, -d는 기본적으로 POST 요청의 본문을 전송합니다.
SQL 처럼 보이는 구두점이 포함된 제목을 생성합니다.
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d "{\"subject\":\"Printer ' OR 1=1 --\"}"
제목이 데이터로 그대로 보존된 201 응답이 표시되어야 합니다. 반환된 숫자 id를 아래의 TICKET_ID에 복사합니다. 반복해서 테스트한 후에도 ID 가 같다고 가정하지 마십시오.
TICKET_ID=YOUR_RETURNED_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
curl -i -X PATCH http://localhost:8787/tickets/$TICKET_ID -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE http://localhost:8787/tickets/$TICKET_ID
curl -i http://localhost:8787/tickets/$TICKET_ID
처음에는 200 조회 응답, 다음에는 closed 상태가 포함된 200 수정 응답, 그다음에는 본문이 없는 204 삭제 응답, 마지막에는 {"error":"not_found"}가 포함된 404 응답이 표시되어야 합니다. 기존 티켓 두 개는 그대로 남아 있어야 합니다.
잘못된 JSON 과 유효하지 않은 상태를 전송합니다.
curl -i http://localhost:8787/tickets -H 'Content-Type: application/json' -d '{'
curl -i -X PATCH http://localhost:8787/tickets/1 -H 'Content-Type: application/json' -d '{"status":"lost"}'
각각 invalid_json과 invalid_status를 포함한 HTTP 400 응답이 표시되어야 합니다. SQL 매개변수화는 입력값이 SQL 로 해석되지 않도록 하고, 애플리케이션 검증은 비즈니스 규칙에서 벗어난 값을 거부합니다. 두 기능은 서로 다른 문제를 해결합니다.
바인딩된 데이터베이스 배포 및 테스트
이 단계에서는 핸들러와 D1 바인딩을 배포합니다. 원격 데이터베이스에는 이미 초기 데이터가 들어 있으므로, 배포 과정에서 로컬 행이 복사되지는 않습니다.
npx wrangler deploy
배포 결과에 표시된 실제 https://...workers.dev URL 을 셸 변수에 복사합니다. 이 API 는 삭제할 합성 테스트 리소스이므로 테스트가 끝난 후 제거합니다.
URL='YOUR_DEPLOYED_HTTPS_URL'
curl -i "$URL/tickets?status=open"
200 응답과 티켓 1 이 표시되어야 합니다. 새로 배포한 직후 일시적으로 플랫폼 오류가 발생하면 몇 초 기다린 후 최대 1 분 동안 이 조회를 반복합니다. 상태 코드와 JSON 이 모두 일치할 때만 계속 진행합니다. 오류가 계속되면 원인을 조사해야 합니다.
원격 API 에서도 CRUD 를 반복하고, 원격 API 가 반환한 ID 를 복사합니다.
curl -i "$URL/tickets" -H 'Content-Type: application/json' -d '{"subject":"Remote test"}'
TICKET_ID=YOUR_RETURNED_ID
curl -i -X PATCH "$URL/tickets/$TICKET_ID" -H 'Content-Type: application/json' -d '{"status":"closed"}'
curl -i -X DELETE "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets/$TICKET_ID"
curl -i "$URL/tickets"
순서대로 201, 200, 204, 404 응답과 변경되지 않은 초기 티켓 두 개가 표시되어야 합니다. Dashboard 에서 이 Worker 와 해당 Worker 의 Bindings 보기를 엽니다. DB가 사용자가 만든 데이터베이스를 가리키는지 확인하고, 데이터베이스 링크를 따라가 읽기 전용으로 확인합니다. 저장된 로컬 바인딩만으로는 배포된 연결이 올바르다는 증거가 되지 않습니다.

이 예시는 배포된 Worker 가 DB를 통해 D1 데이터베이스에 연결된 모습을 보여 줍니다. 무작위 접미사는 이 예시 실행을 식별하므로 실제 리소스 이름은 다릅니다. 표의 Value 링크는 배포된 바인딩이 선택한 데이터베이스를 엽니다.
삭제 가능한 리소스 삭제
이 단계에서는 VM 이 아직 인증된 상태에서 이 실습의 리소스만 삭제합니다. 먼저 모든 기능 검사를 완료합니다. 삭제 확인이 끝날 때까지 구성 파일을 유지합니다.
npx wrangler delete
이번 실행의 구성에 있는 Worker 이름만 표시되는지 확인합니다.
npx wrangler d1 delete DB
확인 프롬프트를 읽고 이번 실행에서 만든 데이터베이스만 표시되는지 확인합니다. 그런 다음 데이터베이스 목록을 조회합니다.
npx wrangler d1 list --json
기록해 둔 데이터베이스 이름과 UUID 가 성공적인 응답에 없어야 합니다. 다른 리소스는 남아 있을 수 있습니다. 인증 또는 네트워크 오류만으로는 삭제 여부를 판단할 수 없습니다. 액세스 문제를 해결한 후 계속하기 전에 목록 조회를 다시 실행합니다. 로그인 상태를 유지한 채 이 단계의 검증을 실행합니다.
로컬 개발 작업도 중지합니다. 작업 목록을 확인한 후 직접 시작한 wrangler dev 작업만 종료합니다. 작업 번호가 다르면 %1을 해당 번호로 바꿉니다.
jobs
kill %1
이 VM 의 인증 종료
이 단계에서는 독립적인 삭제 확인이 통과한 후에만 인증을 종료합니다. 로그아웃하면 이 VM 에 저장된 Wrangler 인증 정보가 제거됩니다. VM 을 닫는 것만으로는 클라우드 정리가 완료되지 않습니다.
npx wrangler logout
npx wrangler whoami --json
loggedIn: false가 표시되어야 합니다. 인증되지 않은 이 조회는 0 이 아닌 종료 코드를 반환할 수 있습니다. 구조화된 응답에 로그아웃 상태가 명시적으로 표시된 경우에만 정상적인 결과입니다. 확인을 완료한 후 실습 환경을 종료합니다.
요약
Worker 에 티켓 검색 기능을 추가하는 방법을 실습했습니다. 확인 가능한 데이터베이스 결과를 점검하고, 선택한 계정과 로컬 상태를 명확히 관리했으며, 로그아웃하기 전에 삭제 가능한 리소스를 제거했습니다.



