

위키백과 설명:
진자 (템플릿 엔진)
진자(Jinja)는 파이썬용 웹 템플릿 엔진이다. 아민 로나처가 개발하였으며 BSD 허가서로 라이선스된다. 진자는 장고 템플릿 엔진과 비슷하지만 파이썬과 비슷한 식을 제공하면서 템플릿이 샌드박스 안에서 평가되는 것을 보장한다. 텍스트 기반 템플릿 언어이기 때문에 마크업과 소스 코드를 모두 생성할 수 있다
FastAPI는 원래 API 서버를 만들 때 사용하지만,
간단한 웹 페이지 UI도 만들 수 있도록 HTMLResponse, Jinja2Templates 를 지원한다.
이 기능을 사용하면:
HTML 파일을 템플릿처럼 만들고
서버에서 변수 / 목록을 넘겨서
브라우저에서 화면을 만들어 주는 방식
즉, Flask + Jinja2와 거의 동일한 구조이다.
템플릿 엔진(Jinja2)은 HTML 안에서 다음을 사용할 수 있다.
{{ 변수명 }}
{% for item in items %}
{{ item }}
{% endfor %}
{% if items %}
{% endif %}
강의에서 가장 길게 설명한 부분이다.
기본 구조는 다음과 같다.
layout.html (부모 템플릿)
<html>
<head>
<title>기본 레이아웃</title>
</head>
<body>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>
home.html (자식 템플릿)
```<html>
{% extends "layout.html" %}
{% block content %}
<h1>홈 화면</h1>
{% endblock %}
</html>
작동 원리:
layout.html의 {% block content %} 부분을
home.html에서 작성한 내용이 덮어쓴다.
이렇게 하면 헤더/메뉴/푸터 같은 공통 UI는 반복해서 만들 필요가 없다.
FastAPI는 템플릿 엔진을 사용하기 위해 아래처럼 등록한다.
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
그리고 HTML을 반환할 때:
return templates.TemplateResponse(
"home.html",
{
"request": request,
"todos": todo_list
}
)
여기서 중요한 점:
request는 반드시 포함해야 한다
Jinja2가 FastAPI와 연결될 때 request 필요.
그래서 항상 context(dict) 안에 넣어야 한다.
React는 개발 단계에서는 둘이 포트를 나눠서 실행된다.
React 개발 서버: localhost:3000
FastAPI 서버: localhost:8000
개발 단계에서는 둘이 포트를 나눠서 실행된다.
React → FastAPI 요청을 보내면 FastAPI가 응답을 주고,
React가 화면에 렌더링한다.
클라이언트(React)
↓ fetch/axios
API(FastAPI)
↓ JSON 응답
React 화면 업데이트
FastAPI가 다음 JSON을 보냄:
{
"todos": [
{ "id": 1, "item": "first" },
{ "id": 2, "item": "second" }
]
}
React에서는:
setTodoList(response.data.todos);
그리고 화면에:
{todoList.map(t => (
<div>{t.item}</div>
))}
빌드 후 배포 구조 설명
강사가 강조한 실무 방식:
React는 "빌드" 해야 실제 웹사이트가 된다
npm run build
React는 그냥 일반 정적 HTML + JS로 웹서버에서 다운받고
FastAPI는 API 서버만 역할 담당.
개발 중에는 CORS 때문에 React → FastAPI가 막힐 수 있다.
그래서 FastAPI 쪽에 다음 설정을 추가:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
이걸 크로스 오리진 허용 이라고 한다.
app.jsx
import React, { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from "react-router-dom";
function MenuList() {
const [menus, setMenus] = useState([]);
useEffect(() => {
fetch("http://localhost:8000/menu")
.then((res) => res.json())
.then((data) => setMenus(data.menus))
.catch(err => console.error(err));
}, []);
const handleDelete = async (id) => {
await fetch(`http://localhost:8000/menu/${id}`, {
method: "DELETE",
});
setMenus(menus.filter((m) => m.id !== id));
};
return (
<div>
<h2>메뉴 리스트</h2>
{menus.length === 0 && <p>등록된 메뉴가 없습니다.</p>}
<ul>
{menus.map((m, index) => (
<li key={m.id} style={{ marginBottom: "10px" }}>
<strong>{index + 1}. {m.name}</strong> — {m.price}원
<Link to={`/edit/${m.id}`}>수정</Link>
<button onClick={() => handleDelete(m.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
function AddMenu() {
const [name, setName] = useState("");
const [price, setPrice] = useState("");
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
const newMenu = { name, price: Number(price) };
try {
await fetch("http://localhost:8000/menu", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newMenu),
});
navigate("/");
} catch (error) {
console.error("메뉴 추가 실패:", error);
}
};
return (
<form onSubmit={handleSubmit}>
<h2>메뉴 추가</h2>
<div>
<label>메뉴명 : </label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label>가격 : </label>
<input
type="number"
value={price}
onChange={(e) => setPrice(e.target.value)}
required
/>
</div>
<button type="submit">추가</button>
</form>
);
}
function EditMenu() {
const { id } = useParams();
const navigate = useNavigate();
const [menu, setMenu] = useState({
id: "",
name: "",
price: "",
});
useEffect(() => {
fetch(`http://localhost:8000/menu/${id}`)
.then((res) => res.json())
.then((data) => setMenu(data.menu));
}, [id]);
const handleSubmit = async (e) => {
e.preventDefault();
await fetch(`http://localhost:8000/menu/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(menu),
});
navigate("/");
};
return (
<form onSubmit={handleSubmit}>
<h2>메뉴 수정</h2>
<div>
<label>ID : </label>
<input value={menu.id} readOnly />
</div>
<div>
<label>이름 : </label>
<input
value={menu.name}
onChange={(e) =>
setMenu({ ...menu, name: e.target.value })
}
/>
</div>
<div>
<label>가격 : </label>
<input
type="number"
value={menu.price}
onChange={(e) =>
setMenu({ ...menu, price: Number(e.target.value) })
}
/>
</div>
<button type="submit">수정완료</button>
</form>
);
}
function App() {
return (
<BrowserRouter>
<div style={{ padding: "20px" }}>
<nav>
<Link to="/">메뉴 리스트</Link> |{" "}
<Link to="/add">메뉴 추가</Link>
</nav>
<hr />
<Routes>
<Route path="/" element={<MenuList />} />
<Route path="/add" element={<AddMenu />} />
<Route path="/edit/:id" element={<EditMenu />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from Menu import router
app = FastAPI()
origins = [
"http://localhost:5173",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(router)
Menu.py
from fastapi import APIRouter, HTTPException
from model import Menu
router = APIRouter()
menus = []
next_id = 1
@router.post("/menu")
async def add_menu(menu: Menu):
global next_id
# ID 자동 생성
menu.id = next_id
next_id += 1
menus.append(menu)
return {"message": "ok"}
@router.get("/menu")
async def get_all_menus():
return {"menus": [m.dict() for m in menus]}
@router.get("/menu/{menu_id}")
async def get_single_menu(menu_id: int):
for m in menus:
if m.id == menu_id:
return {"menu": m.dict()}
raise HTTPException(status_code=404, detail="Menu not found")
@router.put("/menu/{menu_id}")
async def update_menu(menu: Menu, menu_id: int):
for m in menus:
if m.id == menu_id:
m.name = menu.name
m.price = menu.price
return {"message": "Menu updated"}
raise HTTPException(status_code=404, detail="Menu not found")
@router.delete("/menu/{menu_id}")
async def delete_menu(menu_id: int):
for m in menus:
if m.id == menu_id:
menus.remove(m)
return {"message": "Menu deleted"}
raise HTTPException(status_code=404, detail="Menu not found")
model.py
from pydantic import BaseModel
class Menu(BaseModel):
id: int | None = None
name: str
price: int
메뉴 등록하고 메뉴리스트 삭제하기


메뉴 추가하는 기능

React는 BrowserRouter로 페이지를 구성하며, 총 세 가지 화면을 제공한다.
메뉴 리스트
메뉴 추가
메뉴 수정
메뉴 리스트는 서버에서 메뉴 데이터를 가져와 화면에 출력한다.
주요 기능
첫 렌더링 시 GET 요청으로 모든 메뉴 불러오기
삭제 버튼 클릭 시 DELETE 요청 수행
삭제 후 상태 배열에서 해당 항목 제거하여 화면 즉시 업데이트
GET 요청 코드
useEffect(() => {
fetch("http://localhost:8000/menu")
.then((res) => res.json())
.then((data) => setMenus(data.menus));
}, []);
삭제 코드
await fetch(`http://localhost:8000/menu/${id}`, { method: "DELETE" });
setMenus(menus.filter((m) => m.id !== id));
사용자가 입력한 데이터를 POST 요청으로 서버에 전달한다.
제출 시 처리 흐름
name, price 입력
POST 요청으로 서버에 전달
등록 후 메인 페이지로 이동
핵심 코드
await fetch("http://localhost:8000/menu", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newMenu),
});
navigate("/");
URL 파라미터에서 id를 받아 해당 메뉴 정보를 GET 요청으로 불러온 후 수정한다.
기능 흐름
useParams로 id 획득
GET /menu/id 요청으로 기본 데이터 로딩
입력 수정 후 PUT 요청 실행
수정 완료 후 메인으로 이동
PUT 요청 코드
await fetch(`http://localhost:8000/menu/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(menu),
});
FastAPI는 라우터를 사용해 CRUD를 분리하여 구성했다.
메뉴 데이터를 저장하는 리스트와 자동 증가 ID를 사용한다.
Pydantic 모델은 메뉴 데이터를 구조화하여 관리한다.