4편 — 템플릿 = HTML에 데이터 끼우기
AI 활용 테크창 연구팀이 인공지능을 활용해 작성하고, 사실 확인과 편집 심사를 거쳐 발행한 칼럼입니다. 제작 과정 보기
🎬 지난 이야기
3편에서는 URL 안내판이 가리키는 담당자, 즉 뷰(View) 를 뜯어봤습니다. 뷰는 요청을 받아 DB에서 데이터를 꺼내고, render()로 그 데이터를 HTML 파일에 넘겨주는 함수였죠. 그럼 넘겨받은 데이터는 HTML 어디에, 어떻게 꽂힐까요? 오늘은 그 목적지인 템플릿을 뜯어봅니다.
AI가 만들어준 HTML 파일을 열었더니 {{ question.subject }}, {% for question in popular_posts %} 같은 낯선 문자가 잔뜩 섞여 있어서 "이게 HTML이야, 뭐야?" 싶었던 적 있으시죠? 더 당황스러운 건 화면입니다. 어떤 건 제목으로 바뀌어 나오는데, 어떤 건 {{ ... }} 글자가 그대로 찍히거나 아예 빈 화면이 되기도 하니까요.
{{ }}와 {% %}는 각각 무슨 일을 하고, 왜 어떤 건 값이 되고 어떤 건 글자로 남을까요? 오늘 이 질문에 답합니다. 마지막에 이 질문으로 돌아와 정리하겠습니다.
🤖 AI가 짜준 코드
프롬프트: "인기 질문을 카드 형태로 반복 출력하는 Django 템플릿 만들어줘"
아래는 테크창 메인 목록 화면의 실제 코드입니다. 파일 경로는 templates/community/question_list.html이고, 지면상 인기글 카드 반복 부분만 가져왔습니다.
{% for question in popular_posts %}
<a href="{% url 'community:detail' question.id %}" class="tc-popular-card{% if question.id in picks %} is-pick{% endif %}" data-reveal>
<span class="tc-popular-rank">{{ forloop.counter|stringformat:"02d" }}</span>
<span class="tc-popular-body">
<span class="tc-popular-title">{{ question.subject }}</span>
<span class="tc-popular-meta">
<span><i class="far fa-eye" aria-hidden="true"></i> {{ question.view_count }}</span>
{% if question.is_locked and not user.is_authenticated %}<span class="tc-member-only"><i class="fas fa-lock" aria-hidden="true"></i> 회원 전용</span>{% endif %}
</span>
</span>
</a>
{% endfor %}
원본에는 추천 배지, 카테고리 배지 같은 줄이 더 있지만 구조는 같습니다.
실행하면 보이는 것: 인기 질문 카드가 위에서부터 한 장씩 나열됩니다. 카드마다 순번이 01, 02, 03처럼 두 자리로 찍히고, 그 옆에 질문 제목과 조회수가 붙습니다. 비로그인 상태에서 잠긴 글이 목록에 포함되어 있다면 "🔒 회원 전용" 표시가 추가로 나옵니다.
🔍 한 줄씩 통역
렌더링 전후, 한 줄 비교
템플릿은 "구멍과 지시문이 뚫린 HTML"입니다. 소스 한 줄이 렌더링 전후로 어떻게 바뀌는지 나란히 보겠습니다(제목·숫자는 예시입니다).
<!-- 렌더링 전: 서버에 있는 템플릿 소스 -->
<span class="tc-popular-rank">{{ forloop.counter|stringformat:"02d" }}</span>
<span class="tc-popular-title">{{ question.subject }}</span>
<!-- 렌더링 후: 브라우저가 받는 HTML -->
<span class="tc-popular-rank">01</span>
<span class="tc-popular-title">장고 템플릿이 뭔가요?</span>
{{ }}도 {% %}도 사라지고 순수 HTML만 남습니다. 브라우저는 Django 문법을 전혀 모릅니다. {{ }}는 값을 끼우는 구멍이고, {% %}는 반복·조건 같은 동작 지시입니다.
1. {% for question in popular_posts %} — 반복 지시서
"popular_posts 안의 항목을 하나씩 꺼내서 question이라고 부르고, {% endfor %}까지 반복해 주세요"라는 뜻입니다. 인기글이 5개면 <a>…</a> 덩어리가 5번 찍혀 나옵니다.
여기서 가장 헷갈리는 게 이름이 두 개라는 점입니다. 뷰 코드와 함께 보세요(3편 내용의 반복이 아니라 연결용으로 짧게만 가져왔습니다).
# views.py: 뷰가 'popular_posts'라는 이름표를 붙여 템플릿에 넘깁니다
return render(request, 'community/question_list.html', {'popular_posts': qs})
<!-- question_list.html -->
{% for question in popular_posts %} {# popular_posts: 뷰가 붙인 이름 (context 키) #}
{{ question.subject }} {# question: 이 루프 안에서만 쓰는 임시 이름 #}
{% endfor %}
뷰의 {'popular_posts': qs} ──▶ 템플릿의 popular_posts ──(하나씩 꺼내 부름)──▶ question
(context 키, 뷰와 약속) (전체 묶음) (루프 임시 이름)
popular_posts는 뷰와 템플릿이 맞춰야 하는 약속된 이름입니다. 한 글자만 달라도 데이터가 전달되지 않습니다.question은 내가 마음대로 지어도 되는 임시 이름입니다.{% for q in popular_posts %}로 바꾸고 안쪽도q.subject로 맞추면 똑같이 동작합니다.{% endfor %}밖에서는 쓸 수 없습니다.
2. {{ question.subject }} — 값이 들어가는 구멍
question은 질문 한 건이고, .subject는 그 질문의 제목입니다. Django가 이 구멍을 제목 글자로 바꿔 끼웁니다. {{ question.view_count }}도 같은 원리로 조회수가 들어갑니다.
3. {{ forloop.counter|stringformat:"02d" }} — 반복 번호 + 가공
forloop.counter는 for문 안에서만 쓸 수 있는 자동 번호표로, 1부터 셉니다. 뒤의 |stringformat:"02d"는 필터라고 부르는데, 구멍에 끼우기 전에 값을 다듬는 작업대입니다. "02d"는 "두 자리로 맞추고 빈칸은 0으로 채워라"라는 뜻이라 1이 01이 됩니다.
4. {% url 'community:detail' question.id %} — 주소를 이름으로 찾기
2편의 안내판을 기억하시나요? 여기서는 주소를 직접 쓰지 않고 안내판에 등록된 이름(community:detail)과 질문 번호로 주소를 만들어 냅니다. 나중에 주소 규칙이 바뀌어도 템플릿을 고칠 필요가 없습니다.
5. {% if … %} — 조건에 따라 끼우거나 빼기
class="tc-popular-card{% if question.id in picks %} is-pick{% endif %}"
추천글 번호가 담긴 값(picks)에 이 질문의 번호가 들어 있으면 is-pick이라는 클래스를 추가합니다. 같은 코드로 찍어도 어떤 카드만 디자인이 달라지는 이유입니다. 회원 전용 표시도 question.is_locked and not user.is_authenticated, 즉 "잠긴 글이고 로그인 안 한 사람"일 때만 나옵니다.
요청부터 응답까지 (5단계)
- 브라우저가 목록 주소로 요청을 보냅니다.
- URL 안내판이 담당 뷰로 보냅니다.
- 일반적인 Django 흐름에서는 뷰가 필요한 데이터를 context에 담아
render()로 템플릿을 호출합니다. - (템플릿 렌더링) Django가 템플릿을 위에서 아래로 읽으며
{% %}를 실행하고{{ }}를 값으로 바꿉니다. - 완성된 순수 HTML이 브라우저로 갑니다.
도입부 질문의 답: {{ }}는 값을 끼우는 구멍, {% %}는 반복·조건 같은 동작 지시입니다. 글자가 그대로 보이는 것은 주로 위 5단계 중 4단계(템플릿 렌더링)를 거치지 않아, Django가 그 파일을 템플릿으로 처리하지 않았다는 신호입니다. 다만 {% verbatim %}으로 감쌌거나 Vue·Handlebars 같은 JS 템플릿 구문일 때도 글자가 그대로 남습니다. 증상별 원인은 아래 표에서 확인하세요.
⚠️ 자주 깨지는 지점
① 흔한 문법 오류: {% endfor %} 누락
django.template.exceptions.TemplateSyntaxError: Unclosed tag on line 1: 'for'. Looking for one of: empty, endfor.
for를 열고 닫지 않은 것이 원인입니다({% if %}도 {% endif %}가 필요합니다). 메시지의 줄 번호는 열린 태그가 있는 줄을 가리키니, 거기서 아래로 내려가며 닫는 태그를 찾으세요.
② 뷰가 넘긴 이름과 템플릿 이름이 다를 때
뷰에서 {'popular': ...}로 보냈는데 템플릿에서 popular_posts를 쓰면, 에러 없이 반복이 0번 돌아 카드가 하나도 안 나옵니다. Django 템플릿은 없는 변수를 조용히 빈 값으로 처리하기 때문입니다. 뷰의 context 키를 확인하고, 템플릿의 변수명과 글자 단위로 대조해 같게 맞춘 뒤 새로고침하세요.
③ {{ }} 글자가 그대로 보일 때: 증상별 원인과 확인법
| 증상 | 원인 | 확인법 |
|---|---|---|
파일을 브라우저로 직접 열었더니 {{ }}가 그대로 보임 |
브라우저는 Django 문법을 모르고, Django를 거치지 않았음 | 주소창이 file:///…로 시작하는지 확인. 반드시 runserver 주소(http://127.0.0.1:8000/…)로 접속 |
render() 없이 정적 파일로 서빙했더니 글자가 그대로 보임 |
정적 파일(static)은 템플릿 엔진을 거치지 않고 그대로 전달됨 | 파일이 static/에 있는지, 뷰가 render()로 templates/의 파일을 호출하는지 확인 |
{% verbatim %} 안의 {{ }}가 그대로 보임 |
{% verbatim %}…{% endverbatim %}은 Django가 일부러 해석하지 않는 구간 |
템플릿에서 해당 구간을 감싼 verbatim 태그가 있는지 검색 |
Vue·Handlebars 등 JS 템플릿의 {{ }}가 Django에서 먼저 처리돼 비거나 어긋남 |
같은 {{ }} 문법을 Django와 JS가 둘 다 씀 |
JS 쪽에 쓸 구간을 {% verbatim %}으로 감싸 Django가 건드리지 않게 함 |
| 오타 난 변수나 없는 변수 | 글자가 아니라 빈 값이 됨(에러도 없음) | 뷰의 context 키와 템플릿 변수명을 글자 단위로 대조 |
🎮 직접 해보기
일부러 오류를 내 보면 템플릿이 어떻게 동작하는지 가장 빨리 이해됩니다. 내 로컬 Django 프로젝트에서, 목록을 {% for %}로 반복하는 템플릿 하나를 골라 따라 해 보세요.
- (a) context 키 이름 바꾸기: 뷰의
{'popular_posts': qs}를{'popular': qs}로 바꾸고 저장한 뒤 새로고침합니다. - 기대 결과: 에러 없이 반복 영역이 텅 빈 화면이 됩니다. 확인 후 원래대로 되돌리세요.
- (b)
{% endfor %}지우기: 템플릿의{% endfor %}를 지우고 새로고침합니다. - 기대 결과:
TemplateSyntaxError: Unclosed tag … 'for'오류 화면이 나오고, 메시지에서 열린 태그의 줄 번호를 읽을 수 있습니다. 확인 후 복구하세요. - (c) 파일을 브라우저로 직접 열기: 템플릿
.html파일을 탐색기에서 브라우저로 끌어다 놓습니다. - 기대 결과:
{{ question.subject }},{% for … %}같은 글자가 그대로 보입니다. Django를 거치지 않았기 때문입니다. - 내 모델 필드로 바꿔 보기: 내 모델의 불리언 필드 하나(예:
is_done,is_public)를 골라 템플릿에{% if item.is_done %}완료{% endif %}처럼 넣어 보세요. - 기대 결과: 값이
True인 항목에만 "완료"가 보이고,False인 항목에는 아무것도 나오지 않습니다. - 번호 붙이기: 반복문 안에
{{ forloop.counter }}를 추가합니다. - 기대 결과: 항목마다 1, 2, 3… 번호가 붙습니다.
막히면 테크창 커뮤니티에 어떤 화면이나 에러가 나왔는지 함께 남겨 주세요.
⏭️ 다음 편 예고
템플릿에 끼운 question.subject와 view_count는 대체 어디에 저장돼 있던 걸까요? 5편 「모델 = AI가 만든 표(테이블)의 실체」에서 그 창고를 열어봅니다.
📚 시리즈: AI가 짜준 Django, 이제 알고 씁니다 · 4편 (0~9편, 총 10회)
테크창 연구팀
본 칼럼은 AI 보조로 작성되었으며, 코드·예시는 학습용입니다.
오늘 글에서 하나 가져간다면?
다 읽고 남은 한 가지를 한 줄로 적어 주세요. 다른 독자에게도, 다음 글을 쓰는 연구팀에게도 가장 좋은 신호가 됩니다.
AI 연구원 5명이 2번 심사하고 2번 고쳐 쓴 글입니다
- 2편집 심사
- 2재집필
- 1평론
- 3분제작 기간
댓글 (0)
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!