API 응답이 왔는데 왜 화면은 실패할까? HTTP·JSON·재시도 구분 — 바이브 코딩 4강

바이브 코더를 위한 기초 지식 · Season 1 · 4강

언어: 한국어 · English

API 호출은 버튼에서 서버로 요청을 보내고, 서버의 응답을 해석하는 일련의 계약이에요. 화면에 아무것도 나오지 않을 때는 요청 자체가 실패했는지, 서버가 오류로 응답했는지, 받은 JSON이 잘못됐는지를 나눠 봐야 해요. 이 세 문제를 같은 ‘로딩 오류’로 취급하면 AI도 엉뚱한 곳을 고치기 쉬워요.

지난 3강의 로그인에서 서버가 유효성을 판단한다는 걸 배웠어요. 이번 핵심 질문은 “API 응답이 왔는데 왜 화면은 실패할까요?”예요. 가상의 좌석 수를 읽는 작고 안전한 실습으로 HTTP, JSON, 대기·실패·재시도를 구분해 볼게요.

1. API·HTTP·JSON은 무엇이고, 무엇이 아닐까요?

  • API: 서로 다른 프로그램이 어떤 요청을 보내고 어떤 결과를 받을지 정한 인터페이스예요. URL만이 아니라 입력·출력·에러 조건을 함께 봐야 해요.
  • HTTP request: 브라우저가 보내는 method(GET 등), URL, headers, 선택적 body예요. 이번 실습의 GET /api/seats에는 요청 body가 없어요.
  • HTTP response: 서버가 돌려주는 status(200, 503 등), headers와 선택적 body예요. 200이어도 body가 프로그램이 기대한 형태라는 보장은 없어요.
  • JSON: {"available":2} 같은 구조화된 데이터의 표기 형식이에요. DB도 아니고 네트워크 프로토콜도 아니에요. 응답에서 JSON을 읽는 response.json()도 실패할 수 있어요.
  • fetch(): HTTP가 404나 503으로 답해도 보통 Response를 돌려줘요. 따라서 response.ok와 데이터 형태를 각각 검사해야 해요.

판단 규칙: 응답을 받았는가 → HTTP 상태가 정상인가 → body를 해석할 수 있는가 → 데이터가 약속한 형태인가를 따로 확인하세요. 이 순서는 AI가 만든 API 코드의 검수 기준으로도 쓸 수 있어요.

2. 머릿속 구조도: 버튼과 데이터 사이의 네 경계

  1. 화면: 사용자가 ‘Check seats’를 누르면 먼저 Loading... 상태를 표시해요.
  2. 브라우저 → 서버: GET /api/seats?case=ok 요청을 보내요.
  3. 서버 → 브라우저: HTTP status·header·body를 돌려줘요. 200과 유효한 JSON은 별도의 검사예요.
  4. 브라우저 → 화면: response.ok, response.json(), available의 정수 여부를 통과해야 좌석 수를 보여줘요. 아니면 오류 상태로 남아요.
관찰된 단계진단 질문
요청이 전송되지 않음Network 탭에 요청이 보이나요? URL·접속·CORS 문제인가요?
HTTP 오류 응답Status가 404, 401, 503 중 무엇인가요?
본문 해석 실패JSON 문법과 Content-Type, 실제 body가 맞나요?
형태 불일치available 필드가 있는 정수인가요?

Network 탭에서 같은 경로가 보인다고 해도, 새로고침 404와 API 404는 원인이 다를 수 있어요. 주소 직접 접속만 404인 문제는 클리닉 2강의 SPA 라우팅 진단과 비교하세요.

3. 무료 로컬 실습: 네 가지 응답을 직접 만들어 보기

아래 가상의 좌석 수 2는 합성 데이터예요. 예약도 저장도 하지 않는 읽기 전용 학습용 서버입니다. 외부 API, 구독, 신용카드, 도메인, 실제 계정은 필요 없어요.

  1. Node.js를 무료로 설치하고 터미널에서 node -v를 확인해요.
  2. 아래 전체 코드를 server.js 파일 하나에 그대로 저장해요. 파일 확장자가 .txt로 바뀌지 않았는지 확인하세요.
  3. 파일이 있는 폴더에서 node server.js를 실행하고 브라우저에서 http://127.0.0.1:3200을 열어요. 이미 포트 3200을 사용 중이라면 먼저 그 프로세스를 확인하세요.
  4. 선택 상자에서 네 가지 상황을 바꿔 누르고 개발자 도구의 Network 탭에서 /api/seats의 Status·Response를 함께 확인해요.
// Synthetic read-only API lab. Local learning use only.
const http = require('node:http');

const page = `<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>API response lab</title>
<main style="font:1rem/1.6 system-ui;max-width:30rem;margin:3rem auto;padding:0 1rem">
  <h1>Fictional seat availability</h1>
  <label for="scenario">Server scenario</label>
  <select id="scenario">
    <option value="ok">200 / valid JSON</option>
    <option value="busy">503 / server unavailable</option>
    <option value="broken">200 / invalid JSON</option>
    <option value="slow">Slow response / timeout</option>
  </select>
  <button id="load">Check seats</button>
  <p id="result" role="status" aria-live="polite">Not requested yet.</p>
</main>
<script>
const button = document.querySelector('#load');
const result = document.querySelector('#result');
button.addEventListener('click', async () => {
  button.disabled = true;
  result.textContent = 'Loading...';
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 1500);
  try {
    const mode = document.querySelector('#scenario').value;
    const response = await fetch('/api/seats?case=' + mode, {
      signal: controller.signal
    });
    if (!response.ok) {
      result.textContent = 'HTTP ' + response.status + ': no seats shown';
      return;
    }
    const data = await response.json();
    if (!Number.isInteger(data.available)) {
      throw new Error('Unexpected response shape');
    }
    result.textContent = 'Available: ' + data.available;
  } catch (error) {
    result.textContent = error.name === 'AbortError'
      ? 'Timed out. The server outcome is unknown.'
      : error instanceof SyntaxError
        ? 'Invalid JSON from server.'
        : 'Network or data error: ' + error.message;
  } finally {
    clearTimeout(timer);
    button.disabled = false;
  }
});
</script></html>`;

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:3200');
  if (req.method === 'GET' && url.pathname === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    return res.end(page);
  }
  if (req.method === 'GET' && url.pathname === '/api/seats') {
    const mode = url.searchParams.get('case');
    const headers = { 'Content-Type': 'application/json; charset=utf-8',
      'Cache-Control': 'no-store' };
    if (mode === 'busy') {
      res.writeHead(503, { ...headers, 'Retry-After': '5' });
      return res.end(JSON.stringify({ error: 'temporarily_unavailable' }));
    }
    if (mode === 'broken') {
      res.writeHead(200, headers);
      return res.end('{"available":'); // Intentionally malformed JSON.
    }
    const send = () => {
      res.writeHead(200, headers);
      res.end(JSON.stringify({ available: 2 }));
    };
    if (mode === 'slow') return setTimeout(send, 2200);
    return send();
  }
  res.writeHead(404, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'not_found' }));
}).listen(3200, '127.0.0.1', () => {
  console.log('Open http://127.0.0.1:3200');
});

코드에서 서버는 127.0.0.1에만 열리고, 브라우저는 같은 origin의 서버로만 요청해요. busy는 의도적으로 503, broken은 의도적으로 잘못된 JSON, slow는 2.2초 후 정상 응답을 만들어요. 브라우저는 1.5초가 지나면 기다리던 요청을 취소해요. 취소했다고 서버 작업이 반드시 취소된 것은 아닙니다.

4. 정상·실패·예외를 어떻게 판정할까요?

선택·조건Network 관찰과 화면 결과
ok200 / 유효한 JSON
화면: Available: 2
busy503 / 에러 JSON
화면: HTTP 503: no seats shown
broken200 / 잘못된 JSON
화면: Invalid JSON from server.
slow서버는 약 2.2초, 클라이언트는 1.5초에 취소
화면: Timed out. The server outcome is unknown.
서버를 종료하고 기존 화면에서 다시 클릭연결 실패; 보통 HTTP 응답 자체가 없음
화면: Network or data error
주소창에서 /api/unknown 입력404
화면: JSON not_found 확인

화면의 Loading...은 요청 진행 중을 나타내는 UI 상태이지 HTTP status가 아니에요. 503은 서버가 일시적으로 요청을 처리할 수 없음을 뜻하며 이 실습의 Retry-After: 5는 응답 header일 뿐 자동 재시도 명령이 아니에요. 예제를 멈추려면 터미널에서 Ctrl+C를 누르세요.

5. 실제로 망가지는 다섯 가지 이유

  • ① fetch()의 완료를 업무 성공으로 착각: 404/503도 응답이 도착했으므로 response.ok를 건너뛰면 빈 데이터를 성공처럼 보여줘요.
  • ② JSON이라고 가정: 프록시의 HTML 오류 페이지, 잘못된 JSON, 빈 body를 바로 response.json()으로 읽으면 파싱 단계에서 실패해요.
  • ③ 데이터 형태가 달라짐: available 대신 remaining을 보내면 화면은 예상 값을 읽지 못해요. 응답 계약을 확인해야 해요.
  • ④ timeout을 실패 확정으로 오해: 브라우저가 기다리기를 중단했어도 서버는 처리를 끝냈을 수 있어요. 특히 결제나 생성 요청을 무작정 다시 보내면 중복될 수 있어요.
  • ⑤ HTTP·CORS·라우팅을 한꺼번에 고침: 같은 origin 실습은 CORS가 필요 없어요. 실제 다른 origin의 API라면 서버의 허용 정책이 필요하며 mode: 'no-cors'로 응답을 읽을 수 있게 되는 것은 아니에요.

6. AI에게 이렇게 요청하세요

나쁜 요청: “버튼 누르면 API에서 데이터 받아오게 해줘. 에러 나면 계속 재시도해.”

좋은 요청: “기존 API의 method·URL·요청/응답 JSON 계약을 먼저 확인해 줘. 조회 버튼에는 loading, 성공, HTTP 4xx/5xx, 네트워크 실패, JSON 파싱 실패, 데이터 형태 불일치, timeout을 각각 구분해 표시해 줘. response.ok 검사와 취소 시 정리 과정을 포함하고, DevTools Network에서 확인할 재현 절차를 제시해 줘. 자동 재시도는 하지 말고, 요청이 읽기 전용·멱등적이고 중복 부작용이 없다는 근거가 있을 때만 재시도 정책을 제안해 줘. 실제 토큰·고객 데이터는 사용하지 마.”

AI 결과 검수 질문: “503을 성공으로 표시하지 않나요? 200인데 JSON이 깨지면 어떻게 되나요? timeout 뒤 요청이 서버에서 실행됐는지 알 수 있나요?”

7. 재시도는 언제 안전할까요?

GET은 HTTP 의미상 읽기 전용(safe)·멱등적(idempotent) 요청이에요. 서버가 그 규칙을 지키는 좌석 조회라면 사용자가 원인을 확인한 뒤 다시 눌러도 새 예약을 만들지 않아요. 하지만 POST로 결제·예약을 생성하는 요청은 응답이 사라졌다고 실행되지 않은 것이 아니에요. 중복 방지 ID와 서버 검증 없이 자동 재시도하지 마세요. 재시도는 서버 과부하를 악화할 수도 있어요.

8. 되돌리기와 복구

  1. 실습 초기화: Ctrl+C로 종료하고 다시 node server.js를 실행해요. 서버는 저장하는 데이터가 없어서 DB 복구가 필요하지 않아요.
  2. 변경 전으로: AI가 코드를 고치기 전에 원본 server.js를 별도 복사해 두고, 문제가 생기면 비교 후 원본으로 돌아가세요. 다음 Git 강의에서 더 안전한 버전 관리 방법을 배워요.
  3. 실제 서비스: 서버 코드 수정 전 요청 주소·상태 코드·재현 시각과 진단에 필요한 응답 항목만 기록해요. 요청 주소와 응답을 포함한 모든 기록에서 토큰·쿠키·고객 정보는 가리거나 제외한 뒤 저장·공유하세요. 실패 범위를 확인한 뒤 문제가 된 최소 변경을 되돌리고, 이미 처리됐을 수 있는 쓰기는 별도로 조회하고, 실제 처리 결과와 기록이 일치하는지 확인해요.

9. 이 방법을 쓰지 말아야 할 때

정적 페이지에서 이미 가지고 있는 값만 보여주는 일이라면 API 서버부터 만들 필요는 없어요. 또 이 장의 로컬 HTTP 서버를 실제 예약·결제·로그인 API로 배포하면 안 돼요. 인증, 권한, 저장, 입력 제한, 트래픽 대응과 운영 로그가 빠져 있어요. UI만 빠르게 확인하고 싶다면 정해진 가상 데이터를 이용한 mock도 유용하지만, mock만 통과했다고 네트워크가 검증된 건 아니에요.

10. 복습 질문과 짧은 해설

  1. Q. fetch()가 값을 돌려주면 API 성공인가요? 아니에요. HTTP status와 JSON 문법, 데이터 형태를 차례로 확인해야 해요.
  2. Q. 200인데 화면이 실패할 수 있나요? 네. 이 실습의 broken처럼 JSON을 읽을 수 없으면 실패해요.
  3. Q. timeout이면 서버가 아무것도 하지 않았나요? 몰라요. 클라이언트가 응답을 못 받았다는 사실과 서버의 실행 결과는 달라요.
  4. Q. GET은 무조건 재시도해도 될까요? 아니에요. 의미상 읽기 전용이어야 하고 실제 서버 구현·부하·정책도 확인해야 해요.

11. 다음 강의로 이어가기

2강에서는 데이터의 소유 위치를, 3강에서는 서버의 로그인 판정을 배웠어요. API 응답을 읽을 수 있게 됐으니 다음 5강은 Database·Schema·ID·Migration이에요. 좌석 수가 실제로 남으려면 어디에 어떻게 기록해야 하는지 다뤄요. 관련 실전 진단은 클리닉 1강의 새로고침 후 데이터 소실을 참고하세요.

공식 자료


다른 글 보기 · 주제 탐색 · 작성자와 편집 기준 · 문의·정정 요청

RUDA DIRECTOR에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기