목록으로

Jinja2 템플릿과 htmx을 사용해 로그인 페이지 구현

시리즈, 입문
2024. 11. 13. PM 2:44:14

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 템플릿 안에서 템플릿 이름을 가리키면 해당 이름을 가진 템플릿 파일을 찾을 경로를 FileSystemLoadersearchpath인자로 전달해 지정했어요. 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 객체인 requesturl_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으로 간주해요.
우리가 구현한 로그인 종단점 함수는 이와 같이 사용자 전송 적재물을 받으므로 usernamepassword를 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 디렉터리 안에 있는 거죠.
템플릿 파일을 보면 {% block content %}{% endblock content %} 구문이 있는데, login.jinja2 템플릿에서 {% block content%}{% endblock content %} 태그 사이에 있는 내용이 layout.jinja2의 블록에 치환되어 들어가는 거예요.
이제 웹브라우저에서 로그인 페이지를 새로 고침한 후 로그인을 시도하면 정상 동작합니다.
여기까지 진행한 코드 커밋 : 4608895f

(4) htmx를 이용해 페이지 이동 시키기

로그인을 하면 아무 내용이 없는 빈 페이지가 나와요. 정상 동작이예요. 왜냐하면 로그인을 수행하는 login() 종단점 함수는 AuthenticationBackend 객체의 login()이 반환하는 Response 객체를 그대로 받아서 반환하는데요. 우리가 사용하는 CookieTransport는 HTTP Response에 쿠키를 심는 처리만 하고, 아무 내용이 없이 HTTP Status 204만 담거든요. HTTP Status 204는 No Content, 즉 본문이 없을 때 사용하죠. 본문만 없을 뿐 HTTP Header는 사용하는데, 서버가 HTTP 응답을 할 때 htmx가 참조할 정보를 HTTP Header에 실을 수 있어요.
우리는 로그인을 성공하면 할 일 그룹 목록 페이지로 이동(redirect)시키도록 해볼게요.
login()에 두 줄이 추가됐어요. 먼저 HTTP Request 객체를 FastAPI가 인자로 주입하도록 선언했어요. request 객체는 URL 이름으로 URL 주소를 가져오는 데 필요해요. /todo-groups라는 URL 경로가 아니라 list-todo-group-page라는 URL 이름을 사용하는 거죠. 앞서 템플릿에서 url_for라는 템플릿 태그를 사용한 것 기억하시죠? 그 템플릿 태그가 Request 객체의 url_for() 메서드를 사용한다고 했는데, Python 코드로써(request.url_for()) 사용했어요. 근데 이 메서드가 반환하는 객체는 URL 객체이기 때문에 문자열로 변환해서 사용했어요.
Response 객체에 있는 headers는 HTTP Response Header를 다루는 속성인데요. dict처럼 생긴 MutableHeaders 객체예요. 키는 HTTP Header의 키가 되고, 값은 해당 헤더 키의 값이 되지요. HTTP 1.1은 순 텍스트(Plain text)로 데이터를 다루니 request.url_for()가 반환한 URL 객체를 문자열로 변환해 값으로 사용한 거죠.
HX- 접두사가 붙은 HTTP Response Header는 htmx가 참조하는데요. HX-Redirect는 이름에서 유추되듯이 htmx가 사용자를 지정한 URL로 이동(redirect)시키는 데 사용해요. 정리하면 "list-todo-group-page"라 이름 붙은 URL을 문자열로 변환해 HX-Redirect 헤더에 담아서 클라이언트(웹 브라우저)에 전송하는 거예요.
htmx는 서버로부터 받은 HTTP Response에서 htmx 참조 헤더를 찾아보는데, HX-Redirect가 있으니 사용자의 웹브라우저에서 지정한 URL로 이동시켜요.

(5) 임시로 할 일 그룹 목록 페이지 구현

로그인을 성공하면 할 일 그룹 목록 페이지("list-todo-group-page")로 이동하는데, 아직 우린 이 종단점을 구현하지 않았어요. 로그인 성공하는 걸 확인하기 위해 임시로 이 종단점을 후다닥 구현할게요.
pudding_todo/apps/todo/endpoints.py 파일에 list_todo_group() 함수(코루틴)를 작성할게요.
그 다음 pudding_todo/apps/todo/templates/pages 경로에 todo-group-list.jinja2 템플릿 파일도 작성하고요.
현재 로그인한 사용자를 템플릿 변수로 전달하고, 템플릿에서는 해당 사용자의 계정명(username)을 출력하는 거지요.
여기까지 진행한 코드 커밋 : 970abb0
토이스토리 2기 모집 중!
푸딩캠프 뉴스레터를 구독하면 학습과 성장, 기술에 관해 요약된 컨텐츠를 매주 편하게 받아보실 수 있습니다.
목차