HTML에서 대부분의 요소는 여는 태그와 닫는 태그 사이에 콘텐츠를 담는 구조를 갖습니다. 그러나 일부 요소는 태생적으로 콘텐츠를 가질 수 없도록 설계되어 있으며, 이러한 요소를 Void 요소라고 부릅니다. Void 요소는 HTML 콘텐츠 모델에서 “빈 콘텐츠(nothing)“를 갖는 특수한 카테고리로 분류되며, <br>, <img>, <input>, <hr>, <meta>, <link> 등이 대표적입니다. 이 요소들이 왜 닫는 태그를 갖지 않는지, 그리고 자기 닫힘 표기(<br />)는 어떤 의미를 갖는지 이해하는 것은 HTML의 구조적 원리를 파악하는 데 필수적입니다. Void 요소의 개념을 명확히 알면, HTML 파서의 동작 방식과 올바른 마크업 작성법을 더 깊이 이해할 수 있습니다.
🔍 핵심 특징
- 닫는 태그 불필요: Void 요소는 HTML 명세에 의해 콘텐츠를 가질 수 없으므로, 닫는 태그(
</br>,</img>등)를 사용하지 않으며 사용해서도 안 됩니다 - 콘텐츠 모델이 “nothing”: 일반 요소의 콘텐츠 모델은 Flow, Phrasing 등의 카테고리를 허용하지만, Void 요소는 허용되는 콘텐츠가 아예 없습니다
- 자기 닫힘 표기의 오해:
<br />과 같은 자기 닫힘 태그는 XHTML에서 필수였으나, HTML5에서는 선택 사항이며 실제 파서 동작에 영향을 주지 않습니다 - 속성을 통한 기능 정의: Void 요소는 내부 콘텐츠 대신 속성(attribute)을 통해 자신의 역할과 데이터를 전달합니다 (예:
img의src,input의type) - DOM 트리에서 자식 노드 없음: Void 요소는 DOM에서 자식 노드를 가질 수 없으며, JavaScript로 자식을 추가하려 해도 명세 위반이 됩니다
💡 실무에서의 영향
Void 요소에 대한 정확한 이해는 실무에서 여러 방면으로 영향을 미칩니다. 먼저, 잘못된 마크업 작성을 방지할 수 있습니다. 예를 들어 <br> 안에 텍스트를 넣거나 <input>에 닫는 태그를 붙이는 실수는 HTML 검증 도구에서 오류로 잡히며, 브라우저마다 다른 방식으로 오류를 복구하기 때문에 크로스 브라우저 이슈의 원인이 될 수 있습니다. 또한 React나 JSX 환경에서는 모든 태그를 닫아야 하므로 자기 닫힘 표기(<br />)가 필수적인데, 이것이 HTML5 자체의 규칙이 아닌 JSX의 요구사항임을 구분할 수 있어야 합니다. 컴포넌트를 설계할 때도 Void 요소를 children으로 감싸는 패턴은 성립하지 않으므로, 래퍼 컴포넌트 구조를 설계할 때 이를 고려해야 합니다. SEO와 접근성 측면에서도 <img>의 alt 속성, <meta>의 description 등 Void 요소의 속성이 핵심적인 역할을 하므로, 이들의 특성을 정확히 이해하는 것이 중요합니다.
핵심 개념
Void 요소의 정의와 종류
입문
HTML에서 어떤 태그들은 안에 아무것도 넣을 수 없게 만들어져 있어요. 이런 태그를 ‘Void 요소’라고 불러요.
📦 Void가 뭔가요? Void는 영어로 ‘비어 있는’이라는 뜻이에요. 마치 택배 상자 중에 뚜껑을 열 수 없는 상자가 있다고 생각해보세요. 이 상자는 처음부터 물건을 넣을 수 없게 만들어진 거예요. HTML에서도 이렇게 안에 아무것도 넣을 수 없는 특별한 태그가 있답니다.
🖼️ 어떤 것들이 있나요?
우리가 자주 쓰는 Void 요소들이 있어요. 줄을 바꾸는 <br>, 사진을 보여주는 <img>, 글자를 입력하는 칸 <input>, 가로줄을 긋는 <hr> 같은 것들이에요. 이 태그들은 모두 안에 다른 내용을 담을 수 없어요.
🎯 왜 안에 내용을 못 넣나요?
생각해보면 당연해요! 줄바꿈(<br>)은 그 자체가 ‘줄을 바꿔라’라는 명령이에요. 줄바꿈 안에 글자를 넣는 건 말이 안 되죠? 사진(<img>)도 마찬가지예요. 사진 안에 다른 사진을 넣는 게 아니라, 사진 자체를 보여주는 거니까요.
💡 그럼 정보는 어떻게 전달하나요?
뚜껑이 없는 상자에는 겉면에 스티커를 붙이듯이, Void 요소는 ‘속성’이라는 것으로 정보를 전달해요. 예를 들어 <img>는 어떤 사진을 보여줄지 겉면에 적어두는 거예요. 이 겉면에 적는 정보를 ‘속성(attribute)‘이라고 해요.
중급
Void 요소는 HTML 명세에서 콘텐츠 모델이 “nothing”으로 정의된 요소입니다. 닫는 태그를 가질 수 없으며, 자식 노드도 가질 수 없습니다.
HTML Living Standard 기준 Void 요소 목록
HTML 명세에서 정의하는 Void 요소는 다음과 같습니다:
area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr
이 요소들은 모두 콘텐츠를 포함할 수 없으며, 속성(attribute)을 통해서만 기능을 정의합니다.
<!-- 일반 요소: 여는 태그 + 콘텐츠 + 닫는 태그 -->
<p>이것은 단락입니다.</p>
<div>이것은 div 컨테이너입니다.</div>
<!-- Void 요소: 여는 태그만 존재, 닫는 태그 없음 -->
<br>
<img src="photo.jpg" alt="사진">
<input type="text" placeholder="입력하세요">
<hr>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
속성 기반 기능 정의
Void 요소는 내부 콘텐츠 대신 속성으로 자신의 역할을 정의합니다. img는 src로 이미지 경로를, input은 type으로 입력 유형을, meta는 name과 content로 메타 정보를 전달합니다.
심화
Void 요소는 HTML Living Standard(WHATWG)에서 콘텐츠 모델이 “Nothing”으로 명시된 요소 집합으로, 파서 레벨에서 특수한 처리 경로를 거칩니다.
WHATWG HTML 명세의 Void 요소 정의
HTML Living Standard Section 13.1.2 “Elements”에서, Void 요소는 시작 태그(start tag)만 가질 수 있고 종료 태그(end tag)를 가질 수 없는 요소로 정의됩니다. 명세에서 나열하는 Void 요소는 area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr의 13개입니다.
각 요소의 콘텐츠 모델(Content Model)은 “Nothing”으로 명시되어 있으며, 이는 해당 요소가 텍스트 노드(Text Node)든 요소 노드(Element Node)든 어떠한 자식도 가질 수 없음을 의미합니다. 이 제약은 DOM 인터페이스 레벨이 아닌 콘텐츠 모델(Content Model) 레벨의 제약이므로, appendChild() API 호출 자체는 가능하지만 명세 위반(conformance error)이 됩니다.
HTML 파서의 Void 요소 처리 알고리즘 HTML 파서(HTML Parser)는 트리 구성(Tree Construction) 단계에서 Void 요소를 만나면 특수한 경로를 따릅니다. Section 13.2.6.4 “The rules for parsing tokens in HTML content”에서, 시작 태그 토큰(Start Tag Token)을 처리할 때 해당 요소가 Void 목록에 포함되면 즉시 “acknowledge self-closing flag” 처리 후 요소를 현재 노드의 자식으로 삽입하고, 스택에서 즉시 팝(pop)합니다. 일반 요소는 스택에 남아 이후 토큰들을 자식으로 수집하지만, Void 요소는 이 수집 과정이 생략됩니다.
만약 파서가 Void 요소에 대한 종료 태그(End Tag)를 만나면, 이를 파싱 에러(Parse Error)로 처리하고 해당 토큰을 무시합니다. 브라우저는 에러 복구(Error Recovery) 모드로 진입하여 렌더링을 계속하지만, 복구 동작은 명세에 정의되지 않은 구현 의존적(implementation-dependent) 행위입니다.
자기 닫힘 태그 표기법
입문
<br>을 <br />처럼 쓰는 사람도 있고 그냥 <br>로 쓰는 사람도 있어요. 이 차이가 뭔지 알아볼까요?
📝 두 가지 쓰는 방법이 있어요
같은 줄바꿈 태그를 <br>이라고 쓸 수도 있고, <br />이라고 쓸 수도 있어요. 마치 편지를 쓸 때 “안녕”이라고 쓰든 “안녕.”이라고 마침표를 붙이든 같은 인사인 것처럼요.
🕰️ 옛날에는 달랐어요
예전에 XHTML이라는 규칙이 있었는데, 이 규칙에서는 반드시 <br />처럼 슬래시(/)를 붙여야 했어요. 마치 선생님이 “문장 끝에 반드시 마침표를 찍어라!”라고 규칙을 정한 것과 같았죠.
🌐 지금의 HTML5에서는요?
지금 쓰는 HTML5에서는 슬래시를 붙여도 되고 안 붙여도 돼요. 브라우저는 둘 다 똑같이 이해해요. <br>이든 <br />이든 결과는 완전히 같아요.
⚛️ 그런데 React에서는 다르대요?
React라는 도구를 쓸 때는 <br />처럼 반드시 슬래시를 붙여야 해요. 이건 HTML의 규칙이 아니라 React라는 도구만의 규칙이에요. 마치 학교마다 교복 규칙이 다른 것처럼, 도구마다 규칙이 다른 거예요.
중급
자기 닫힘 태그(self-closing tag)는 <br />처럼 슬래시(/)를 포함하는 표기법입니다. HTML5에서는 Void 요소에 한해 선택적으로 허용되며, 파서 동작에 영향을 주지 않습니다.
표기법 비교
| 표기법 | 예시 | HTML5 유효성 | 비고 |
|---|---|---|---|
| 슬래시 없음 | <br> | 유효 | 권장 |
| 자기 닫힘 | <br /> | 유효 | XHTML 호환 |
| 닫는 태그 | <br></br> | 파싱 에러 | 사용 금지 |
<!-- HTML5: 슬래시 선택 사항 (둘 다 유효) -->
<br>
<br />
<img src="photo.jpg" alt="사진">
<img src="photo.jpg" alt="사진" />
<!-- XHTML: 슬래시 필수 (없으면 에러) -->
<!-- <br /> -->
<!-- <img src="photo.jpg" alt="사진" /> -->
// JSX: 슬래시 필수 (XML 기반 문법)
function Component() {
return (
<div>
<br /> {/* 슬래시 없으면 컴파일 에러 */}
<img src="photo.jpg" alt="사진" />
<input type="text" />
</div>
);
}
핵심 구분: HTML5와 JSX의 규칙 차이
HTML5에서 <br />의 슬래시는 파서가 무시하는 장식(decoration)입니다. 반면 JSX는 XML 기반 문법이므로 모든 태그를 닫아야 합니다. 이 차이를 이해하지 못하면, JSX의 요구사항을 HTML 표준으로 오해하는 실수가 발생합니다.
심화
자기 닫힘 태그 표기는 SGML, XML, HTML5라는 서로 다른 직렬화 형식(Serialization Format) 간의 역사적 차이에서 비롯된 문법적 유산입니다.
HTML5 파서의 자기 닫힘 플래그 처리
HTML Living Standard Section 13.2.5 “Tokenization”에서, 토크나이저(Tokenizer)는 시작 태그 토큰을 생성할 때 자기 닫힘 플래그(self-closing flag)를 설정할 수 있습니다. <br />을 만나면 토크나이저는 br 시작 태그 토큰에 self-closing flag를 true로 설정합니다.
그러나 트리 구성(Tree Construction) 단계에서 이 플래그의 처리는 요소 종류에 따라 달라집니다. Void 요소의 경우, self-closing flag가 “acknowledged(확인됨)” 처리되어 정상적으로 소비됩니다. 반면 일반 요소(예: <div />)에서 self-closing flag가 설정되면, 이 플래그는 acknowledged 되지 않고 파싱 에러가 발생합니다. 즉, <div />는 <div>와 동일하게 처리되며, 자기 닫힘으로 해석되지 않습니다.
XHTML과 HTML5의 직렬화 모델 차이
XHTML 1.0은 XML 직렬화(XML Serialization)를 사용하므로, XML의 빈 요소 태그(Empty Element Tag) 규칙이 적용됩니다. XML 1.0 명세 Section 3.1에서 빈 요소는 <element />으로 직렬화해야 하며, 닫는 태그 없이 여는 태그만 사용하면 웰폼(Well-formed) 위반입니다. 반면 HTML5는 자체 파서 명세를 가지므로, XML 규칙에 구속되지 않습니다.
HTML5의 XML 직렬화(XHTML5)를 사용할 경우 Content-Type: application/xhtml+xml로 서빙되며, 이때는 XML 파서가 처리하므로 Void 요소에도 자기 닫힘 표기가 필수입니다. 동일한 문서라도 MIME 타입(MIME Type, 미디어 유형 식별자)에 따라 다른 파서가 활성화되는 것입니다.
Void 요소의 콘텐츠 모델과 DOM
입문
Void 요소는 HTML의 규칙상 안에 아무것도 넣을 수 없어요. 이 규칙이 웹페이지의 구조(DOM)에서 어떤 의미를 갖는지 알아봐요.
🏗️ 웹페이지는 나무 구조예요
브라우저가 HTML을 읽으면 마치 가족 나무(가계도)처럼 구조를 만들어요. <html> 안에 <body>가 있고, <body> 안에 <div>가 있고, <div> 안에 <p>가 있는 식이에요. 이 구조를 DOM이라고 해요.
🍃 Void 요소는 나뭇잎이에요 나무에서 가지 끝에 달린 잎처럼, Void 요소는 항상 구조의 끝에 위치해요. 잎사귀에서 새로운 가지가 나올 수 없듯이, Void 요소 안에서 새로운 요소가 생길 수 없어요. 그래서 Void 요소는 DOM 나무의 ‘잎 노드’라고 불려요.
🚫 억지로 넣으면 어떻게 되나요?
만약 <br>여기에 글자</br> 이렇게 쓰면 어떻게 될까요? 브라우저는 이걸 오류로 판단하고, 나름대로 고치려고 해요. 그런데 문제는 브라우저마다 고치는 방법이 달라서 결과가 제각각일 수 있다는 거예요.
🔧 JavaScript로도 안 되나요? 프로그래밍으로 Void 요소 안에 뭔가를 넣을 수는 있지만, 이건 규칙 위반이에요. 마치 “주차 금지” 표지판이 있는 곳에 차를 세울 수는 있지만 딱지를 떼이는 것처럼, 기술적으로 가능하더라도 올바른 행동은 아니에요.
중급
Void 요소는 콘텐츠 모델이 “Nothing”이므로, DOM 트리에서 자식 노드를 가질 수 없는 리프 노드(leaf node)로 존재합니다.
DOM 트리에서의 위치 일반 요소는 DOM에서 부모-자식 관계를 형성하여 트리를 구성합니다. Void 요소는 이 트리의 말단(리프)에 위치하며, 텍스트 노드나 자식 요소 노드를 가질 수 없습니다.
// 일반 요소: 자식 노드를 가질 수 있음
const div = document.querySelector('div');
console.log(div.childNodes.length); // 1 이상 가능
console.log(div.children.length); // 자식 요소 포함
// Void 요소: 자식 노드가 없음
const br = document.querySelector('br');
console.log(br.childNodes.length); // 0
console.log(br.children.length); // 0
// JavaScript로 자식 추가 시도 (명세 위반)
const img = document.querySelector('img');
img.appendChild(document.createTextNode('텍스트'));
// 브라우저에서 실행은 되지만, HTML 명세 위반
// W3C 검증 도구에서 에러 보고됨
에러 복구와 크로스 브라우저 차이
Void 요소에 콘텐츠를 넣으려는 잘못된 마크업(예: <br>텍스트</br>)을 파서가 만나면, 에러 복구 알고리즘이 작동합니다. 이 복구 동작은 브라우저 간 일관성이 보장되지 않으므로, 올바른 마크업 작성이 중요합니다.
심화
Void 요소의 콘텐츠 모델 제약은 HTML 파서의 트리 구성 알고리즘과 DOM 인터페이스 양쪽에서 서로 다른 수준으로 적용됩니다.
콘텐츠 모델 “Nothing”의 명세적 의미 HTML Living Standard에서 콘텐츠 모델(Content Model)이 “Nothing”인 요소는 두 가지 제약을 받습니다. 첫째, 요소 간 공백(inter-element whitespace)을 포함한 어떤 텍스트 노드도 자식으로 가질 수 없습니다. 둘째, 어떤 요소 노드도 자식으로 가질 수 없습니다. 이는 콘텐츠 모델이 “Transparent(투명)“이거나 “Phrasing Content(구문 콘텐츠)“인 요소와 명확히 구별됩니다.
주의할 점은 이 제약이 DOM API 레벨이 아닌 적합성 요구사항(Conformance Requirement) 레벨이라는 것입니다. DOM 인터페이스인 Node.appendChild()와 Element.insertAdjacentHTML()은 요소의 콘텐츠 모델을 검사하지 않습니다. 따라서 imgElement.appendChild(textNode)는 DOM 조작으로서 성공하지만, 결과 DOM 트리는 HTML 적합성 검사(Conformance Checking)를 통과하지 못합니다.
HTML 파서의 에러 복구 동작 분석 파서가 Void 요소에 대한 종료 태그를 만나면 Section 13.2.6.4의 “Any other end tag” 항목에 따라 처리됩니다. 이때 파서는 열린 요소 스택(Stack of Open Elements)에서 해당 요소를 검색하는데, Void 요소는 시작 태그 처리 시 즉시 스택에서 팝(Pop) 되었으므로 스택에 존재하지 않습니다. 결과적으로 파싱 에러(Parse Error)가 발생하고 종료 태그 토큰은 무시됩니다.
<br>텍스트</br> 같은 잘못된 마크업의 경우, 실제 처리는 다음과 같습니다: <br> 토큰 → br 요소 생성 후 즉시 팝 → “텍스트” → br의 부모(parent) 노드의 자식으로 텍스트 노드 삽입 → </br> → 파싱 에러, 토큰 무시. 즉 텍스트는 br 요소의 자식이 아닌 형제(sibling) 노드가 됩니다. 이 동작은 HTML 명세에 정의된 에러 복구(Error Recovery) 알고리즘을 따르지만, 비표준 마크업에 대한 복구 동작이 모든 브라우저에서 동일하게 구현된다는 보장은 없으므로, Void 요소에는 콘텐츠를 포함하지 않는 것이 원칙입니다.