Jinja2 템플릿과 htmx을 사용해 로그인 페이지 구현
Jinja2 템플릿 사용
(1) Jinja2 소개
Jinja2는 유명한 템플릿 엔진이예요. Django 템플릿 문법과 비슷하지만 더 유연하고 표현력이 좋죠. Flask를 사용하셨다면 친숙하실 거예요. FastAPI에서 공식(?) 템플릿 엔진은 Jinja2인데, FastAPI의 애플리케이션 서버 축인 Starlette이 Jinja2 템플릿을 통합해(intgrated) 지원하거든요.
Jinja2는 비동기 렌더링을 지원하는데, 이 컨텐츠에서 다루기엔 범위를 벗어나므로 더 자세히 다루진 않을게요.
저는 Django를 사용하는 프로젝트에도 Django 템플릿 대신 Jinja2를 사용하는 편이예요. 🙂
(2) FastHX 소개
FastHX는 FastAPI에서 htmx를 편하게 사용하는 데 유용한 패키지예요. 그렇지만 htmx도 결국 서버측 렌더링(Server Side Rendering)을 기반으로 하기 때문에 자연스레 Jinja2를 사용하는 데 돠움을 줘요. htmx는 다음 편에서 살펴보기로 하고, 이번 편에서는 FastAPI에서 Jinja2를 편하게 다루는 도구로 사용할게요.
(3) 설정
SQLAlchemy가 그랬듯이, Jinja2도 FastAPI에서 사용하는 데 필요한 FastAPI용 설정이 따로 있진 않아요. 한 가지 조치를 더 취해주면 되는데, Jinja2 템플릿 인스턴스 객체를 만든 후 이 객체를 Starlette에서 제공하는 Jinja2 관련 템플릿 객체에서 사용할 수 있게 전달해주는 거죠. 설정해보죠!
먼저 패키지들을 설치하고요. Jinja 객체를 생성해요.
Jinja2 템플릿 안에서 템플릿 이름을 가리키면 해당 이름을 가진 템플릿 파일을 찾을 경로를 FileSystemLoader의 searchpath인자로 전달해 지정했어요. base_dir = Path()는 FastAPI 애플리케이션 서버를 구동한 경로를 뜻해요. 이 경로는 pudding_todo 디렉터리의 상위, 그러니까 pyproject.toml 파일이 있는 경로이고, 이 경로를 기준으로 템플릿 파일이 있는 경로를 지정한 거죠.
Jinja2Templates 클래스는 Starlette에서 제공하는 클래스인데, Jinja2 동작 환경 정보를(Environment) 전달하면 이 정보를 기반으로 템플릿을 렌더링해 Starlette의 Response 객체를 생성해 반환해주죠. FastAPI가 Starlette을 기반으로 하기도 하고, fastapi.templating.Jinja2Templates 자체가 starlette.templating.Jinja2Templates예요.
마지막으로 FastHX에 있는 Jinja 클래스에 Jinja2Templates 인스턴스 객체를 전달하며 객체를 생성했어요. 이 객체를 이용해 종단점(Endpoint) 함수에서 편리하게 템플릿 렌더링을 선언할 수 있어요.
로그인 페이지
(1) 로그인 페이지 템플릿
할 일 그룹과 할 일 종단점엔 로그인한 사용자만 접근할 수 있어요. 그러니 로그인 페이지를 먼저 구현할게요.
account 앱의 템플릿 경로는 apps/account/templates로 설정했으니 이 경로를 만들고, 그 안에 pages 디렉터리를 만들어요. 그리고 그 안에 login.jinja2 파일을 만듭니다.
url_for()는 템플릿에서 사용할 수 있는 함수로 Starlette의 Jinja2Templates 객체에서 주입해줘요. Request 객체인 request의 url_for() 메서드를 사용하여 URL 이름으로 URL 경로를 문자열로 생성해요.
템플릿을 만들었으니 로그인 종단점 함수를 만들어 이 템플릿을 사용해 화면을 출력할게요.
기존에 구현한 종단점 함수와 다른 점이 있어요. 바로 templates.py에 구현해 정의한 tpl 객체를 장식자(Decorator)로 사용해 사용할 템플릿 파일을 지정한 거죠. 이 기능은 FastHX의 Jinja 객체예요. 빈 사전형 객체를 반환하는데, 반환하는 이 사전형 객체를 템플릿의 컨텍스트, 간단히 말해서 템플릿 내 변수로 사용해요.
FastHX의 page 장식자를 사용하지 않고 Starlette의 Jinja2Templates 객체를 사용하면 다음과 같이 코드를 작성해요.
FastHX를 사용하는 게 조금 더 간결하고 편하죠.
(2) JSON 요청 처리
웹브라우저를 열고 http://localhost:8000/login URL에 접속해보면 로그인 화면이 나와요. Admin에서 생성한 계정의 계정명과 비밀번호를 입력하고 로그인하면, 예상치 못한 응답을 받아요. 바로 다음과 같은 JSON 응답을 받는 거죠.
우리는 HTML 폼 데이터로 전송하는데, FastAPI는 기본적으로 데이터의 컨텐츠 유형을 application/json으로 간주해요.
우리가 구현한 로그인 종단점 함수는 이와 같이 사용자 전송 적재물을 받으므로 username과 password를 JSON 형식으로 전송해야 해요. 물론 FastAPI도 Form데이터를 받을 순 있지만, 기존 LoginSchema를 그대로 사용하도록 하고, 그 대신 클라이언트에서 application/json 형식으로 데이터를 전송하도록 할게요. 바로 htmx를 이용해서요.
(3) htmx 적용하기
htmx에 대한 이야기는 지난 소식지에서 다룬 HTML 문서를 확장하다, htmx 컨텐츠로 갈음하고, 바로 로그인 페이지에 적용할게요.
코드가 조금 달라졌지요? htmx에 관한 부분은 HTML form 태그에 있는 속성 중 hx- 접두사가 붙은 속성들이예요. 폼에 submit 이벤트가 발생하면(hx-trigger) 지정한 URL로 HTTP POST 방식으로 데이터를 전송하는데(hx-post), 서버로부터 받은 응답 본문을 현 HTML 요소(element) 중 어느 것과도 바꿔치지 않겠다(hx-swap)는 뜻이지요. 더불어 데이터 전송 시 JSON 형식으로 인코딩하도록 htmx 확장기능을 사용(hx-ext)했고요.
{% extends %}는 템플릿을 확장하는 템플릿 태그예요. layout.jinja2에 템플릿 내용을 끼워넣어 확장하도록 했어요. layout.jinja2는 아직 작성하지 않았는데, pudding_todo/templates 디렉터리를 만들고, 이곳에 layout.jinja2 파일을 생성해 다음 코드를 작성하세요. login.jinja2 파일은 pudding_todo/apps/account/templates 디렉터리 안에 있고, layout.jinja2 파일은 pudding_todo/templates 디렉터리 안에 있는 거죠.