요청과 응답 계약
180분 안팎
학습 목표
개발자 도구에서 요청·상태·JSON 계약을 확인합니다.
개념
정적 배열에서 요청 계약으로
행사 배열이 파일에 있으면 필드 이름과 실패 조건을 작성자가 통제합니다. 외부 응답은 서버가 새 버전을 배포하거나 장애가 발생할 때 달라질 수 있습니다. 이번 레슨은 요청을 보내기 전에 어떤 응답을 기대할지 정하고, 받은 값이 그 약속을 지키는지 확인하는 과정입니다. 화면을 고치기 전에 요청 URL·메서드·상태·응답 구조를 한 줄씩 기록하면 undefined를 화면에서 추적하는 시간을 줄일 수 있습니다.
이 모듈의 local 실습은 Node 18 이상과 Python 3을 사용합니다. zip을 풀고 해당 폴더에서 npm ci, npm test를 차례로 실행합니다. 외부 npm 패키지는 없습니다. assets/api.js의 TODO를 구현하며 테스트는 바꾸지 않습니다. npm start로 제공 서버를 실행하면 127.0.0.1:8000의 results.html을 확인합니다. 사용한 서버는 터미널에서 Ctrl+C로 종료합니다. 브라우저 실습은 표준 입력 JSON을 읽고 JSON 한 줄을 출력하며 DOM이나 실제 네트워크를 사용하지 않습니다.
미션 starter에는 m04 미션 solution의 문서·스타일·검색 함수·DOM 렌더러를 복사했습니다. 기존 results.js는 legacy-results.js로도 보존해 동기 탐색 회귀를 검사합니다. 새 화면은 api.js와 새 results.js를 사용합니다. 데이터는 같은 events.json에서 제공하지만 데이터 출처는 로컬 모의 서버입니다. API 연결의 책임을 데이터 계산이나 카드 생성과 섞지 않는 것이 이번 확장의 핵심입니다.
목록과 상세는 다른 응답입니다
목록 계약은 GET /mock/events?q=책®ion=busan입니다. q는 검색어, region은 all·seoul·busan 중 하나입니다. URL의 검색 조건은 URLSearchParams로 인코딩합니다. 검색어 안의 &나 #를 그대로 주소 문자열에 이어 붙이면 쿼리 구분자나 fragment로 해석되어 다른 조건이 전달될 수 있습니다. 출력 예제에서는 인코딩 결과보다 서버가 다시 읽은 값이 원문과 같은지 확인합니다.
목록 성공 응답은 행사 객체의 배열입니다. 각 객체는 양의 안전한 정수 id, 문자열 name, seoul 또는 busan인 region, YYYY-MM-DD 표기 date를 가집니다. 빈 배열도 계약을 지키는 성공 응답입니다. 200이라는 상태만으로 올바른 행사를 받았다고 결론 내리지 않습니다. 이번 검증은 날짜의 표기 형식까지 확인하며 실제 달력 날짜의 유효성 검증은 포함하지 않습니다. 이 범위를 README와 코드로 구분합니다.
상세는 GET /mock/events/3처럼 id를 경로에 넣고 행사 객체 하나를 받습니다. 배열로 감싸진 상세나 목록에 객체만 오는 응답은 contract 오류입니다. 없는 id에는 404와 오류 객체가 오므로 성공 행사의 필드 검사를 시도하기 전에 상태를 확인합니다. id가 없는 목록 경로와 id가 있지만 찾지 못한 상세 경로의 화면 의미가 서로 다릅니다. 상세를 빈 목록으로 처리하면 사용자가 잘못된 링크를 알아차리기 어렵습니다.
fetch가 완료됐다는 말의 범위
fetch의 Promise가 이행되면 Response를 얻습니다. HTTP 응답을 받았다는 뜻이며 애플리케이션 성공을 뜻하지 않습니다. 404나 500 응답에서도 Response를 받을 수 있으므로 response.ok를 확인합니다. ok는 200~299 범위의 상태를 나타냅니다. 이 실습 서버는 성공에 200만 제공하며, 본문 없는 204를 성공 데이터로 읽으려면 별도의 계약이 필요합니다. 상태 범위와 현재 프로젝트의 성공 형식을 구분합니다.
response.json()은 본문을 비동기로 읽고 JSON 값으로 파싱합니다. Content-Type에 application/json이 있는지 확인한 뒤 await로 파싱 완료를 기다립니다. JSON 문법이 맞아도 값이 숫자나 객체이면 목록 계약을 만족하지 않을 수 있습니다. 파싱 다음에 Array.isArray와 필드 타입 검사를 둡니다. 응답 본문은 한 번 읽는 흐름으로 유지하고 디버깅을 위해 json과 text를 연달아 읽지 않습니다.
API 오류는 ApiError의 kind와 status에 담습니다. 404는 not-found, 그 밖의 실패 응답은 http, 응답을 얻지 못한 요청 실패는 network, 형식 문제는 contract입니다. network의 status는 0으로 기록하지만 이것은 서버가 HTTP 0을 보냈다는 의미가 아닙니다. Response가 없다는 실습 내부 표기입니다. 화면 문구와 진단용 분류를 분리하면 서버의 상세 오류 문자열을 사용자에게 그대로 노출하지 않을 수 있습니다.
개발자 도구에서 근거를 모읍니다
브라우저 Network를 연 뒤 검색을 제출합니다. 요청 한 건을 선택해 Request URL과 Method, Status, Response Headers, Response 본문을 순서대로 읽습니다. Query String Parameters에서 검색어와 지역을 확인합니다. 500을 재현할 때는 results.html?mode=500을 열고 요청에도 mode=500이 포함됐는지 확인합니다. 본문이 오류 객체여도 목록 데이터로 표시되지 않아야 합니다. 눈에 보이는 문구와 Network 근거를 함께 기록합니다.
연결 실패는 results.html?mode=network에서 서버가 소켓을 닫도록 제공했습니다. 이때 앱은 연결 안내와 재시도 행동을 보여 줍니다. 브라우저 Console의 TypeError: Failed to fetch 또는 다른 런타임의 fetch failed만으로 서버 다운을 단정하지 않습니다. 주소 오류·연결 문제·브라우저 정책 등 여러 원인이 같은 상위 오류로 보일 수 있습니다. HTTP 상태를 실제로 받았는지부터 확인하고 개발자 도구의 상세 이유를 읽습니다.
CORS는 다른 출처의 응답을 브라우저 스크립트가 읽도록 서버가 허용하는 경계입니다. 출처에는 스킴·호스트·포트가 들어갑니다. 127.0.0.1 페이지에서 localhost 주소로 요청하면 같은 맥이더라도 호스트가 달라집니다. 제공 /cors.json에는 허용 헤더가 없으므로 브라우저 Console의 CORS 사유를 관찰할 수 있습니다. Node fetch는 이 브라우저 제한을 강제하지 않으므로 Node 테스트로 CORS 성공을 증명할 수 없습니다.
실패를 가리지 않고 수정합니다
mode: no-cors를 넣어 오류를 숨기는 것은 JSON API 읽기 해결책이 아닙니다. 불투명 응답은 기대한 본문 읽기를 제공하지 않습니다. 이 프로젝트에서는 화면과 API를 같은 출처로 제공하고 CORS 관찰 경로만 호스트를 바꿉니다. 실제 서비스에서는 서버 측 허용 출처나 같은 출처 프록시 구성을 담당자와 확인합니다. 허용 헤더를 요청에 임의로 추가해 서버의 응답 정책을 바꿀 수는 없습니다.
Unexpected token 오류가 나면 응답의 첫 글자와 Content-Type을 확인합니다. HTML 오류 페이지를 JSON으로 읽었을 수 있습니다. Cannot read properties of undefined가 나면 json을 기다렸는지와 name 필드 계약을 확인합니다. 테스트의 contract 기대 실패는 상태·본문 타입·필드 중 어디가 틀렸는지 작은 입력으로 좁힙니다. 제목을 innerHTML로 넣어 테스트를 우회하지 않고 앞 모듈의 textContent 경계를 그대로 유지합니다.
요청 계약의 근거는 MDN Fetch 안내와 MDN CORS 안내에서 확인합니다. 서재의 fetch와 JSON 장은 다양한 사용법을 더 읽는 경로입니다. 이 레슨 제출물은 성공·빈 배열·404·500·연결 실패 계약표와 브라우저 CORS 관찰 기록입니다. 실행하지 못한 관찰은 미확인으로 표시합니다.
따라하기
URL 조건을 보존합니다
contract.cjs에 다음 코드를 저장하고 node contract.cjs로 실행합니다. 인코딩 뒤 서버가 읽을 조건을 출력합니다.
const u=new URL('/mock/events','http://127.0.0.1:8000');u.searchParams.set('q','책 & 모임');u.searchParams.set('region','busan');console.log(u.searchParams.get('q'));console.log(u.searchParams.get('region'));실행 결과
책 & 모임 busan
상태와 본문을 따로 확인합니다
Response를 사용한 네트워크 없는 예제입니다. 404도 Response 객체이며 ok는 false입니다.
(async()=>{for(const status of [200,404,500]){const r=new Response('[]',{status,headers:{'content-type':'application/json'}});console.log(status,r.ok,Array.isArray(await r.json()));}})();실행 결과
200 true true 404 false true 500 false true
잘못된 필드 계약을 찾습니다
id를 문자열로 받은 상황과 숫자로 받은 상황을 비교합니다. 타입을 임의 변환하기 전에 서버 계약을 확인합니다.
for(const e of [{id:3,name:'책 모임'},{id:'3',name:'책 모임'}])console.log(Number.isSafeInteger(e.id),typeof e.name);실행 결과
true string false string
API 경계 TODO와 실제 요청을 확인합니다
local 실습의 assets/api.js에서 상태 검사 TODO를 완성하고 npm test로 확인합니다. 제공 request의 나머지 검사 순서를 읽습니다. 브라우저가 가능한 환경에서 npm start 후 README의 Network·CORS 절차를 수행합니다. 이 작성 환경은 서버 바인딩 EPERM으로 실제 HTTP와 CORS 관찰은 미확인입니다. 화면 캡처 대신 URL·상태·응답 유형·Console 이유를 기록해도 됩니다.
확인 문제
실습
assets/api.js request의 HTTP 상태 검사 TODO를 구현합니다. 404는 not-found, 그 밖의 실패 HTTP는 http로 던지며 상태를 기록합니다. 자동 검사 뒤 README의 실제 브라우저 CORS 기록을 제출합니다.
실행 명령
npm test
기대 결과
기존 회귀 검사와 API 7개 모두 통과합니다. starter는 TODO 관련 assertion 실패, solution은 실패 0개입니다.
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 로딩·오류·빈 결과 화면을 나눈 이유를 설명합니다.