# Day18 Fast api -JINJA,React와 FastAPI 연동해서 사용하기

D0-$ANG ₩0N·2025년 12월 2일
post-thumbnail

0. JINJA


위키백과 설명:
진자 (템플릿 엔진)
진자(Jinja)는 파이썬용 웹 템플릿 엔진이다. 아민 로나처가 개발하였으며 BSD 허가서로 라이선스된다. 진자는 장고 템플릿 엔진과 비슷하지만 파이썬과 비슷한 식을 제공하면서 템플릿이 샌드박스 안에서 평가되는 것을 보장한다. 텍스트 기반 템플릿 언어이기 때문에 마크업과 소스 코드를 모두 생성할 수 있다

1. FastAPI에서 HTML을 렌더링하는 이유

FastAPI는 원래 API 서버를 만들 때 사용하지만,
간단한 웹 페이지 UI도 만들 수 있도록 HTMLResponse, Jinja2Templates 를 지원한다.
이 기능을 사용하면:

HTML 파일을 템플릿처럼 만들고

서버에서 변수 / 목록을 넘겨서

브라우저에서 화면을 만들어 주는 방식

즉, Flask + Jinja2와 거의 동일한 구조이다.

2. Jinja2 템플릿 시스템 핵심 원리

템플릿 엔진(Jinja2)은 HTML 안에서 다음을 사용할 수 있다.

2-1 변수를 출력

{{ 변수명 }}

2-2 반복문(루프)

{% for item in items %}
{{ item }}
{% endfor %}

2-3 조건문

{% if items %}
{% endif %}

2-4 템플릿 상속(extend)

강의에서 가장 길게 설명한 부분이다.

기본 구조는 다음과 같다.

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는 반복해서 만들 필요가 없다.

2-5. templates 폴더 등록 방식

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) 안에 넣어야 한다.

3.React와 FastAPI의 통신 구조 이해

React는 개발 단계에서는 둘이 포트를 나눠서 실행된다.

React 개발 서버: localhost:3000

FastAPI 서버: localhost:8000

개발 단계에서는 둘이 포트를 나눠서 실행된다.

AJAX 요청 구조

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 / API 개통

개발 중에는 CORS 때문에 React → FastAPI가 막힐 수 있다.

그래서 FastAPI 쪽에 다음 설정을 추가:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

이걸 크로스 오리진 허용 이라고 한다.

4.메뉴 수정 삭제 입력 과제

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}&nbsp;&nbsp;
                        <Link to={`/edit/${m.id}`}>수정</Link>
                        &nbsp;&nbsp;
                        <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

4-2. 결과

메뉴 등록하고 메뉴리스트 삭제하기

메뉴 추가하는 기능

4-3.분석

전체 기능 구조

React는 BrowserRouter로 페이지를 구성하며, 총 세 가지 화면을 제공한다.

메뉴 리스트

메뉴 추가

메뉴 수정

메뉴 리스트(MenuList) 설명

메뉴 리스트는 서버에서 메뉴 데이터를 가져와 화면에 출력한다.

주요 기능

첫 렌더링 시 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));

메뉴 추가(AddMenu) 설명

사용자가 입력한 데이터를 POST 요청으로 서버에 전달한다.

제출 시 처리 흐름

name, price 입력

POST 요청으로 서버에 전달

등록 후 메인 페이지로 이동

핵심 코드

await fetch("http://localhost:8000/menu", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(newMenu),
});
navigate("/");

메뉴 수정(EditMenu) 설명

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 백엔드 구성

FastAPI는 라우터를 사용해 CRUD를 분리하여 구성했다.

메뉴 데이터를 저장하는 리스트와 자동 증가 ID를 사용한다.

model.py

Pydantic 모델은 메뉴 데이터를 구조화하여 관리한다.

profile
Change Up

0개의 댓글