템플릿팅(Templating)이란 API가 보낸 다양한 형식의 데이터를 화면에 표시하는 프로세스다.
템플릿은 웹 애플리케이션 상의 프론트엔드 컴포넌트처럼 처리된다.
Jinja는 파이썬으로 작성된 템플릿팅 언어로, API 응답을 쉽게 렌더링할 수 있게 해준다.
Jinja는 파이썬으로 작성된 템플릿팅 엔진으로, API 응답을 쉽게 렌더링할 수 있도록 한다. 모든 템플릿팅 언어에는 변수가 사용되며 템플릿이 렌더링될 때 이 변수가 실제 값으로 변환된다. 이 외에도 태그를 사용해 템플릿 로직을 제어한다.
Jinja는 중괄호 {}를 사용해서 템플릿 파일의 일반적인 HTML, 텍스트 등을 표현식 및 구문과 구분한다. {{}} 구문을 변수 블록이라고 하며 이 안에 변수를 저장한다.
{% %}는 if/else, 반복, 매크로 같은 구조를 제어할 때 사용된다.
Jinja 언어에서 자주 사용되는 구문은 다음과 같다.
Jinja는 문자열로 변환 가능한 모든 파이썬 유형 또는 객체를 템플릿 변수로 사용할 수 있다. 모델, 리스트, 딕셔너리 유형을 템플릿에 전달해서 값이나 속성을 사용할 수 있는데, 이때 {{}} 구문이 사용된다.
필터는 모든 템플릿팅 엔진에서 가장 중요한 요소이며 특정 함수를 실행할 수 있게 해준다. 예를 들어 리스트의 값들을 병합하는 함수나 객체의 길이를 추출하는 함수 등을 사용할 수 있다.
파이썬과 Jinja는 유사한 구문을 사용하지만 문자열 병합이나 첫 문자를 대문자로 변환하기 등 파이썬의 문자열 수정 구문은 Jinja에서 사용할 수 없다. 따라서 이런 수정 작업은 Jinja의 필터 기능을 사용해야 한다.
필터는 파이프 기호(¦)를 사용해서 변수와 구분하며 괄호를 사용해 선택적 인수를 지정한다.
{{ variable ¦ filter_name(*args) }}
인수가 없다면:
{{ variable ¦filter_name }}
자주 사용되는 필터:
기본 필터: 전달된 값이 None일 때(값이 없을 때) 사용할 값을 지정
{{todo.item ¦ default('이것은 기본 todo 아이템입니다.'}}
이스케이프 필터: HTML을 변환하지 않고 그대로 렌더링한다
{{ "<
title>
Todo Application</
title>
¦ escape }}
<
title>
Todo Application</
title>
변환 필터: 데이터 유형을 변환
{{ 3.142 ¦ int }}
3
{{ 31 ¦ float }}
31.0
병합 필터: 리스트 내의 요소들을 병합해서 하나의 문자열로 만든다
{{ ['한빛미디어는', '훌륭한', '책을', '만든다.'] ¦ join(' ')}}
한빛미디어는 훌륭한 책을 만든다.
길이 필터: 전달된 객체의 길이를 반환. 파이썬의 len()과 같은 역할
Todo count: {{ todos ¦ length }}
Todo count: 4
Jinja의 if문은 파이썬과 사용법이 유사하며 {% %} 제어 블록 내에서 사용할 수 있다.
{% if todo ¦ length < 5 %}
할 일 목록에 할 일이 많지 않네요.
{% else %}
바쁜 날을 보내고 있군요!
{% endif %}
{% for todo in tods %}
<b> {{ todo.item }} </b>
{% endfor %}
반복문 내에서 특수한 변수를 사용할 수도 있다.
반복문에서 사용되는 특수 변수:
Jinja의 매크로는 하나의 함수로, HTML 문자열을 반환한다.
주요 목적은 하나의 함수를 사용해 반복적으로 작성하는 코드를 줄이는 것이다.
예를 들어 입력(input) 매크로를 정의해서 HTML 폼에 반복적으로 정의하는 입력 태그를 줄일 수 있다.
{% macro input(name, value='', type='text', size= 20 %}
<div class="form">
<input type="{{ type }}" name="{{ name }}"
value="{{value¦escape }}" size="{{ size }}">
</div>
{% endmacro %}
이 매크로를 호출해서 폼에 사용할 입력 요소를 간단하게 받을 수 있다.
{{ input('item') }}
이것은 다음과 같은 HTML을 반환한다.
<div class="form">
<input type="text" name="item" value="" size="20">
</div>
이 기능은 중복 배제(DRY) 원칙에 근거한 것이며 큰 규모의 웹 애플리케이션을 개발할 때 많은 도움이 된다. 템플릿 상속은 기본 템플릿을 정의한 다음 이 템플릿을 자식 템플릿이 상속하거나 교체해서 사용할 수 있게 한다.
Jinja2 패키지를 설치하고 기존 작업 디렉터리에 template라는 신규 폴더를 만들어야 한다. 이 폴더에 모든 Jinja 관련 파일(Jinja 구문이 섞여 있는 HTML 파일)이 저장된다.
CSS 부트스트랩 라이브러리 활용
부트스트랩 라이브러리는 페이지 로딩 시 CDN(온라인 상의 라이브러리)에서 다운로드하지만 추가 파일은 별도의 폴더에 저장할 수 있다.
pip install jinja2 python-multipart
mkdir templates
cd templates
touch {home, todo}.html
from fastapi import APIRouter, Path, HTTPException, status, Request, Depends
from fastapi.templating import Jinja2Templates
...
templates = Jinja2Templates(directory="templates/")
@todo_router.post("/todo", status_code=201)
async def add_todo(request: Request, todo: Todo = Depends(Todo.as_form)):
todo.id = len(todo_list) + 1
todo_list.append(todo)
return templates.TemplateResponse("todo.html",
{
"request": request,
"todos": todo_list
})
@todo_router.get("/todo", response_model=TodoItems)
async def retireve_todos(request: Request):
return templates.TemplateResponse("todo.html",
{
"request": request,
"todos": todo_list
}
)
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(request: Request,todo_id: int = Path(..., title="The ID of the todo to retrieve.")) -> dict:
for todo in todo_list:
if todo.id == todo.id:
return templates.TemplateResponse(
"todo.html",
{
"request": request,
"todo": todo
}
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo with supplied ID doesn't exist",
)
Jinja가 template 폴더를 참조해서 그 안에 있는 특정 템플릿을 사용하도록 지정한다.
템플릿은 templates.TemplateResponse() 메서드를 통해 전달된다.
todo를 추가하는 POST 메서드는 의존성을 사용해서 입력값을 전달한다.
from pydantic import BaseModel
from typing import List, Optional
from fastapi import Form
...
class Todo(BaseModel):
id: Optional[int]
item: str
@classmethod
def as_form(
cls,
item: str = Form(...)
):
return cls(item=item)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=devicewidth, initial-scale=1.0">
<title>Packt Todo Application</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1Fw05qRGvFX0dJZ4" crossorigin="anonymous">
<link rel="stylesheet" href="https://use/fontawesome.com/releases/v5.0.10/css/all.css" integrity="sha384-+d0P83n9kaQMCwj8F4RJB66tzIwOKmrdb46+porD/OvrJ+37WqIM7UoBtwHO6Nlg" crossorigin="anonymous">
</head>
<body>
<header>
<nav class="navar">
<div class="container-fluid">
<center>
<h1>Packt Todo Appkication</h1>
</center>
</div>
</nav>
</header>
<div class="container-fluid">
{% bolck todo_container %}{% endblock %} // 자식 템플릿에서 정의된 todo_container를 사용한다는 의미. todo_container 블록을 가지고 있는 자식 템플릿의 콘텐츠가 여기에 표시된다.
</div>
</body>
</html>
source venv/bin/activate
uvicorn api:app --host=127.0.0.1 --port 8000 --reload
{% extends "home.html" %}
{% bolck todo_container %}
<main class="container">
<hr>
<section class="container-fluid"?
<form method="post">
<div class="col-auto">
<div class="input-group mb-3">
<input aria-describedby="button-addon2" aria-label="Add a todo" class="form-control" name="item"
placeholder="Purchase Packt's Python workshop course" type="text" value="{{ item }}" />
<button class="btn btn-outline-primary" data-mdb-ripple-color="dark" id="button-addon2" type="submit">
Add Todo
</button>
</div>
</div>
</form>
</section>
{% if todo %} // todo 변수가 전달되는지 확인
<article class="card container-fluid">
<br />
<h4>Todo ID: {{ todo.id }}</h4>
<p>
<strong>
Item: {{ todo.item }}
</strong>
</p>
</article>
{% else %} // todo 변수가 없으면
<section class="container-fluid">
<h2 align="center">Todos</h2>
<br>
<div class="card">
<ul class="list-group list-group-flush">
{% for todo in todos %}
<li class="list-group-item">
{{ loop.index }}. <a href="/todo/{{ loop.index }}">
{{ todo.item }}
</a>
</li>
{% endfor %}
</ul>
</div>
</section>
</main>
{% endblock %}
todo 템플릿이 home 템플릿을 상속한다. 또한 todo_container 블록을 정의해서 부모 템플릿(home 템플릿)이 이 템플릿의 콘텐츠를 표시할 수 있게 한다.
todo 템플릿은 모든 todo를 추출하는 라우트와 단일 todo 추출하는 라우트 모두에서 사용된다. 결과적으로 라우트에 따라 다른 콘텐츠를 렌더링하게 된다.