4편 — 템플릿 = HTML에 데이터 끼우기 - 테크창
프로그래밍

4편 — 템플릿 = HTML에 데이터 끼우기

AI 활용 테크창 연구팀이 인공지능을 활용해 작성하고, 사실 확인과 편집 심사를 거쳐 발행한 칼럼입니다. 제작 과정 보기

조회 69회
댓글 0개
0
📚 연재 시리즈 AI가 짜준 Django, 이제 알고 씁니다 5 / 8

🎬 지난 이야기

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단계)

  1. 브라우저가 목록 주소로 요청을 보냅니다.
  2. URL 안내판이 담당 뷰로 보냅니다.
  3. 일반적인 Django 흐름에서는 뷰가 필요한 데이터를 context에 담아 render()로 템플릿을 호출합니다.
  4. (템플릿 렌더링) Django가 템플릿을 위에서 아래로 읽으며 {% %}를 실행하고 {{ }}를 값으로 바꿉니다.
  5. 완성된 순수 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 %}로 반복하는 템플릿 하나를 골라 따라 해 보세요.

  1. (a) context 키 이름 바꾸기: 뷰의 {'popular_posts': qs}를 {'popular': qs}로 바꾸고 저장한 뒤 새로고침합니다.
  2. 기대 결과: 에러 없이 반복 영역이 텅 빈 화면이 됩니다. 확인 후 원래대로 되돌리세요.
  3. (b) {% endfor %} 지우기: 템플릿의 {% endfor %}를 지우고 새로고침합니다.
  4. 기대 결과: TemplateSyntaxError: Unclosed tag … 'for' 오류 화면이 나오고, 메시지에서 열린 태그의 줄 번호를 읽을 수 있습니다. 확인 후 복구하세요.
  5. (c) 파일을 브라우저로 직접 열기: 템플릿 .html 파일을 탐색기에서 브라우저로 끌어다 놓습니다.
  6. 기대 결과: {{ question.subject }}, {% for … %} 같은 글자가 그대로 보입니다. Django를 거치지 않았기 때문입니다.
  7. 내 모델 필드로 바꿔 보기: 내 모델의 불리언 필드 하나(예: is_done, is_public)를 골라 템플릿에 {% if item.is_done %}완료{% endif %}처럼 넣어 보세요.
  8. 기대 결과: 값이 True인 항목에만 "완료"가 보이고, False인 항목에는 아무것도 나오지 않습니다.
  9. 번호 붙이기: 반복문 안에 {{ forloop.counter }}를 추가합니다.
  10. 기대 결과: 항목마다 1, 2, 3… 번호가 붙습니다.

막히면 테크창 커뮤니티에 어떤 화면이나 에러가 나왔는지 함께 남겨 주세요.


⏭️ 다음 편 예고

템플릿에 끼운 question.subject와 view_count는 대체 어디에 저장돼 있던 걸까요? 5편 「모델 = AI가 만든 표(테이블)의 실체」에서 그 창고를 열어봅니다.


📚 시리즈: AI가 짜준 Django, 이제 알고 씁니다 · 4편 (0~9편, 총 10회)
테크창 연구팀
본 칼럼은 AI 보조로 작성되었으며, 코드·예시는 학습용입니다.

오늘 글에서 하나 가져간다면?

다 읽고 남은 한 가지를 한 줄로 적어 주세요. 다른 독자에게도, 다음 글을 쓰는 연구팀에게도 가장 좋은 신호가 됩니다.

Making of · 이 칼럼이 만들어진 과정

AI 연구원 5명이 2번 심사하고 2번 고쳐 쓴 글입니다

  • 2편집 심사
  • 2재집필
  • 1평론
  • 3분제작 기간
심사 점수 64 → 85점점선은 프리미엄 기준 90점
제작 과정 전체 보기
T
techchang연구팀
시리즈 목차 전체 보기

아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!

댓글을 작성하려면 로그인해주세요

로그인
모바일 버전