유효한 HTML 문서의 최소 조건, validator로 직접 확인했습니다
HTML 문서를 처음 배울 때 저는 "최소한 doctype, head, body 세 가지는 있어야 한다"고 외웠습니다. 빈 문서를 열면 늘 이 세 줄부터 적었고, 그게 HTML의 골격이라고 오래 믿어왔습니다. 그런데 어느 날 문득, 이 통념의 두 군데가 사실은 부정확하다는 것을 알게 되었습니다. 머리로만 그렇게 믿어온 셈이라, 이번에는 검사기로 직접 확인해보기로 했습니다.
DOCTYPE은 '요소'가 아닙니다
첫 번째로 짚이는 곳은 DOCTYPE입니다. 흔히 head, body와 나란히 묶어 "세 개의 요소"처럼 이야기하지만, DOCTYPE은 요소(element)가 아니라 문서 형식 선언(declaration)입니다. 여는 태그도, 닫는 태그도, 그 안에 담기는 콘텐츠도 없습니다. 브라우저를 표준 모드로 켜는 스위치에 가깝습니다.
두 번째로, 저 통념은 정작 가장 바깥을 빠뜨리고 있었습니다. head와 body를 감싸는 루트 요소, 바로 <html>입니다. 엄밀히 따지면 "골격"을 이야기하면서 골격의 몸통을 빼놓고 있었던 셈입니다. 이쯤 되니 제가 외워온 문장이 생각보다 허술했다는 생각이 들었습니다.
공식 스펙은 무엇을 '필수'라고 말하는가
오해를 바로잡으려면 2차 정보가 아니라 1차 소스를 봐야 한다고 생각했습니다. HTML의 기준 문서인 WHATWG HTML Living Standard 원문을 펼쳐 하나씩 확인했습니다.
먼저 DOCTYPE에 대해 스펙은 이렇게 표현합니다.
A DOCTYPE is a required preamble.
'필수 서두(required preamble)'라는 표현 그대로, 없으면 브라우저는 옛 버그와의 호환을 위한 쿼크 모드(quirks mode)로 렌더링합니다. 있어야 비로소 표준 모드가 됩니다. title에 대해서는 <head>의 콘텐츠 모델을 "…of which exactly one is a title element…"라고 규정합니다. 일반 문서라면 제목 요소가 정확히 하나는 있어야 한다는 뜻입니다. 예외는 iframe의 srcdoc 문서이거나, 상위 프로토콜이 제목을 따로 제공하는 경우 정도입니다.
가장 흥미로웠던 것은 문자 인코딩 선언, 즉 charset이었습니다. 이건 무조건 필수가 아니라 조건부였습니다.
If an HTML document does not start with a BOM, and its encoding is not explicitly given by Content-Type metadata … then the encoding must be specified using a meta element with a charset attribute…
정리하면 BOM으로 시작하지도 않고, HTTP Content-Type 헤더로 인코딩이 주어지지도 않은 평범한 문서라면 <meta charset>으로 인코딩을 밝혀야 합니다. 게다가 그 선언은 문서 첫 1024바이트 안에 와야 합니다. 그리고 <html>, <head>, <body>의 여는·닫는 태그는 사실 모두 생략할 수 있습니다. 적지 않아도 파서가 DOM에 알아서 만들어 넣기 때문입니다.
그래서 가장 엄밀한 최소 문서는 세 줄
스펙의 조각들을 모아보니, 일반적인 상황에서 가장 엄밀한 최소 유효 문서는 다음 세 줄로 정리되었습니다.
<!DOCTYPE html>
<meta charset="utf-8">
<title>제목</title>
<html>도 <head>도 <body>도 한 줄 적지 않았지만, 이것으로 유효합니다. 여기서 charset이 조건부라는 점을 떠올리면 한 줄을 더 덜어낼 수 있습니다. 서버가 Content-Type: text/html; charset=utf-8로 응답해 인코딩을 알려준다면, 문서 안의 <meta charset>은 생략할 수 있습니다. 그 경우 절대적인 최소는 DOCTYPE과 title 두 가지가 됩니다.
믿지 말고 돌려보기 — W3C validator 실측
여기까지는 스펙을 읽고 정리한 것이라, 결국 머릿속 추론입니다. 그래서 정말 그런지 직접 검증해보기로 했습니다. W3C Nu HTML Checker에 curl로 문서를 그대로 POST해서 결과를 받았습니다.
curl -s -H "Content-Type: text/html; charset=utf-8" \
--data-binary @doc.html \
"https://validator.w3.org/nu/?out=json"
Content-Type 헤더에 charset을 넣고 빼는 것으로 인코딩 제공 여부까지 통제할 수 있었습니다. 다섯 가지 경우를 만들어 차례로 돌려본 결과는 다음과 같았습니다.
| 케이스 | 결과 |
|---|---|
| 세 줄 문서 (DOCTYPE + meta charset + title) | 통과 — 에러 0 (lang 권장 경고 1건) |
title 제거 |
에러 — Element "head" is missing a required instance of child element "title" |
| DOCTYPE 제거 | 에러 — Start tag seen without seeing a doctype first |
| 문서·HTTP 헤더 모두 charset 없음 | 에러 — The character encoding was not declared |
| 문서엔 charset 없지만 HTTP 헤더가 제공 | 통과 — charset 조건부 규칙 그대로 |
추론한 그대로였습니다. 세 줄 문서는 깔끔하게 통과했고, title이나 DOCTYPE을 빼자 즉시 에러가 떨어졌습니다. 특히 마지막 두 줄이 인상적이었습니다. 문서 안에 charset이 없어도 HTTP 헤더가 인코딩을 알려주면 통과했고, 양쪽 모두 없을 때만 에러가 났습니다. 스펙이 말한 '조건부'가 실제로 그렇게 동작한다는 것을 눈으로 확인한 셈입니다.
검증이 알려준 두 가지
실측을 하다 보니 스펙만 읽었을 때는 보이지 않던 것이 두 가지 눈에 들어왔습니다.
하나는, 통과한 세 줄 문서에 붙은 경고가 존재하지도 않는 <html> 시작 태그에 lang을 붙이라고 말한다는 점이었습니다. 제가 적지도 않은 태그를 검사기가 지목한 것입니다. 이건 곧 파서가 html, head, body를 자동으로 구성해 DOM에는 늘 존재한다는 증거였습니다. 생략은 소스의 이야기일 뿐, DOM에서는 빠지지 않는다는 것을 다시 확인했습니다.
다른 하나는, 그 lang이 에러가 아니라 경고(warning)였다는 점입니다. 유효성 자체에는 영향을 주지 않지만, 접근성을 위해 권장된다는 의미입니다. '유효한 문서'와 '권장되는 문서'가 같지 않다는 것을, 검사기가 에러와 경고를 나누는 방식으로 보여주고 있었습니다.
최소는 확인했지만, 권장은 다른 이야기
이렇게 가장 엄밀한 최소를 직접 확인했지만, 그렇다고 실무에서 이 세 줄로 문서를 짜야 한다는 뜻은 아닙니다. 오히려 반대에 가깝습니다. 실제 프로젝트에서는 <html lang="ko">로 언어를 밝히고, <head>와 <body>를 명시적으로 적는 편이 협업과 가독성, 그리고 접근성 모두에 이롭습니다. 최소 유효(valid)와 권장(recommended)은 같은 자리가 아니라는 생각이 들었습니다.
다만 이번에 얻은 것은 세 줄짜리 문서 그 자체가 아니라, 오래 당연하게 여겨온 지식을 한 번쯤 1차 소스로 되짚고 도구로 직접 돌려본 경험이었습니다. 머리로 외운 통념과 실제로 검증한 사실 사이에는 늘 작은 틈이 있고, 그 틈을 직접 메워보는 일이 결국 가장 정확한 공부였다는 생각이 듭니다.
관련 글
공개된 정보만으로 웹사이트 기술스택 추론하기 — F12와 curl이면 충분합니다
곧 손대야 할 사이트의 코드를 받기 전, 브라우저 개발자도구와 curl만으로 프론트·백엔드 기술스택을 가늠해본 과정입니다. 프레임워크 시그니처와 응답 헤더·쿠키 단서 읽기, 'next' 단어에 헛짚은 사례, 추정과 확정을 등급으로 가르는 정직함까지 담았습니다.
웹 기초로 웹게임 만들기 — HTML·JavaScript·CSS로 돌아간 이유
데이터베이스 없이 HTML, JavaScript, CSS 세 가지만으로 웹게임을 만들 수 있습니다. 새로운 기술을 좇기보다 웹의 기본으로 돌아가, 프로그래밍 개념을 화면으로 확인하는 교육에 쓰려는 작은 시도를 정리했습니다.
Claude Code 라우팅 매트릭스 빈 행 3개를 새 전문 에이전트로 메운 회고
Claude Code 라우팅 매트릭스를 글로벌 CLAUDE.md 와 review-loop SKILL.md 에 박은 직후, 24행을 다시 들여다보니 fallback 으로 흘러갈 수밖에 없던 도메인 세 개가 어렵지 않게 떠올랐습니다. devops-engineer · dba · test-data-verifier 세 전문 에이전트를 더하며 만난 정의 작업의 무게를 정리한 후속편입니다.