콘텐츠로 이동

fetch API — 프론트엔드와 백엔드 연결하기

이제 진짜 연결할 차례

11장에서 만든 Todo 앱은 데이터를 브라우저 메모리에만 저장했습니다.
새로고침하면 사라집니다.

이제 7~8장에서 만든 FastAPI 서버와 연결해서 데이터를 저장하겠습니다.

브라우저 (JavaScript)          FastAPI 서버
       │                            │
       │  fetch("/todos")  ────────▶│
       │                            │  SELECT * FROM todos
       │  [{id:1, ...}, ...]  ◀────│
       │                            │

비동기(Async)란?

HTTP 요청은 시간이 걸립니다. 서버가 응답할 때까지 기다려야 합니다.

JavaScript는 기다리는 동안 다른 작업을 계속 합니다 — 이를 비동기라고 합니다.

// 동기 (Synchronous) — 순서대로 실행, 앞이 끝날 때까지 대기
const data = fetchData();    // 1초 걸림
console.log(data);           // 1초 후 실행

// 비동기 (Asynchronous) — 기다리지 않고 다음 줄 실행
fetchData().then((data) => {
  console.log(data);         // 나중에 실행
});
console.log("이 줄이 먼저 실행됨");

async / await — 비동기를 동기처럼 쓰기

// then 방식 (구형)
fetch("/todos")
  .then((response) => response.json())
  .then((data) => console.log(data));

// async/await 방식 (현대적)
async function loadTodos() {
  const response = await fetch("/todos");
  const data = await response.json();
  console.log(data);
}

awaitasync 함수 안에서만 사용할 수 있습니다.
await를 만나면 그 결과가 올 때까지 이 함수만 잠시 대기합니다.

fetch API 기본 사용법

GET — 데이터 가져오기

async function getTodos() {
  const response = await fetch("http://localhost:8000/todos");
  const todos = await response.json();
  return todos;
}

// 사용
const todos = await getTodos();
console.log(todos);
// [{id: 1, title: "운동하기", is_done: false}, ...]

POST — 데이터 보내기

async function createTodo(title) {
  const response = await fetch("http://localhost:8000/todos", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ title }),
  });
  const newTodo = await response.json();
  return newTodo;
}

PATCH — 수정

async function toggleTodo(id, isDone) {
  const response = await fetch(`http://localhost:8000/todos/${id}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ is_done: isDone }),
  });
  return response.json();
}

DELETE — 삭제

async function deleteTodo(id) {
  await fetch(`http://localhost:8000/todos/${id}`, {
    method: "DELETE",
  });
}

인증 헤더 포함하기

8장에서 만든 JWT 인증이 있다면, 요청에 토큰을 포함해야 합니다.

// 로그인 후 토큰 저장
async function login(email, password) {
  const response = await fetch("http://localhost:8000/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });
  const data = await response.json();
  localStorage.setItem("token", data.access_token);
}

// 인증이 필요한 요청에 토큰 포함
async function getTodos() {
  const token = localStorage.getItem("token");
  const response = await fetch("http://localhost:8000/todos", {
    headers: {
      "Authorization": `Bearer ${token}`,
    },
  });
  return response.json();
}

에러 처리

async function getTodos() {
  try {
    const response = await fetch("http://localhost:8000/todos");

    if (!response.ok) {
      // 4xx, 5xx 응답
      const error = await response.json();
      throw new Error(error.detail || "요청 실패");
    }

    return response.json();
  } catch (err) {
    console.error("오류:", err.message);
    alert("데이터를 불러오지 못했습니다.");
    return [];
  }
}

fetch()는 네트워크 오류일 때만 catch로 넘어갑니다.
404, 500 같은 HTTP 오류는 response.ok로 직접 확인해야 합니다.

CORS 문제

프론트엔드가 http://localhost:5500이고 백엔드가 http://localhost:8000이면,
브라우저가 보안상 요청을 차단합니다.

Access to fetch at 'http://localhost:8000/todos' from origin 
'http://localhost:5500' has been blocked by CORS policy

8장에서 설명한 대로 FastAPI에서 CORS를 허용해야 합니다.

# main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5500"],  # 개발용
    allow_methods=["*"],
    allow_headers=["*"],
)

Todo 앱 — 백엔드 연동 완성

// app.js — 백엔드와 연결된 버전
const API = "http://localhost:8000";

const form = document.getElementById("todo-form");
const input = document.getElementById("title-input");
const list = document.getElementById("todo-list");

// 목록 불러오기
async function loadTodos() {
  const todos = await fetch(`${API}/todos`).then((r) => r.json());
  renderTodos(todos);
}

// 화면에 그리기
function renderTodos(todos) {
  list.innerHTML = "";
  todos.forEach((todo) => {
    const item = document.createElement("div");
    item.className = "todo-item" + (todo.is_done ? " done" : "");

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.checked = todo.is_done;
    checkbox.addEventListener("change", async () => {
      await fetch(`${API}/todos/${todo.id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ is_done: !todo.is_done }),
      });
      loadTodos();  // 목록 새로고침
    });

    const span = document.createElement("span");
    span.textContent = todo.title;

    const delBtn = document.createElement("button");
    delBtn.className = "delete-btn";
    delBtn.textContent = "삭제";
    delBtn.addEventListener("click", async () => {
      await fetch(`${API}/todos/${todo.id}`, { method: "DELETE" });
      loadTodos();
    });

    item.appendChild(checkbox);
    item.appendChild(span);
    item.appendChild(delBtn);
    list.appendChild(item);
  });
}

// 할 일 추가
form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const title = input.value.trim();
  if (!title) return;

  await fetch(`${API}/todos`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title }),
  });

  input.value = "";
  loadTodos();
});

// 시작 시 목록 로드
loadTodos();

로딩 상태 표시

네트워크 요청 중에는 사용자에게 피드백을 줘야 합니다.

async function loadTodos() {
  list.innerHTML = "<p>불러오는 중...</p>";

  try {
    const todos = await fetch(`${API}/todos`).then((r) => r.json());
    renderTodos(todos);
  } catch {
    list.innerHTML = "<p>오류가 발생했습니다. 다시 시도해 주세요.</p>";
  }
}

전체 파일 구조

frontend/
├── index.html     ← HTML 구조
├── style.css      ← 스타일
└── app.js         ← API 호출 + DOM 조작

backend/
├── main.py
├── database.py
└── auth.py

브라우저에서 index.html을 열면 백엔드 서버와 통신하며 동작합니다.

Network 탭으로 요청 확인

F12 → Network 탭
→ "Fetch/XHR" 필터 선택
→ 버튼을 클릭하면 요청이 목록에 표시됨
→ 클릭하면 요청/응답 헤더와 본문 확인 가능

디버깅할 때 가장 유용한 탭입니다.

실습 미션

미션 1: GET 연결

1. FastAPI 서버를 실행하세요 (uvicorn main:app --reload).
2. fetch로 GET /todos를 호출하고 결과를 console.log로 출력하세요.
3. 받아온 데이터로 화면을 렌더링하세요.

미션 2: 추가 연결

폼 제출 시 POST /todos를 호출하고,
성공 후 목록을 새로 불러오세요.

미션 3: 완료/삭제 연결

체크박스 변경 시 PATCH /todos/{id}를 호출하세요.
삭제 버튼 클릭 시 DELETE /todos/{id}를 호출하세요.
각각 성공 후 목록을 새로 불러오세요.

미션 4 (심화): 로그인 화면

1. 로그인 폼(이메일, 비밀번호)을 만드세요.
2. POST /auth/login 을 호출해 토큰을 받아 localStorage에 저장하세요.
3. 토큰이 있으면 Todo 화면을, 없으면 로그인 화면을 보여주세요.
4. 모든 API 요청에 Authorization 헤더를 포함하세요.

핵심 요약

개념 설명
비동기 요청 결과를 기다리는 동안 다른 작업 가능
async/await 비동기 코드를 동기처럼 읽기 쉽게 작성
fetch() 브라우저 내장 HTTP 요청 함수
response.ok 2xx 성공 여부 확인
response.json() 응답 본문을 JavaScript 객체로 파싱
Authorization 헤더 JWT 토큰 전달 방법
CORS 다른 출처 간 요청 허용 설정

fetchcatch는 네트워크 오류만 잡습니다.
404나 500 같은 HTTP 오류는 response.ok로 별도로 확인하세요.