지원 요청 API 구축

CloudflareBeginner
지금 연습하기

소개

지원 양식에는 유효한 요청, 잘못된 JSON, 존재하지 않는 경로, 사용할 수 없는 티켓 서비스를 구분하는 API 가 필요합니다. 이 실습에서는 JavaScript 로 HTTP 경계를 구축하고 로컬에서 테스트한 다음, 임시 업스트림 Worker 와 함께 배포합니다.

개인 Cloudflare 학습 계정과 연결 실습에서 사용한 디바이스 인증 지식을 준비합니다. 이 실습은 /home/labex/project/support-api에 Node.js 22.22.0 과 프로젝트 로컬 Wrangler 4.131.1 이 설치된 새 VM 에서 시작합니다. 기본적인 JavaScript 함수, 객체 및 모듈 사용법이 선수 조건입니다. HTTP 동작과 비동기 요청은 이 실습에서 설명합니다. 공개 Worker 두 개는 합성 데이터만 사용합니다. 제공된 업스트림은 요청을 확인하지만 아무것도 저장하지 않으므로, 영구적인 티켓 시스템이 아닙니다. 이 간단한 실습에는 Workers Free 와 workers.dev 서브도메인이면 충분하며, 데이터베이스, 구매한 도메인 또는 유료 업그레이드는 필요하지 않습니다. 요청은 계정의 Workers 사용량에 포함됩니다.

실습을 끝내기 전에 두 Worker 를 모두 삭제하고 로그아웃합니다. 리소스 이름과 URL 에 사용되는 셸 변수를 유지하려면 같은 터미널을 계속 사용합니다.

경로와 메서드로 요청 라우팅

이 단계에서는 지원하는 각 URL 에 명시적인 메서드와 응답을 지정합니다. 경로는 작업을 식별하고, 메서드는 동작을 설명합니다. GET /health는 사용 가능 여부를 확인하고, POST /requests는 지원 요청을 받습니다.

준비된 프로젝트로 이동하고 도구를 확인합니다.

cd /home/labex/project/support-api
node --version
npx wrangler --version

Node 는 v22.22.0, Wrangler 는 4.131.1로 표시되어야 합니다. 설치는 이미 완료되어 있습니다. 개인 컴퓨터에서는 Node 를 설치한 뒤 프로젝트에서 npm install --save-dev wrangler@4.131.1을 사용합니다. lockfile 이 있는 프로젝트를 재현할 때는 npm ci를 사용합니다.

고유한 임시 이름을 생성합니다. openssl rand -hex 6은 임의의 16 진수 문자 12 개를 출력하고, $(...)은 그 출력을 삽입합니다. 셸 할당은 이후 명령에서 사용할 수 있도록 값을 저장합니다.

WORKER_NAME="labex-support-$(openssl rand -hex 6)"

표준 Wrangler 구성을 작성합니다. cat > file <<MARKER는 닫는 마커가 나올 때까지 다음 줄을 파일에 작성합니다. 따옴표가 없는 마커를 사용하므로 셸이 $WORKER_NAME을 치환합니다.

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "vars": {"UPSTREAM_URL": "http://127.0.0.1:8081"}
}
CONFIG

main은 핸들러를 지정하고, compatibility_date는 런타임 동작을 선택하며, varsenv를 통해 비밀이 아닌 업스트림 주소를 제공합니다. 현재는 나중에 시작할 로컬 픽스처를 가리킵니다. 리소스 목록을 단순하게 유지하기 위해 공개 미리 보기 URL 은 비활성화합니다.

핸들러를 작성합니다. 따옴표로 묶은 JS 마커는 JavaScript 를 그대로 보존합니다. new URL(...).pathname은 경로를 추출합니다. 삼항 표현식은 허용되는 메서드를 선택하며, HTTP 405 응답은 Allow 헤더에 해당 메서드도 표시합니다. Response.json은 객체를 직렬화하고 콘텐츠 형식을 설정합니다. async 핸들러를 사용하면 이후 단계에서 비동기 작업을 await할 수 있습니다.

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    return Response.json({error: 'not_implemented'}, {status: 501});
  }
};
JS

로컬 Wrangler 를 백그라운드에서 시작합니다. >는 출력을 리디렉션하고, 2>&1은 오류도 포함하며, &는 서버가 실행되는 동안 터미널 프롬프트를 바로 돌려줍니다.

npx wrangler dev --port 8080 > api.log 2>&1 &
cat api.log

로그에 포트 8080 에서 준비되었다는 내용이 표시될 때까지 기다립니다. 아직 시작 중이면 cat api.log를 다시 실행합니다. curl -i는 HTTP 상태와 헤더를 함께 표시합니다.

curl -i http://127.0.0.1:8080/health
curl -i http://127.0.0.1:8080/missing
curl -i http://127.0.0.1:8080/requests

각각 200 과 {"status":"ok"}, 404 와 {"error":"not_found"}, 405 와 {"error":"method_not_allowed"}Allow: POST가 표시되어야 합니다. 이러한 오류 응답은 의도된 결과입니다. 서버가 계속 실행 중일 때 검증 버튼을 사용합니다.

JSON 입력 구문 분석 및 검증

이 단계에서는 업스트림 서비스를 호출하기 전에 잘못된 입력을 거부합니다. HTTP 415 는 미디어 형식을 지원하지 않는다는 뜻이고, 400 은 JSON 을 구문 분석할 수 없다는 뜻이며, 422 는 구문 분석한 데이터가 계약을 충족하지 않는다는 뜻입니다. subject는 공백을 제거한 뒤 1~80 자를 포함하는 문자열이어야 합니다.

핸들러를 다음의 완전한 버전으로 교체합니다. headers.get은 선언된 미디어 형식을 읽고, ;에서 분할하면 charset 매개변수를 허용할 수 있습니다. await request.json()은 구문 분석이 끝날 때까지 기다리며 본문을 한 번 소비합니다. try/catch는 구문 분석 예외를 예측 가능한 응답으로 변환합니다. JSON 은 null, 배열 또는 숫자도 나타낼 수 있으므로 문자열 메서드를 사용하기 전에 데이터 형태를 확인합니다. trim()은 허용되는 subject를 정규화합니다.

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    return Response.json({subject}, {status: 201});
  }
};
JS

소스가 변경되면 Wrangler 가 다시 로드합니다. 컴파일 오류가 있는지 cat api.log로 확인합니다. 유효한 요청을 보냅니다. -H는 헤더를 지정하고, --data는 본문을 지정하며 POST 를 선택합니다. 작은따옴표를 사용하면 셸에서 JSON 의 큰따옴표가 그대로 유지됩니다.

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"  Printer offline  "}'

201 과 {"subject":"Printer offline"}이 표시되어야 합니다. 이는 메모리에서만 확인하는 응답이며 저장된 티켓이 아닙니다. 세 가지 서로 다른 거부 경로를 실행합니다.

curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"   "}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: text/plain" --data 'hello'

각각 400 invalid_json, 422 invalid_subject, 415 unsupported_media_type이 표시되어야 합니다. JSON null, [], {"subject":5}도 시도합니다. 각각 예외를 발생시키지 않고 422 를 반환해야 합니다. 검증 버튼을 사용합니다. 검증 버튼은 이러한 경계 조건과 기존 health 및 라우팅 동작을 확인합니다.

업스트림 호출 및 오류 처리

이 단계에서는 API 를 제공된 티켓 서비스 시뮬레이터에 연결합니다. 업스트림은 서비스가 호출하는 종속 서비스입니다. 시뮬레이터는 일반적인 subject에 대해 합성 티켓 하나를 반환하고, 특수한 subjectsimulate-outage에 대해서는 HTTP 503 을 반환합니다. 요청은 저장하지 않습니다.

제공된 소스를 확인하여 픽스처의 동작을 이해한 다음, 고유한 Worker ID 를 구성합니다.

cat upstream/index.js
cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false
}
CONFIG

--config는 두 번째 구성을 선택합니다. 두 로컬 Worker 를 함께 실행할 수 있도록 포트 8081 과 별도의 인스펙터 포트를 사용합니다.

npx wrangler dev --config upstream/wrangler.jsonc --port 8081 --inspector-port 9230 > upstream.log 2>&1 &
cat upstream.log
curl -i http://127.0.0.1:8081/health

준비될 때까지 기다립니다. 200 과 {"service":"support-upstream","status":"ok"}가 표시되어야 합니다. 이제 주 핸들러를 완전한 통합 버전으로 교체합니다. 전역 fetch 함수는 외부 요청을 보내고, JSON.stringify는 검증된 subject를 인코딩합니다. await는 응답을 기다립니다. HTTP 오류는 예외를 발생시키지 않으므로 upstream.ok로 상태를 명시적으로 확인해야 합니다. catch는 연결 실패 또는 JSON 응답을 읽을 수 없는 경우를 별도로 처리합니다. HTTP 502 는 내부 응답 본문을 노출하지 않고 종속 서비스가 실패했음을 클라이언트에 알립니다.

cat > src/index.js <<'JS'
export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    if (path !== '/health' && path !== '/requests') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const allowed = path === '/health' ? 'GET' : 'POST';
    if (request.method !== allowed) {
      return Response.json({error: 'method_not_allowed'}, {
        status: 405, headers: {Allow: allowed}
      });
    }
    if (path === '/health') return Response.json({status: 'ok'});
    const mediaType = (request.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    if (mediaType !== 'application/json') {
      return Response.json({error: 'unsupported_media_type'}, {status: 415});
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return Response.json({error: 'invalid_json'}, {status: 400});
    }
    if (!body || Array.isArray(body) || typeof body.subject !== 'string' ||
        body.subject.trim().length < 1 || body.subject.trim().length > 80) {
      return Response.json({error: 'invalid_subject'}, {status: 422});
    }
    const subject = body.subject.trim();
    try {
      const upstream = await fetch(`${env.UPSTREAM_URL}/tickets`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({subject})
      });
      if (!upstream.ok) {
        return Response.json({error: 'upstream_unavailable'}, {status: 502});
      }
      const ticket = await upstream.json();
      return Response.json({ticket: ticket.ticket, subject}, {status: 201});
    } catch {
      return Response.json({error: 'upstream_unavailable'}, {status: 502});
    }
  }
};
JS
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i http://127.0.0.1:8080/requests -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'

첫 번째 요청에서는 201 과 {"ticket":"demo-1001","subject":"Printer offline"}이 표시되고, 두 번째 요청에서는 502 와 {"error":"upstream_unavailable"}이 표시되어야 합니다. 시뮬레이터의 내부 진단 정보는 나타나면 안 됩니다. subject는 합성 데이터이며, 이 엔드포인트에는 영구적인 부작용이 없습니다. 두 로컬 서버가 실행 중인 상태에서 검증 버튼을 사용합니다.

이 실습에서는 외부 서비스 경계를 연습하기 위해 일반 HTTP 를 사용합니다. 이후 실습에서는 Worker 간 내부 호출에 service binding 을 사용하는 방법을 설명합니다. 제한 시간과 더 자세한 진단은 Diagnose Worker Failures 에서 다룹니다. 픽스처는 크기가 제한된 작은 응답을 반환하지만, 프로덕션 API 에서는 신뢰할 수 없는 요청 및 응답의 크기도 제한해야 합니다.

공개 API 배포 및 실행

이 단계에서는 두 Worker 를 같은 학습 계정에 배포하고 로컬 업스트림 주소를 공개 URL 로 바꿉니다. 먼저 두 로컬 작업을 중지합니다. jobs를 확인하고 실제 작업 번호를 사용합니다. 아래 예에서는 API 가 1 번이고 업스트림이 2 번이라고 가정합니다.

jobs
kill %1 %2

새 VM 을 인증합니다. 이 권한 부여는 계정을 식별하고 Worker 배포 및 삭제를 허용합니다. 마지막 권한은 배포 실습의 권한 부여와 일치하지만, 이 실습에서는 로그 스트림이 필요하지 않습니다.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

표시된 브라우저 링크를 열고 현재 디바이스 코드를 입력합니다. Wrangler 의 권한 (필수 Background Access 포함) 을 검토한 뒤 학습 계정만 선택하고 권한을 부여합니다. 터미널로 돌아와 완료될 때까지 기다립니다.

npx wrangler whoami --json

loggedIn: true, 계정 이름 및 accounts에 표시된 실제 ID 를 확인합니다. 아래의 YOUR_ACCOUNT_ID를 해당 ID 로 바꿉니다. 1 단계에서 생성한 이름을 그대로 사용합니다. 변수를 잃어버렸다면 다른 리소스 이름을 생성하지 말고 저장된 구성에서 이름을 읽어 복원합니다.

cat > upstream/wrangler.jsonc <<CONFIG
{
  "name": "${WORKER_NAME}-upstream",
  "main": "index.js",
  "compatibility_date": "2026-09-14",
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy --config upstream/wrangler.jsonc

배포 출력에서 정확한 workers.dev URL 을 복사합니다. 계정의 기존 서브도메인을 다시 사용합니다. Wrangler 가 처음으로 서브도메인 등록을 제안하면 사용 가능한 이름을 선택하고 확인 절차를 따릅니다. 기존 계정 서브도메인은 변경하지 않습니다.

이제 주 구성을 다시 작성하고 두 자리 표시자를 계정 ID 와 업스트림 URL 로 바꿉니다. 업스트림 URL 에는 끝에 슬래시를 넣지 않습니다. global_fetch_strictly_public은 다른 Worker 가 이 계정의 workers.dev 서브도메인에 있더라도 외부 fetch()가 공개 인터넷 라우팅을 사용하도록 합니다. 이 설정이 없으면 두 Worker 가 각각 정상적으로 동작하더라도 동일한 영역에 대한 HTTP 호출이 실패할 수 있습니다. 이 플래그는 배포할 API 구성에 필요하며, 앞 단계의 로컬 loopback 픽스처에는 필요하지 않습니다. 자세한 내용은 Fetch API 안내를 참조합니다.

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-09-14",
  "compatibility_flags": ["global_fetch_strictly_public"],
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID",
  "vars": {"UPSTREAM_URL": "YOUR_UPSTREAM_URL"}
}
CONFIG
cat wrangler.jsonc
npx wrangler deploy

배포 출력에서 주 API URL 을 복사하여 아래 변수에 넣습니다.

API_URL="https://YOUR_API.YOUR_SUBDOMAIN.workers.dev"
curl -i "$API_URL/health"
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"Printer offline"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{"subject":"simulate-outage"}'
curl -i "$API_URL/requests" -H "Content-Type: application/json" --data '{'

로컬에서와 동일한 계약이 표시되어야 합니다. health 는 200, 합성 티켓은 201, 업스트림 오류는 502, 잘못된 JSON 은 400 이어야 합니다. 연결 오류가 발생하면 호스트 이름이 전파될 때까지 기다린 뒤 다시 시도합니다. Dashboard 에서 같은 학습 계정을 선택하고 Compute → Workers & Pages를 엽니다. 두 개의 정확한 이름을 찾아 배포 출력의 주소와 비교합니다. 이 단계는 읽기 전용 확인 단계이므로, 여기에서 중복 애플리케이션을 만들지 않습니다.

아래 예에는 주 API 와 이에 대응하는 -upstream 서비스가 표시되어 있습니다. 왼쪽 사이드바에서 Compute를 펼치고 Workers & Pages를 선택합니다. 계정에 다른 프로젝트가 있으면 Search applications를 사용합니다. 전체 생성 이름과 각 이름 아래의 주소를 두 배포 출력과 비교합니다. 무작위 접미사와 계정 서브도메인은 이 예와 다릅니다.

지원 API 와 이에 대응하는 업스트림 Worker 를 보여 주는 Workers and Pages

두 리소스가 선택한 같은 계정에 표시되어야 합니다. 리소스가 표시되면 배포된 위치를 확인할 수 있고, 위의 HTTP 응답으로 API 동작 여부를 확인할 수 있습니다. 이름이 하나라도 없으면 계정 선택기와 배포 출력을 확인한 뒤 다시 시도합니다. CLI 로 배포한 리소스를 복제하기 위해 Create application을 사용하지 않습니다.

검증 버튼을 사용합니다. 검증 버튼은 두 Worker 의 소유권, 배포된 업스트림 연결, 공개 엔드포인트의 성공 및 실패 응답을 독립적으로 확인합니다. 이 실습의 시뮬레이터에는 합성된 상태 비저장 요청만 전송합니다.

두 임시 Worker 삭제

이 단계에서는 두 리소스 삭제 결과를 확인할 수 있도록 인증이 유지되는 동안 API 와 업스트림을 삭제합니다. 이 실습에서 생성하는 클라우드 리소스는 이 두 개뿐입니다. 삭제하기 전에 두 구성을 확인합니다.

cat wrangler.jsonc
cat upstream/wrangler.jsonc

주 구성의 labex-support-... 이름과 이에 대응하는 -upstream 접미사, 그리고 동일한 학습 계정 ID 를 확인합니다. 먼저 주 API 를 삭제한 다음 업스트림을 삭제합니다. 각 프롬프트에서 정확한 이름을 확인하고 단일 키 y를 누릅니다.

npx wrangler delete
npx wrangler delete --config upstream/wrangler.jsonc

Wrangler 4.131.1 은 Worker 를 삭제한 뒤 레거시 Workers Sites KV 데이터를 확인할 때 인증 오류를 출력할 수 있습니다. 이 권한에는 KV 액세스가 없기 때문입니다. 이 특정 진단 메시지만으로는 삭제 성공 또는 실패를 판단할 수 없습니다. 메시지를 없애기 위해 추가 권한을 부여하지 않습니다. Workers & Pages 를 새로 고치고 검증 버튼을 사용합니다. 권한이 있는 인벤토리 확인이 성공하려면 두 이름이 모두 없어야 합니다. 네트워크 또는 인증 오류는 결과를 확정할 수 없으므로, 계속하기 전에 해결합니다. 다른 애플리케이션, 학습 계정 및 해당 서브도메인은 유지합니다.

VM 연결 해제

이 단계에서는 두 리소스 정리 확인이 완료된 후 이 VM 의 Wrangler 인증을 제거합니다. 로그아웃해도 Worker 는 삭제되지 않으므로 먼저 정리해야 합니다.

npx wrangler logout
npx wrangler whoami --json

명시적인 "loggedIn": false가 표시되어야 합니다. 인증되지 않은 상태 확인 명령은 비정상 종료 코드로 끝날 수 있지만, 구조화된 결과에 로그아웃이 명확히 표시되면 정상입니다. 네트워크 오류는 로그아웃과 같은 의미가 아닙니다. 검증 버튼을 사용한 다음 LabEx 환경을 종료합니다. 브라우저 로그인과 학습 계정은 이후 실습에서 계속 사용할 수 있습니다. 새 VM 을 시작할 때마다 자체 인증이 필요합니다.

요약

메서드를 인식하는 HTTP API 를 구축하고, JSON 을 구문 분석 및 검증했으며, 허용되는 데이터를 정규화하고, 업스트림 오류를 예측 가능한 공개 오류로 변환했습니다. 로컬과 Cloudflare 에서 정상 요청 및 거부된 요청을 테스트하고, 두 배포의 소유권을 확인했으며, 임시 리소스를 삭제하고 VM 연결을 해제했습니다.

자세한 내용은 공식 Request API, Response API, Fetch API를 참조합니다.