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);
}
await는 async 함수 안에서만 사용할 수 있습니다.
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 | 다른 출처 간 요청 허용 설정 |
fetch의catch는 네트워크 오류만 잡습니다.
404나 500 같은 HTTP 오류는response.ok로 별도로 확인하세요.