[Django] 장고 템플릿(Template)
- -
장고 템플릿(Template)
장고(Django)에서 템플릿은 웹 애플리케이션의 사용자 인터페이스를 구성하는 데 사용되는 파일이다. 템플릿은 HTML, CSS 및 일부 특수 구문을 포함하며, 동적으로 생성된 내용을 표시할 수 있도록 한다. 이를 통해 개발자는 서버에서 전달된 데이터를 템플릿에 적용하여 동적 웹 페이지를 생성할 수 있다. 장고의 템플릿 시스템은 Django의 모델, 뷰 컨트롤러(MVC) 아키텍처에서 뷰 부분에 해당한다. 템플릿은 뷰에서 전달된 데이터를 적절한 형식으로 표시하고, 사용자가 보는 웹 페이지의 모습을 결정한다.
템플릿 시스템의 설계 철학
장고에서 템플릿을 만들 때 설계 철학을 알면 보다 더 효율적인 설계를 할 수 있으므로, 장고에서 템플릿을 만들 때 추구하는 철학들에 대해 한 번 살펴보자.
템플릿 언어는 간결해야 한다.
템플릿 코드는 파이썬에 익숙한 개발자가 아니라, 파이썬에 익숙하지 않은 디자이너가 작성하는 것을 전제로 한다. 디자이어는 파이썬보단 HTML과 CSS에 더 익숙할 것이기 때문이다.
또한, 분기와 반복같이 표현 계층에 꼭 필요한 프로그래밍 기능을 제공하는 것을 목표로 한다.
{% extends "blog/base.html" %}
{% load humanize %}
{% block content %}
<h2>포스팅 목록</h2>
{% for post in post_list %}
<div>
<h3>{{ post.title | upper }}</h3>
{{ post.content | truncatewords : 100 }}
hits: {{ post.hits | intcomma }}
</div>
{% endfor %}
{% endblock %}
위의 예제코드의 template 코드는 보여주는 목적에 최적화가 되어있는 코드이다.
HTML 조합에서 자주 사용
위와 같이 복잡한 문자열 조합에서 또한 자주 사용한다. 흔히 우리가 오해하는 것 중에 하나가 템플릿 시스템은 뷰 응답에서 HTML 응답을 목적으로만 사용을 한다라고 생각하는 것이다. 하지만 어떠한 문자열이라도 조합할 목적으로 템플릿 시스템을 활용할 수 있다. 하지만 현재 우리가 사용하는 것은 웹 프레임워크이니, 대부분 브라우저로부터 요청을 처리하고 있고 이때 HTML 문자열 응답을 하는 경우가 많을 뿐이다.
상황에 따라 HTML 뿐만 아니라, CSS/JS 코드, 파이썬 코드를 생성하는 데에 사용할 수도 있다. 이 모든 것이 가능한 것이 장고의 템플릿 시스템이다.
다양한 문자열을 조합할 수 있도록 설계
장고의 템플릿 시스템은 다양한 포맷의 문자열을 조합할 수 있도록 설계가 되어있다. 복잡한 포맷의 문자열의 언어의 기본 기능 만으로 조합하는 것은 너무 어렵기 때문이다.
from django.http import HttpResponse
from django.shortcuts import render
def index(request) -> HttpResponse:
# render는 내부적으로 render_to_string을 사용한다.
response : HttpResponse = render(request, "app/index.html", {
"title" : "hello title",
})
return response
따라서 보통 render API를 뷰에서 많이 사용한다. 왜냐하면 render의 리턴타입이 HttpResponse이기 때문이다.
from django.http import HttpResponse
from django.shortcuts import render
def index(request) -> HttpResponse:
# 이메일/푸쉬 메시지 문자열 조합 및 전송
response : str = render_to_string("app/welcome_message.txt" , {
"customer_name" : "Djagno",
})
return HttpResponse("...")
그리고 render 말고 render_to_string API도 있다. render의 리턴타입은 HttpResponse이지만 render_to_string의 리턴타입은 str이다.
두 API의 사용법은 거의 비슷하지만 첫 번째 인자로 HttpRequest를 받느냐, 받지 않느냐의 차이 정도가 있겠다. 결론적으로, 장고 템플릿 시스템을 통해 이메일/푸시 메시지와 같이 단순히 str 문자열을 조합할 경우엔 render_to_string을 사용할 수 있겠다.
템플릿 파일은 HTML 파일이 아니다
템플릿 파일은 HTML 파일이 아니다. 장고 외적으로 단순히 HTML 파일을 index.html, hello.html 파일을 만들어서 더블클릭 만으로 브라우저를 통해서 열어 볼 수 있지 않은가? 이와 같은 HTML 파일은 우리가 수동으로 최종 HTML 문자열을 하나하나 하드코딩으로 만든 것이다. 하지만 우리가 장고든 스프링이든 웹 프레임워크를 사용하면, 웹 요청에 대한 HTML 응답은 하드코딩으로 만든 HTML 문자열이 아니라, 동적으로 로직을 통해 생성된 HTML로 응답을 하는 것이다.
장고 뷰 및 스프링 컨트롤러에서 응답하는 최종 아웃풋이 최종 HTML이다. 템플릿 시스템에서 읽은 템플릿 파일은 그 최종 HTML을 만들기 위한 소스이다.
{# blog/templates/blog/post_list.html #}
{% extends "blog/base.html" %}
{% block content %}
<h2>포스팅 목록</h2>
{% for post in post_list %}
<div>{{ post.title }}</div>
{% endfor %}
{% endblock %}
위와 같인 코드가 있을 때 장고의 랜더링을 거치지 않고 해당 HTML파일을 실행시키면 문자열 태그를 제외한 나머지 문자열들은 그대로 출력될 것이다. 하지만 장고의 서버를 거쳐 랜더링이 된다면 정상적인 포스팅 목록이 출력될 것이다. 따라서, 템플릿은 결국 문자열 조합 룰(Rule)을 정의한 것이고, 이 조합룰이 HTML 포맷일 수도 있고, JSON 포맷일 수도 있다.
서버 단에서 HTML 템플릿 코드를 로딩하여, "아 , 이런 형태로 HTML 문자열을 구성하는구나"라는 골격을 가지고 최종 HTML 문자열을 생성하는 것이다.
스파게티 개발을 원천적으로 봉쇄
장고의 템플릿 시스템은 스파게티 개발을 원천적으로 봉쇄한다.
<%-- JSP --%>
<body>
<%!
String hello(String name){
return "Hello " + name;
}
class Hello{
String name;
public Hello(String name){
this.name = name;
}
public String greet(){
return "Hello " + name;
}
}
%>
<%
String message1 = hello("Tom");
out.println(message);
Hello hello = new Hello("Tom");
String message2 = hello.greet();
out.println(message2);
%>
</body>
이는 JSP(Java Server Pages) 코드이다. out.println()을 통해 출력도 있고, 심지어 함수 정의와 클래스 정의까지 하나의 jsp 파일 안에 같이 있다. 이 JSP 파일은 얽힌 실타래처럼 로직이 복잡하게 꼬여, 관리성이 나빠질 가능성이 높다.
장고의 기본적인 주요 철학 중 하나인 Stupid 템플릿 시스템(해당 용어는 장고의 공식 문서에 있는 건 아님)은 제한된 기능의 템플릿 시스템이라는 의미이다. 그래서 템플릿 코드 내에 복잡한 비즈니스 로직을 원천적으로 넣을 수 없고 순수하게 표현에만 집중할 수 있도록 템플릿 시스템을 설계했다는 의미이다. 그래서 템플릿 시스템이 표현을 제어하는 도구이자, 표현과 관련된 로직만 쓸 수 있도록 제약을 둔 것이라고 생각하면 된다.
{# app/templates/app/index.html #}
{# 템플릿에서는 값을 표현하는 데에 집중할 수 있도록 #}
{# 의도적인 기능 제약 #}
{# 조건/반복의 제어구조는 지원 #}
<body>
{{ message1 }}
{{ message2 }}
</body>
앞선 JSP 코드를 장고로 옮겨 본다면, 장고 템플릿에서는 단지 위와 같이 쓰는 게 전부이다. 템플릿에서는 값을 표현하는 데에만 집중할 수 있도록 의도적으로 구현을 제한한다.
# app/views.py
from django.shortcuts import render
from app.utils import hello, Hello
def index(request):
# 템플릿 렌더링 전에 렌더링에 필요한 값을 준비해준다.
# 이를 contesxt data라고 부른다.
return render(request, "app/index.html", {
"message1" : hello("Tom").
"message2" : Hello("Tom").great(),
})
# app/utils.py
def hello(name : str) -> str:
return "Hello " + name
class Hello:
def __init__(self, name : str):
self.name = name
def greet(self):
return "Hello " _ self.name
그래서 그 외의 로직은 템플릿을 렌더링 하기 전에 값을 미리 다 준비해 준다. 위의 예제에서 장고의 함수/클래스 구현은 JSP 구현과 동일하다.
안전과 보안
안전과 보안은 앞서 언급했던 Stupid 템플릿 시스템이기 때문에 안전과 보안이 저절로 챙겨진다. 템플릿 시스템은 데이터베이스의 레코드를 삭제하는 명령과 같은 악의적인 코드가 거의 원천적으로 동작할 수 없도록 설계가 되어 있어서 안전과 보안이 챙겨지는 것이다.
템플릿 세스템에 의해, 백엔드 단에서 임의의 파이썬 코드를 실행할 수 없는 또 다른 이유이기도 하다. 개발자가 의도하지 않은 파이썬 코드는 실행할 수 없도록 원천적으로 막혀있다.
중복을 배제
대게의 웹사이트는 여러 페이지의 헤더(header), 푸터(footer)를 비롯하여 기본 디자인이 같다. 그럼 매 페이지에 걸쳐 헤더/푸터/기본적인 디자인 CSS, JS, HTML이 반복된다. 공통 디자인을 가지기 때문이다. 그래서 각 템플릿 파일마다 필연적으로 중복이 생길 수밖에 없다.
아래의 코드를 한 번 살펴보자.
post_list.html
<!DOCTYPE html>
<html lang = "ko">
<head>
<meta charset = "UTF-8"/>
<title>Django Template Engine</title>
</head>
<body>
<!-- 헤더 -->
<div>
<h1>Header</h1>
</div>
<!-- 컨텐츠 -->
<h2>포스팅 목록</h2>
{% for post in post_list %}
<div>{{ post.title }}</div>
{% endfor %}
<!-- 푸터 -->
<div>© I love you Python</div>
</body>
</html>
post_detail.html
<!DOCTYPE html>
<html lang = "ko">
<head>
<meta charset = "UTF-8"/>
<title>Django Template Engine</title>
</head>
<body>
<!-- 헤더 -->
<div>
<h1>Header</h1>
</div>
<!-- 컨텐츠 -->
<h2>포스팅 : {{ post.title }}</h2>
{{ post.content|linebreaks }}
<hr/>
<a href = "#">목록</a>
<a href = "#">수정</a>
<a href = "#">삭제</a>
<!-- 푸터 -->
<div>© I love you Python</div>
</body>
</html>
post_form.html
<!DOCTYPE html>
<html lang = "ko">
<head>
<meta charset = "UTF-8"/>
<title>Django Template Engine</title>
</head>
<body>
<!-- 헤더 -->
<div>
<h1>Header</h1>
</div>
<!-- 컨텐츠 -->
<h2>포스팅 폼</h2>
<form action = "" method = "post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type = "submit">
</form>
<!-- 푸터 -->
<div>© I love you Python</div>
</body>
</html>
3개 파일의 코드를 모두 살펴보았다면, 콘텐츠 부분만 달라질 뿐이고 나머지 부분은 전부 같은 것을 확인할 수 있다. 만약 <title> 태그의 내용을 변경해야 한다고 하면 3개의 파일 모두 변경을 해야 한다는 엄청난 번거로움이 발생한다. 파일이 3개만 있을 경우는 그나마 괜찮겠지만 저렇게 중복되는 코드를 가진 파일들이 10개, 100개가 넘는다고 상상해 보자. 개발자들도 사람인지라 분명히 빠뜨리거나 실수를 저지를 확률이 높다.
그래서 저렇게 중복되는 코드들은 이대로 운영을 해도 괜찮을까? 아니면 중복을 없앨 수 있을까? 하는 질문들이 떠오를 것이다. 따라서, 장고 템플릿 시스템에서는 중복을 제거할 수 있는 방법인 템플릿 상속을 지원한다. 상속을 통해 템플릿에서의 중복 코드를 어떻게 제거하는지 한 번 살펴보자.
먼저 위에서 살펴보았던 post_list.html, post_detail.html, post_form.html을 아래와 같이 수정하자.
post_list.html
{# app/templates/app/post_list.html #}
{% extends "app/base.html" %}
{% block content %}
<h2>포스팅 목록</h2>
{% for post in post_list %}
<div>{{ post.title }}</div>
{% endfor %}
{% endblock %}
post_detail.html
{# app/templates/app/post_detail.html #}
{% extends "app/base.html" %}
{% block content %}
<h2>포스팅 : {{ post.title }}</h2>
{{ post.content|linebreaks }}
<hr/>
<a href = "#">목록</a>
<a href = "#">수정</a>
<a href = "#">삭제</a>
{% endblock %}
post_form.html
{# app/templates/app/post_form.html #}
{% extends "app/base.html" %}
{% block content %}
<h2>포스팅 폼</h2>
<form action = "" method = "post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type = "submit"/>
</form>
{% endblock %}
각 템플릿에는 메인 콘텐츠만 코딩되어 있고, 나머지 중복되는 부분은 부모 템플릿으로 뽑아낸다. 각 파일에서 가장 위에 있는 extends는 템플릿을 상속받기 위한 키워드이다. 따라서, app/base.html 파일을 상속받는다는 의미이다. 그리고 각 콘텐츠 내용들은 block content로 감싸져 있는데, 블록을 지정해 주는 것이고, content는 블록의 이름이다. 따라서 개발자가 블록을 지정할 때 이름을 마음대로 정할 수 있다. 하지만 블록은 부모 템플릿에서 정의를 해주고 자식 콘텐츠에서는 "여기서부터 여기까지 블록으로 지정할게" 정도만 해주면 된다. 그럼 이제 부모 템플릿인 app/base.html을 한 번 살펴보자.
base.html
{# app/templates/app/base.html #}
<!DOCTYPE html>
<html lang = "ko">
<head>
<meta charset = "UTF-8">
<title>Django Template Engine</title>
</head>
<body>
<!-- 헤더 -->
<div>
<h1>Header</h1>
</div>
<!-- 컨텐츠 -->
{% block content %}
{% endblock %}
<!-- 푸터 -->
<div>© I love you Python</div>
</body>
</html>
부모 템플릿에서 하는 일은 자식 템플릿이 비집고 들어올 수 있는 block 들을 정의한다. 부모가 정의한 block이 아닌 다른 영역에는 자식이 콘텐츠를 넣을 수 없다. 부모는 content라는 이름의 block을 정의한 것을 위의 코드에서 확인할 수 있다. 그럼 content라는 이름의 block에다가 자식 템플릿에서 content라는 이름으로 지정한 block 범위의 코드가 해당 위치에 렌더링 되는 것이다.
확장성
장고 템플릿 시스템은 확장성도 가지고 있다. 그래서 단순히 어떤 값을 그대로 표현만 하는 것이 아니라, 템플릿 만의 어떤 함수가 있다. 이를 장고 템플릿 태그와 필터라고 부른다. Template Tag가 html 태그는 아니다. Django Template Tag와 Django Template Filter이다. 사용하는 문법은 아주 단순하다. 그래서 파이썬 문법을 몰라도 손쉽게 사용할 수 있다. HTML을 읽고 쓸 수 만 있다면 사용할 수 있고, 커스텀 템플릿 태그/필터를 개발하여 템플릿 시스템을 확장할 수 도 있다.
{# ["a", "b", "c"]를 ["a", "b"]로 슬라이싱 #}
{{ some_list|slice:":2" }}
{# "Jack is a slug"를 "jack-is-a-slug"로 변환 #}
{# Tip : 공백은 URL에서 %20으로 변환된다. #}
{{ value|sllugify }}
{# <b>Jack</b> <button>is</button> a <span>slug</span>.를 #}
{# Jack is a slug.로 변환 #}
{{ value|striptags }}
{# "Jack is a slug"를 "Jack is ..."로 변환 #}
{{ value|truncatewords:2 }}
{# 3자리마다 콤마. django.contrib.humanize 앱에서 지원 #}
{% load humanize %}
{{ 4500|intcomma }}
{# static 저장소에 대한 상대경로를 절대경로로 변환 #}
{% load static %}
{% static "images/hi.jpg" %}
중괄호 2개는 그 값을 해당 위치에 렌더링 하는 것이고, 별도로 {% %}는 지정 Template Tag를 호출하는 것이다. Template Tag를 호출하면서 인자를 넘기는 방식이다.
slice 템플릿 필터는 ["a", "b", "c"] 값이 담긴 some_list에서 처음 2개 항목만 슬라이싱 하고자 한다면, slice:":2"를 하게 되면 파이썬에서 some_list[:2]를 한 거와 동일하게 처음 2개만 슬라이싱이 된다.
slugify 템플릿 필터는 URL 친화적인 문자열로 변환을 해주는 것이다."Jack is a slug" 문자열을 URL에 쓰면 공백은 "%20"으로 변환된다. 템플릿 값을 표현할 때 파이프(|)와 함께 사용한 대상을 우리가 Django Template Filter라고 부른다.
striptags 필터는 html 태그 문자열을 제거해 주는 것이다.
truncatewords 필터는 단어(word) 단위로 지정 개수를 잘라준다. 그래서 위의 예제에서는 2로 지정했기 때문에 단어 2개를 잘라주는 것이다. 단어(word)는 공백을 구분자로 나눠준다. 결론적으로 "Jack is" 까지만 잘라주고 나머지는... 문자열로 대체된다.
humaize는 장고 기본 앱 중 하나인데, 우리가 숫자를 적을 때 3자리마다 콤마를 찍는 것처럼 intcomma 필터를 사용하면 인자로 받은 숫자를 문자열로 바꾸고, 3자리마다 자동으로 콤마가 찍히게 된다. 그래서 위의 예제에서의 결과는 "4,500"이 될 것이다.
'Framework > Django' 카테고리의 다른 글
[Django] 장고 URL 설계 (0) | 2023.12.17 |
---|---|
[Django] 장고 뷰(View) (0) | 2023.12.15 |
[Django] 장고에서의 요청 처리 (0) | 2023.12.14 |
[Django] 장고의 설계 철학 (1) | 2023.12.12 |
[Django] 장고 app 생성 (0) | 2023.11.01 |
소중한 공감 감사합니다