사전 HTML 템플릿
용어함정

HTML 템플릿

gabury1고친 사람 github-actions[bot]

HTML 템플릿은 화면의 뼈대를 미리 써 둔 틀입니다. 내용만 바꿔 끼우면 같은 모양의 페이지를 여러 번 찍어 낼 수 있습니다. 이 이름은 두 물건을 부릅니다. 첫째는 값이 들어갈 빈칸을 남긴 페이지 글자 틀이고, 서버 개발에서 먼저 만납니다. 둘째는 웹 페이지의 <template> 태그가 만드는 요소로, 브라우저가 화면에 그리지 않고 담아 두기만 하는 페이지 조각입니다.

쉽고 빠른 이해

무슨 일을 하나 — 게시글 목록 페이지는 글 제목만 바뀌고 나머지는 늘 같습니다. 그 늘 같은 부분을 틀로 써 둡니다. 바뀌는 칸만 채워 같은 모양을 여러 번 찍어 냅니다. 이 이름이 부르는 물건은 둘입니다. 빈칸을 남긴 글자 틀과, 브라우저가 그리지 않고 담아 두는 <template> 조각입니다.

왜 이렇게 하나 — 틀이 없으면 코드 안에서 페이지 글자를 한 줄씩 이어 붙여야 합니다. 화면 모양이 코드 곳곳에 흩어집니다. 사용자가 입력한 글을 무심코 그대로 넣어 공격에 뚫리기도 쉽습니다.

어떻게 도나

  1. 서버 쪽은 틀과 데이터를 합쳐 빈칸을 채웁니다. 완성된 페이지를 응답으로 보냅니다
  2. 브라우저 쪽은 <template> 태그 안에 든 조각을 화면에 내지 않고 담아 둡니다
  3. 스크립트가 그 조각을 복사해 값을 넣고 페이지에 붙이면 그때 화면에 나옵니다

언제 쓰나 — 글자 틀은 서버가 화면을 다 만들어 보내는 페이지와 메일 본문에 씁니다. 서버가 화면 없이 데이터만 내주는 구조라면 서버 쪽에는 틀이 없습니다. <template> 요소는 스크립트가 같은 모양의 행을 여러 번 붙일 때 씁니다. 화면을 그려 주는 프론트엔드 라이브러리를 쓰면 라이브러리가 그 일을 맡아서 직접 다룰 일이 드뭅니다.

대가 — 서버 쪽 틀은 값을 글자로 이어 붙입니다. 위험한 글자를 바꿔 주는 처리가 꺼져 있으면 공격이 다시 열립니다. 두 뜻을 섞어 부르면 대화가 엇갈립니다.

상세

이 절은 「HTML 템플릿」이 부르는 두 물건을 차례로 봅니다. 데이터로 빈칸을 채우는 HTML 글자 틀과, 브라우저가 담아 두었다가 스크립트가 복제하는 <template> 요소입니다.

HTML 템플릿이 가리키는 두 물건

HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)은 웹 페이지에 무엇이 어떤 순서로 놓이는지 적는 언어입니다. 템플릿은 모양을 정해 두고 내용만 비워 둔 틀입니다. 관공서 서식에 이름과 날짜 칸만 비어 있는 것과 같습니다.

두 뜻 모두 「같은 모양을 여러 번 찍어 낼 틀」을 말합니다. 갈리는 것은 틀의 정체입니다. 첫째 뜻은 빈칸을 남긴 HTML 글자이고, 데이터를 부어 완성된 HTML 을 얻습니다. 둘째 뜻은 <template> 태그가 만드는 요소이고, 브라우저가 그리지 않은 채 담아 둡니다.

뜻 정체 채우는 쪽 예
첫째 · 글자 틀 값이 들어갈 빈칸을 남긴 HTML 파일 주로 서버 프로그램, 프론트엔드 라이브러리면 브라우저 게시글 목록 페이지 · 가입 환영 메일
둘째 · template 요소 <template> 태그 안에 넣어 둔, 화면에 안 그려지는 HTML 조각 브라우저에서 도는 스크립트 목록에 한 줄씩 붙이는 행 · 여러 번 쓰는 화면 부품의 속 구조

백엔드 개발자가 먼저 만나는 것은 첫째 뜻입니다. 프론트엔드 라이브러리가 말하는 템플릿도 빈칸을 남긴 글자 틀이라 첫째 뜻에 듭니다. 둘째 뜻은 <template> 태그 자체를 가리킬 때만 씁니다.

flowchart TD
    subgraph S["서버 · 첫째 뜻"]
        A["HTML 템플릿 파일"]
        B["서버 프로그램이 빈칸을 채운다"]
    end
    C["완성된 HTML 응답"]
    subgraph W["브라우저"]
        subgraph T["둘째 뜻"]
            E["template 요소 · 그리지 않고 담아 둔다"]
            F["스크립트가 복제해 붙인다"]
        end
        D["화면"]
    end
    A --> B --> C
    C --> D
    C --> E --> F --> D

그림은 서버가 페이지를 만들어 보낼 때 두 뜻이 일하는 곳을 보입니다. 첫째 뜻은 서버 안에서 쓰이고 끝납니다. 브라우저가 받는 것은 빈칸이 다 채워진 HTML 뿐입니다. 둘째 뜻은 그 HTML 에 실려 브라우저로 넘어간 뒤에야 일을 시작합니다.

서버에서 빈칸을 채우는 템플릿

서버가 페이지를 HTML 로 돌려줄 때 페이지 대부분은 매번 같습니다. 머리말과 메뉴와 바닥글은 그대로입니다. 바뀌는 것은 글 제목이나 사용자 이름뿐입니다. 그래서 안 바뀌는 부분은 파일로 써 두고 바뀌는 부분만 빈칸으로 남깁니다. 이 파일이 서버 쪽에서 말하는 HTML 템플릿입니다.

틀이 없으면 서버 코드 안에서 HTML 글자를 이어 붙여야 합니다. "<li>" + title + "</li>" 같은 줄이 페이지마다 수십 줄씩 쌓입니다. 페이지 모양을 조금 바꾸려 해도 서버 코드를 고쳐야 합니다. 템플릿은 화면 모양을 코드 밖의 파일로 떼어 내서 이 일을 덜어 줍니다.

빈칸을 채우는 프로그램이 템플릿 엔진입니다. 템플릿 파일과 데이터를 받아 빈칸에 값을 넣고 완성된 HTML 문자열을 내놓습니다. 템플릿을 채워 HTML 을 만드는 이 일을 렌더링이라 합니다.

아래는 게시글 목록 페이지의 템플릿 한 토막입니다. 빈칸 문법은 파이썬 템플릿 엔진 Jinja 를 따랐습니다.

HTML
<h1>{{ user }}님의 글</h1>
<ul>
{% for post in posts %}
  <li>{{ post.title }}</li>
{% endfor %}
</ul>

{{ }} 는 값을 넣을 빈칸입니다. {% %} 는 반복이나 조건 같은 제어를 적습니다. 이 토막은 글 목록 posts 를 돌며 글마다 <li> 한 줄을 찍습니다. 글이 셋이면 세 줄이 나옵니다. 하나도 없으면 빈 목록이 나옵니다.

다른 언어에도 같은 일을 하는 엔진이 있습니다. 자바에는 Thymeleaf 가 있습니다. 루비에는 ERB(Embedded Ruby, 임베디드 루비)가 있습니다. 빈칸을 적는 기호는 엔진마다 다르지만, 어느 엔진이든 틀에 데이터를 부어 HTML 을 얻습니다.

화면 모양만 맡는 코드를 뷰라고 부릅니다. 뷰는 웹 애플리케이션을 데이터(모델) · 요청 처리(컨트롤러) · 화면(뷰) 셋으로 나누는 MVC(Model-View-Controller, 모델-뷰-컨트롤러) 구조의 한 부분입니다. 이 구조에서는 서버 템플릿이 뷰를 맡습니다.

요청 하나가 응답이 되기까지 템플릿은 맨 끝에 한 번 쓰입니다.

sequenceDiagram
    participant 브라우저
    participant 앱 as 웹 애플리케이션
    participant DB as 데이터베이스
    participant 엔진 as 템플릿 엔진
    브라우저->>앱: 글 목록 페이지 요청
    앱->>DB: 글 목록 조회
    DB-->>앱: 글 세 건
    앱->>엔진: 템플릿 + 글 세 건
    엔진-->>앱: 완성된 HTML
    앱-->>브라우저: HTML 응답

그림에서 템플릿은 웹 애플리케이션과 엔진 사이만 오갑니다. 브라우저는 템플릿 파일을 보지 못합니다. 데이터베이스의 데이터까지 빈칸에 다 채워진 결과만 받습니다.

빈칸에 넣는 값의 이스케이프

템플릿 엔진은 HTML 을 글자로 다룹니다. 빈칸에 넣는 값도 글자로 이어 붙입니다. 사용자가 이름을 <script> 로 시작하는 글로 지었다면 그 글이 페이지에 그대로 박힙니다. 브라우저는 그것을 태그로 읽고 스크립트를 실행합니다.

남의 페이지에 스크립트를 심는 이 공격이 XSS(Cross-Site Scripting, 교차 사이트 스크립팅)입니다. 심긴 스크립트는 그 페이지를 연 사람의 권한으로 돕니다. 로그인한 사용자의 정보를 빼 가는 데 쓰일 수 있습니다.

막는 방법이 이스케이프입니다. 값을 넣기 전에 < 를 &lt; 로, > 를 &gt; 로 바꿔 둡니다. 브라우저는 바뀐 글자를 태그로 읽지 않고 화면에 < 와 > 로 보여 줍니다.

flowchart TD
    A["사용자가 지은 이름 · script 태그가 든 글"]
    B["템플릿 엔진의 빈칸"]
    C{"이스케이프가 켜져 있나"}
    D["위험한 글자를 바꿔 넣는다"]
    E["브라우저가 글자로 보여 준다"]
    F["글이 그대로 박힌다"]
    G["브라우저가 태그로 읽고 실행 · XSS"]
    A --> B --> C
    C -- 켬 --> D --> E
    C -- 끔 --> F --> G

같은 입력이 이스케이프 하나로 두 결말로 갈립니다. 아래 코드는 켠 쪽을 Jinja 로 확인합니다. 같은 빈칸에 평범한 이름과 태그가 든 이름을 차례로 넣어 봅니다. autoescape=True 가 이스케이프를 켜는 설정입니다.

Python
from jinja2 import Template
t = Template("<p>{{ n }}</p>", autoescape=True)
t.render(n="유미")  # <p>유미</p>
t.render(n="<b>")  # <p>&lt;b&gt;</p>

둘째 값의 <b> 는 태그가 되지 않고 글자로 남습니다. 웹 프레임워크에 딸린 템플릿 엔진은 흔히 이 이스케이프를 켜 둔 채로 시작합니다. 빈칸 하나만 골라 끄는 기능도 있습니다. 끈 빈칸에 사용자 입력이 들어가면 그림의 끔 쪽 길로 가서 XSS 가 다시 열립니다.

서버 템플릿을 쓰는 때와 안 쓰는 때

서버 템플릿은 서버가 화면을 다 만들어 보내는 방식에서 씁니다. 이 방식이 서버 사이드 렌더링입니다.

관리자 화면처럼 빨리 만들어야 하는 페이지가 이 방식을 자주 씁니다. 화면 코드를 따로 짤 필요 없이 서버 코드 옆에 틀만 두면 되기 때문입니다. 검색 엔진이 내용을 읽어 가야 하는 페이지도 그렇습니다. 검색 엔진이 받는 HTML 에 내용이 이미 다 들어 있습니다.

HTML 메일도 같은 틀로 만듭니다. 가입 환영 메일과 주문 확인 메일은 받는 사람 이름과 주문 내용만 다릅니다. 메일 본문을 템플릿으로 두고 보낼 때마다 채웁니다. 화면이 없는 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스) 서버도 메일을 보내면 템플릿 엔진을 씁니다.

반대로 서버가 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 데이터만 내주고 화면은 브라우저가 그리는 구조가 있습니다. 이 구조가 클라이언트 사이드 렌더링입니다. 이때 서버 쪽에는 HTML 템플릿이 없습니다. HTML 을 만드는 일은 브라우저에서 도는 프론트엔드 라이브러리가 맡습니다.

그 라이브러리들도 틀을 씁니다. Vue.js 와 Angular 는 빈칸을 남긴 HTML 을 템플릿이라고 부릅니다. 채우는 곳은 대개 서버가 아니라 브라우저입니다. 그래도 빈칸을 남긴 글자 틀이니 첫째 뜻에 듭니다.

브라우저가 담아 두는 template 요소

이번에는 둘째 뜻, HTML 의 <template> 태그가 만드는 요소입니다. 이 요소를 보려면 브라우저가 페이지를 어떻게 담아 두는지부터 알아야 합니다.

브라우저는 HTML 을 읽어 요소들을 나무 모양으로 이어 둔 DOM(Document Object Model, 문서 객체 모델)을 만듭니다. 화면에 그려지는 것은 이 DOM 입니다. 스크립트는 DOM 에 요소를 붙이거나 떼서 화면을 바꿉니다.

HTML 에는 <template> 이라는 태그가 있습니다. 이 태그 안에 쓴 HTML 은 브라우저가 읽기는 하지만 화면에 그리지 않습니다. 나중에 스크립트가 꺼내 쓸 조각을 페이지 안에 넣어 두는 상자입니다. 이 태그가 만드는 요소가 둘째 뜻의 HTML 템플릿입니다.

스크립트가 같은 모양의 행을 여러 번 만들 때 이 상자가 쓸모 있습니다. 채팅 메시지 한 줄이나 표의 한 행이 그렇습니다. 틀이 없으면 요소를 하나씩 만드는 함수를 줄줄이 불러야 합니다. 그러면 행 모양이 코드에 흩어집니다.

다른 길은 innerHTML 입니다. innerHTML 은 요소 안의 내용을 HTML 글자로 통째로 갈아 끼우는 속성입니다. 넣은 글자를 태그로 해석하므로, 사용자 입력을 섞으면 XSS 가 열립니다.

template 안의 조각은 화면에만 안 보이는 것이 아닙니다. 스타일로 숨긴 요소와 견주면 차이가 드러납니다. 숨긴 요소는 보이지만 않을 뿐 나머지 일은 다 합니다.

template 안의 조각 스타일로 숨긴 요소
<img> 의 그림 내려받기 ✗ ✓
<script> 실행 ✗ ✓
DOM 에서 요소를 찾는 함수에 걸림 ✗ ✓

이렇게 되는 것은 조각이 DOM 밖에 있기 때문입니다. 페이지의 DOM 에는 template 요소 자신만 붙어 있습니다. 그 안의 조각은 브라우저가 따로 떼어 담아 둡니다.

template 요소의 content 속성이 그 조각을 꺼내 줍니다. 꺼낸 조각은 DocumentFragment 라는 그릇에 담겨 있습니다. DocumentFragment 는 어느 DOM 에도 붙지 않은 채 요소 여러 개를 담아 두는 그릇입니다.

아래 HTML 은 빈 목록 하나와 목록 한 줄짜리 템플릿을 둡니다.

HTML
<ul></ul>
<template id="row">
  <li></li>
</template>

아래 스크립트는 템플릿에서 한 줄을 복제해 값을 채운 뒤 목록에 붙입니다. 둘째 줄은 content 로 꺼낸 것이 DocumentFragment 임을 보입니다. 그 뒤로는 붙이기 전과 뒤에 DOM 안의 <li> 개수를 셉니다.

JavaScript
const t = document.getElementById("row");
t.content.nodeName; // "#document-fragment"
document.querySelectorAll("li").length; // 0

const row = t.content.cloneNode(true);
row.querySelector("li").textContent = "유미";
document.querySelector("ul").append(row);
document.querySelectorAll("li").length; // 1

아래 그림은 이 코드가 붙이기 전과 뒤에 무엇이 어디에 있는지 보입니다.

flowchart TD
    subgraph P["붙이기 전"]
        D1["DOM"] --> U1["ul · 비어 있다"]
        D1 --> T1["template 요소"]
        T1 -. content .-> F1["DocumentFragment · DOM 밖"]
        F1 --> L1["li 원본"]
    end
    subgraph Q["붙인 뒤"]
        D2["DOM"] --> U2["ul"]
        U2 --> L2["li 복사본 · 유미"]
        D2 --> T2["template 요소"]
        T2 -. content .-> F2["DocumentFragment · DOM 밖"]
        F2 --> L3["li 원본 · 그대로"]
    end
    P -- "cloneNode 로 복제해 붙인다" --> Q

template 요소는 DOM 안에 있어서 getElementById 로 찾힙니다. 그런데 첫 셈이 0 인 것은 그 속의 <li> 가 DOM 밖 DocumentFragment 에 있기 때문입니다. cloneNode(true) 는 조각을 안쪽 요소까지 모두 복사합니다. 복사본을 DOM 에 붙이고 나서야 <li> 가 화면에 나오고 셈에도 잡힙니다. 원본 조각은 남아 있어서 몇 번이든 다시 복제할 수 있습니다.

값은 textContent 로 넣었습니다. textContent 는 innerHTML 과 달리 넣은 값을 HTML 로 해석하지 않고 글자로만 씁니다. 그래서 서버 템플릿과 달리 이스케이프를 따로 챙기지 않아도 됩니다.

웹 컴포넌트 안의 template 요소

<template> 요소는 웹 컴포넌트의 재료 하나입니다. 웹 컴포넌트는 브라우저 표준만으로 나만의 태그를 만드는 기술 묶음입니다. 새 태그의 속 구조를 템플릿으로 적어 둡니다. 그 태그가 페이지에 생길 때마다 이 템플릿을 복제해 씁니다.

복제한 조각은 대개 그 태그의 섀도 DOM 에 붙입니다. 섀도 DOM 은 바깥 페이지와 스타일이 섞이지 않게 떼어 둔 작은 DOM 입니다. 바깥 페이지에 적은 스타일 규칙이 컴포넌트 안의 요소를 골라 꾸미지 못합니다.

템플릿 안에는 slot 요소를 둘 수 있습니다. <slot> 태그로 적는 이 요소는 컴포넌트를 쓰는 쪽이 넣은 내용이 꽂히는 빈칸입니다. 틀은 컴포넌트가 정합니다. 알맹이는 쓰는 쪽이 채웁니다.

React 나 Vue.js 를 쓰는 화면에서는 행을 찍어 내는 일을 라이브러리가 맡습니다. 그래서 <template> 요소를 직접 다룰 일이 드뭅니다. 서버가 행까지 다 그려 보내는 페이지에도 필요하지 않습니다.

어느 뜻인지 가르는 단서

대화나 문서에서 「HTML 템플릿」이 나오면 함께 나온 낱말을 봅니다. 대개 그것만으로 어느 뜻인지 갈립니다.

함께 나오는 말 뜻
템플릿 엔진 · 렌더링 · {{ }} · 뷰 · 컨트롤러 · Jinja · Thymeleaf 첫째 · 빈칸을 남긴 글자 틀
<template> · content · cloneNode · 웹 컴포넌트 · 섀도 DOM · <slot> 둘째 · 브라우저의 template 요소

뜻을 골랐으면 두 물건이 다루는 것도 다르다는 점을 떠올립니다. 아래 표는 앞 절들에서 본 차이를 한데 모은 것입니다.

글자 틀 template 요소
정체 빈칸을 남긴 HTML 글자(문자열) 그리지 않고 담아 둔 DOM 조각
채우는 곳 주로 서버, 프론트엔드 라이브러리면 브라우저 브라우저
서버가 보내는 것 서버에서 채우면 빈칸이 다 채워진 HTML 비어 있는 조각이 든 HTML
XSS 를 막는 방법 엔진의 이스케이프 textContent 로 값 넣기

Vue.js 의 싱글 파일 컴포넌트는 파일 맨 위에 <template> 블록을 둡니다. 태그 이름은 같지만 브라우저의 template 요소가 아닙니다. 이 블록은 빈칸을 둔 글자 틀, 곧 첫째 뜻입니다. 빌드 도구가 미리 자바스크립트 코드로 바꿉니다.

관련 항목

HTML 템플릿을 채워 HTML 을 만드는 엔진

템플릿 엔진 · Jinja · Thymeleaf · ERB · Mustache · Handlebars · Django 템플릿

서버 템플릿이 쓰이는 화면 렌더링 방식

서버 사이드 렌더링 · 클라이언트 사이드 렌더링 · 렌더링 · 하이드레이션 · MVC · 뷰 · 웹 프레임워크

템플릿에 값을 넣을 때 막아야 하는 공격과 방어

교차 사이트 스크립팅 · 이스케이프 · HTML 인코딩 · 콘텐츠 보안 정책 · innerHTML · textContent

template 요소와 함께 쓰는 웹 컴포넌트 기술

웹 컴포넌트 · 커스텀 엘리먼트 · 섀도 DOM · slot 요소 · DocumentFragment · DOM

빈칸을 남긴 틀로 HTML 을 짓는 프론트엔드 문법

템플릿 리터럴 · JSX · Vue.js · Angular · 싱글 파일 컴포넌트 · React

HTML 템플릿이 찍어 내는 문서와 그 표준

HTML · HTML Living Standard · HTML 요소 · HTML 메일

다른 이름: HTML template · HTML templates · template 요소 · 템플릿 요소