안녕하세요! 프론트엔드 강사입니다.
HTML과 CSS를 다루다 보면 누구나 한 번쯤 겪는 미스터리한 현상이 있습니다. "어? 나는 분명히 줄바꿈이랑 들여쓰기만 했는데 왜 화면에 요상한 빈칸이 생기지?" 혹은 "엔터를 쳤는데 왜 브라우저에선 다닥다닥 붙어 나오지?" 이런 고민 해보셨나요?
그 비밀은 바로 브라우저가 '공백(Whitespace)'을 처리하는 독특한 규칙에 있습니다. 오늘은 MDN 공식 문서를 통해 이 공백이 DOM과 CSS에서 각각 어떻게 처리되는지, 그리고 우리를 괴롭히는 이 공백 문제들을 어떻게 깔끔하게 해결할 수 있는지 완벽하게 파헤쳐 보겠습니다!
DOM(문서 객체 모델) 안에 존재하는 공백(whitespace)은 그 위치에 따라 레이아웃을 망가뜨리거나 콘텐츠 트리 조작을 예상치 못하게 어렵게 만들 수 있습니다. 이 문서는 언제 이런 어려움이 발생하는지 살펴보고, 그로 인한 문제들을 어떻게 완화할 수 있는지 알아봅니다.
공백(Whitespace) 문자는 프로그래밍 언어의 문맥에 따라 서로 다른 문자들로 구성됩니다. CSS 공백 처리 규칙에서 말하는 문서 공백 문자 (Document white space characters)는 딱 4가지만 포함합니다.
우리는 이 문자들을 코드를 예쁘게 정렬하고 읽기 쉽게 만들기 위해(포맷팅) 사용합니다. 그래서 소스 코드에는 이 공백 문자들로 가득 차 있으며, 파일 크기를 줄이기 위해 프로덕션 빌드 단계(Minification)에서나 이것들을 싹 지워버리곤 하죠.
👨🏫 강사님의 꿀팁:
이 목록에 단어 잘림 방지 공백 (non-breaking spaces, / U+00A0)은 포함되지 않는다는 점을 꼭 기억하세요!
는 일반 공백 규칙의 지배를 받지 않아서 여러 개를 연속해서 써도 하나로 합쳐지지(collapse) 않습니다. 그래서 HTML에서 억지로 긴 빈칸을 띄우고 싶을 때 개발자들이 이 를 도배하곤 하는 겁니다.
또한, CSS는 HTML 문맥에서는 LF(라인 피드) 문자와 동일한 세그먼트 브레이크(segment breaks)라는 개념도 정의하고 있습니다.
"HTML은 공백을 무시한다"는 말은 프론트엔드 세계의 아주 흔한 미신입니다. 진실은 다릅니다! HTML은 여러분이 소스 코드에 작성한 모든 공백 텍스트 콘텐츠를 고스란히 보존합니다. HTML은 마크업 언어로서, 텍스트 콘텐츠 안의 모든 공백이 살아있는 DOM 트리를 만들어냅니다. 그래서 자바스크립트의 Node.textContent 같은 DOM API를 통해 이 공백들을 그대로 가져오거나 조작할 수 있죠. 만약 HTML 파서가 DOM에서 공백을 맘대로 지워버린다면, DOM을 가져다 렌더링하는 CSS 역시 white-space 속성을 써서 공백을 살려낼 방법이 없을 것입니다.
참고 (Note):
헷갈리지 마세요! 지금 우리가 이야기하는 것은 HTML 태그 사이에 존재하여 DOM에서 텍스트 노드(text nodes)가 되는 공백들을 말합니다. <div class="box">처럼 태그 안(꺾쇠 괄호 내부)에 쓴 공백은 그냥 HTML 문법의 일부일 뿐이며, DOM 트리에는 나타나지 않습니다.
다음 문서를 예로 들어볼까요:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>My Document</title>
</head>
<body>
<h1>Header</h1>
<p>Paragraph</p>
</body>
</html>
이 코드의 DOM 트리는 아래와 같이 생겼습니다:
다음을 주의 깊게 보세요:
<head>와 <body> 사이)DOM에 공백 문자를 그대로 보존하는 것은 여러모로 유용하지만, 때로는 특정 레이아웃을 구현하기 까다롭게 만들고, 자바스크립트로 DOM 노드를 순회(iterate)하려는 개발자들을 골치 아프게 만들 수 있습니다. 이 문제와 해결책은 공백 노드 문제 해결하기 섹션에서 알아보겠습니다.
HTML이 만들어낸 DOM 트리가 렌더링을 위해 CSS 엔진으로 넘어가면, 이때부터 기본적으로 공백이 대거 잘려나갑니다(stripped). 즉, 여러분이 코드 에디터에서 들여쓰기를 얼마나 예쁘게 했든 간에 최종 사용자 눈에는 보이지 않습니다. 요소들 주변이나 내부에 여백을 만드는 것은 스페이스바가 아니라 CSS의 고유한 역할이기 때문이죠.
<!doctype html>
<h1> Hello World! </h1>
이 소스 코드는 맨 위 선언문 뒤에 두 번의 줄바꿈이 있고, <h1> 태그 앞, 안쪽, 뒤쪽에 스페이스가 잔뜩 있습니다. 하지만 브라우저는 이 스페이스들을 무시하고 마치 이런 공백들이 처음부터 없었던 것처럼 "Hello World!"라는 단어 두 개만 화면에 띄웁니다.
그렇다고 모든 공백이 사라진 건 아닙니다! "Hello"와 "World!" 사이에 있던 수많은 스페이스 중 단 하나는 브라우저 렌더링에 살아남았죠. CSS는 도대체 어떤 공백을 지우고 어떤 공백을 살려둘지 결정하기 위해 아주 구체적인 알고리즘을 사용합니다.
이해를 돕기 위해 공백 문자가 눈에 보이도록 기호로 치환해 보겠습니다. 스페이스는 ◦, 탭은 ⇥, 줄바꿈(엔터)은 ⏎ 기호로 표시했습니다.
<h1> Hello
<span> World!</span> </h1>
이 <h1> 요소는 다음 3가지를 포함합니다:
1. 텍스트 노드 ("Hello" 단어 앞뒤로 스페이스와 줄바꿈, 탭이 섞여 있음)
2. 인라인 요소 (<span>, 안에 스페이스 하나와 "World!"라는 단어가 있음)
3. 텍스트 노드 (<span> 뒤에 탭과 스페이스들이 있음)
<h1> 요소 안에는 인라인(inline) 요소들만 있기 때문에, 이것은 인라인 서식 맥락(inline formatting context)을 형성합니다. 이 맥락 안에서 공백은 다음 순서대로 뼈를 깎는 가공 처리를 거칩니다:
참고: 이 알고리즘은 white-space-collapse(또는 단축 속성 white-space) 속성으로 설정을 바꿀 수 있습니다. 일단은 기본값인 white-space-collapse: collapse 상황이라고 가정하고 보겠습니다.
⏎) 바로 앞이나 바로 뒤에 있는 모든 스페이스와 탭이 지워집니다.<h1>◦◦◦Hello◦⏎⇥⇥⇥⇥<span> → <h1>◦◦◦Hello⏎<span> (엔터 앞의 ◦ 하나와 뒤의 ⇥ 네 개가 날아감)⏎)가 스페이스(◦)로 변신합니다.<h1>◦◦◦Hello⏎<span> → <h1>◦◦◦Hello◦<span>⇥)도 스페이스(◦)로 변신합니다.</span>⇥◦◦</h1> → </span>◦◦◦</h1><h1>◦◦◦Hello◦<span>◦World!</span>◦◦◦</h1><h1>◦Hello◦<span>World!</span>◦</h1> 자, 이 과정을 거쳤기 때문에 사용자는 들여쓰기가 난무했던 소스코드 대신, 아주 깔끔하게 "Hello World!"가 적힌 화면을 보게 되는 것입니다.
👨🏫 강사님의 요약:
CSS의 기본 공백 처리 규칙은 무조건 "모든 탭과 줄바꿈은 스페이스 1개로 바꾸고, 연속된 스페이스들은 무조건 1개로 압축한다!" 입니다. 이 규칙만 기억하시면 레이아웃 짤 때 절대 헷갈리지 않으실 거예요.
텍스트 래핑(wrapping)으로 만들어지는 '인라인' 맥락이 아니라, 각각의 블록이 하나의 줄을 형성하는 '블록(block)' 포맷팅 맥락에서는 렌더링 과정에서 공백이 한 번 더 다듬어집니다.
<body>
<div> Hello </div>
<div> World! </div>
</body>
<body>◦<div>◦Hello◦</div>◦<div>◦World!◦</div>◦</body><body>가 만든 블록 서식 맥락에 따라 이 요소들이 배치됩니다. 블록 요소인 <div> 앞뒤의 텍스트 노드(스페이스들)가 각각 5개의 독립된 '줄(line)'을 만듭니다.결과적으로 맨 앞, 중간, 맨 끝에 있던 3개의 "오직 공백으로만 이루어진 텍스트 노드"들은 아무런 시각적 콘텐츠가 남지 않게 되어 화면에서 차지하는 공간이 완전히 사라집니다(0px). 그래서 우리는
자, CSS가 공백을 싹둑싹둑 잘라내는 걸 보셨죠? 하지만 명심하세요. HTML은 DOM에 공백을 그대로 보존합니다.
그래서 자바스크립트로 Node.textContent를 찍어보면 HTML 코드에 적어둔 엔터와 탭이 그대로 다 출력됩니다. Node.childNodes를 호출하면 화면엔 보이지도 않는 '오직 공백으로만 된 텍스트 노드'들까지 몽땅 배열에 잡히죠.
하지만 렌더링된 후의 텍스트(즉, CSS가 공백을 다 압축해 버린 상태의 텍스트)를 가져오도록 설계된 API들도 있습니다.
HTMLElement.innerText: 화면에 보이는 그대로(공백이 압축되고 잘린 상태)의 텍스트를 반환합니다.Selection.toString(): 사용자가 드래그해서 복사할 때의 텍스트를 반환하므로, 역시 공백이 압축된 상태입니다.사용자 눈에는 안 보이는 이 공백 노드들이 개발자들의 뒤통수를 치는 대표적인 두 가지 사례를 살펴보겠습니다.
이건 CSS를 처음 배우는 모든 분이 한 번쯤 겪는 통과의례 같은 버그입니다!
inline-block 속성은 정말 편리하죠. 겉보기엔 가로로 나열되는 inline 같으면서도, 내부는 너비/높이를 가질 수 있는 block처럼 행동하니까요. 내비게이션 메뉴나 아바타 리스트를 만들 때 자주 씁니다.
그런데 문제가 있습니다. inline 요소들은 글자처럼 취급받습니다. 그래서 요소와 요소 사이에 있는 HTML의 줄바꿈이나 탭이 CSS 파싱 규칙에 의해 '스페이스 1개'로 변환되어 화면에 렌더링 돼버립니다! 분명히 블록(박스)을 나란히 붙였는데, 박스들 사이에 알 수 없는 미세한 틈새가 생겨버리는 거죠.
<ul class="people-list">
<li></li> <li></li> </ul>
.people-list li {
display: inline-block;
width: 2em; height: 2em;
background: #ff0066;
}
이 지긋지긋한 틈새 문제를 해결하는 방법은 다음과 같습니다:
[추천] Flexbox 사용하기: 가장 현대적이고 완벽한 방법입니다. display: inline-block을 버리고 부모인 <ul>에 display: flex를 주세요! 플렉스박스는 자식들 사이의 텍스트 공백 노드를 완전히 무시해 버리기 때문에 여백이 싹 사라집니다.
부모 요소의 폰트 사이즈를 0으로 만들기:
여백의 정체는 결국 '글자(스페이스)'니까, 부모인 <ul>에 font-size: 0;을 주면 공백 크기도 0이 되어 사라집니다. 단, 자식인 <li>에 다시 폰트 사이즈를 선언해 줘야 하는 번거로움이 있습니다.
HTML 태그를 징그럽게(?) 붙여 쓰기:
애초에 공백이 안 생기게 HTML 코드 자체를 붙여버리는 원시적인 방법입니다.
<li>
...
</li><li> ...
</li>
HTML 소스 코드의 엔터와 탭이 DOM 트리 안에서 버젓이 '텍스트 노드'로 살아있기 때문에, 자바스크립트로 DOM을 탐색할 때 치명적인 버그를 유발합니다.
예를 들어, 특정 요소의 첫 번째 자식을 가져오고 싶어서 element.firstChild를 호출했는데, 여러분이 기대했던 <div> 태그가 아니라 줄바꿈 기호(\n)가 들어있는 투명한 텍스트 노드가 튀어나와서 .className을 찾지 못하고 에러를 뿜는 것이죠.
👨🏫 강사님의 꿀팁:
이 문제를 우회하려면 Element(태그)만 콕 집어서 찾아주는 최신 DOM API를 사용해야 합니다!
firstChild대신 👉firstElementChild사용lastChild대신 👉lastElementChild사용nextSibling대신 👉nextElementSibling사용이렇게
~Element~가 붙은 메서드들을 사용하면 귀찮은 공백 텍스트 노드들을 브라우저가 알아서 쏙 빼고 진짜 HTML 태그들만 탐색해 줍니다. 코드가 훨씬 안전해지겠죠?
이 페이지가 도움이 되었나요? (Was this page helpful to you?)
[예 (Yes)]
[아니요 (No)]
이 페이지는 MDN 기여자들에 의해 2025년 12월 15일에 마지막으로 수정되었습니다.
어떠셨나요? 공백이 어떻게 처리되는지 그 속사정을 알고 나니, 원인 모를 여백이나 자바스크립트 DOM 에러가 왜 발생했는지 속 시원하게 이해되셨을 겁니다. 이 지식은 프론트엔드 트러블슈팅의 핵심이니 잊지 마세요!