html entity encode decode

HTML 엔티티 인코더 및 디코더: 안전한 웹 콘텐츠 관리

XSS를 방지하고 모든 브라우저에서 특수 문자가 완벽하게 렌더링되도록 HTML 엔티티를 즉시 인코딩 및 디코딩하세요.

HTML 엔티티란 무엇인가?

HTML 엔티티는 HTML 문서에서 특별한 의미를 갖거나 직접 입력하기 어려운 문자를 표현하기 위한 특수 텍스트 시퀀스입니다. 엔티티는 &(앰퍼샌드)로 시작하고 ;(세미콜론)으로 끝납니다. 그 사이에는 설명적인 이름(명명 엔티티& 등) 또는 문자의 코드 포인트(숫자 엔티티& 또는 &)가 위치합니다.

HTML 엔티티는 단순한 문자 표현 도구가 아닙니다. 웹 보안의 핵심 요소입니다. 사용자 입력, CMS 데이터, 이메일 템플릿, 템플릿 엔진 등 동적 콘텐츠를 다루는 모든 웹 개발자는 HTML 엔티티의 작동 원리를 깊이 이해해야 합니다.


문자 인코딩의 역사

ASCII 시대 (1960년대~1980년대)

ASCII(American Standard Code for Information Interchange)는 128개의 문자를 정의했습니다. 영문자(대소문자), 숫자, 구두점, 제어 문자가 포함됩니다. 미국식 영어에는 충분했지만 세계의 다른 언어에는 완전히 불충분했습니다.

Latin-1 / ISO-8859-1 (1980년대~1990년대)

ISO-8859-1(Latin-1이라고도 함)은 8번째 비트를 사용하여 ASCII를 256자로 확장하고, 서유럽 언어에서 사용하는 악센트 문자(é, ü, ñ 등)를 추가했습니다. HTML 2.0과 HTML 3.2는 Latin-1을 참조 문자 집합으로 공식 채택하고, é(é), ü(ü) 등 많은 명명 엔티티를 정의했습니다.

그러나 256자로는 일본어, 아랍어, 중국어, 한국어 등의 문자 체계를 감당할 수 없었습니다. 각 지역이 호환되지 않는 독자적인 인코딩 방식(Shift-JIS, Big5, EUC-KR 등)을 개발하여, 인코딩이 혼재할 때 문자가 깨지는 '깨진 글자' 문제가 발생했습니다.

Unicode와 UTF-8 (1991년~현재)

Unicode Consortium은 1991년 첫 Unicode 표준을 발표했습니다. 목표는 세계 모든 문자 체계의 모든 문자에 고유한 코드 포인트를 할당하는 것입니다. 현재 Unicode는 150개 이상의 문자 체계에 걸쳐 14만 개 이상의 문자를 포함합니다.

UTF-8은 1992년 Ken Thompson과 Rob Pike가 고안했으며, Unicode 코드 포인트를 1~4바이트로 인코딩하고 ASCII와 하위 호환성을 유지합니다. 2000년대에 웹의 주요 인코딩 방식이 되었으며, 2024년 기준 98% 이상의 웹 페이지가 UTF-8을 사용합니다.

UTF-8 시대에도 엔티티가 필요한 이유

UTF-8로 모든 문자를 인코딩할 수 있다면 엔티티가 왜 여전히 필요할까요? 세 가지 이유가 있습니다:

  1. 예약된 문자: <, >, &는 HTML 마크업에서 특별한 의미를 가집니다. UTF-8 문서에서도 리터럴로 표시하려면 이스케이프가 필요합니다.
  2. 속성 구분자: "'는 속성 값을 구분하며, 속성 값 내에서 사용할 때는 이스케이프가 필요합니다.
  3. 공백 제어: &nbsp;(줄바꿈 없는 공백)는 일반 공백으로는 구현할 수 없는 레이아웃 제어를 제공합니다.

핵심 개념: HTML 엔티티의 작동 방식

명명 엔티티

명명 엔티티는 가장 가독성이 높은 형식으로, 문자 설명에서 유래한 니모닉 이름을 사용합니다. HTML5는 2,000개 이상의 명명 엔티티를 정의합니다.

<!-- 명명 엔티티 사용 예시 -->
<p>빵 &amp; 버터</p>              <!-- 표시: 빵 & 버터 -->
<p>3 &lt; 5이고 10 &gt; 7입니다</p>  <!-- 표시: 3 < 5이고 10 > 7입니다 -->
<p>저작권 &copy; 2026</p>         <!-- 표시: 저작권 © 2026 -->
<p>가격: 49&euro;</p>             <!-- 표시: 가격: 49€ -->

숫자 엔티티: 십진수와 십육진수

모든 Unicode 문자를 십진수 또는 십육진수 코드 포인트로 참조할 수 있습니다:

  • 십진수: &# 다음에 십진수 코드 포인트 — 예: &#60;<(U+003C)
  • 십육진수: &#x 다음에 십육진수 코드 포인트 — 예: &#x3C;<

두 형식은 동등합니다. Unicode 코드 포인트는 일반적으로 십육진수(U+003C)로 표현되기 때문에 기술 문서에서는 십육진수 형식이 자주 사용됩니다.

<!-- 다음 세 가지는 모두 동등하며 < 를 표시합니다 -->
&lt;
&#60;
&#x3C;

보안의 5가지 핵심 엔티티

이 5개 문자가 HTML 인젝션 방어의 기반을 이룹니다:

문자 명명 엔티티 십진수 십육진수 의미
< &lt; &#60; &#x3C; HTML 태그 시작
> &gt; &#62; &#x3E; HTML 태그 종료
& &amp; &#38; &#x26; 엔티티 시작
" &quot; &#34; &#x22; 큰따옴표 속성
' &apos; &#39; &#x27; 작은따옴표 속성

HTML 엔티티 참조 표

문자 명명 엔티티 십진수 십육진수 용도
< &lt; &#60; &#x3C; 태그 구분자
> &gt; &#62; &#x3E; 태그 구분자
& &amp; &#38; &#x26; 엔티티 접두사
" &quot; &#34; &#x22; 속성 값
' &apos; &#39; &#x27; 속성 값
&nbsp; &#160; &#xA0; 줄바꿈 없는 공백
© &copy; &#169; &#xA9; 저작권
® &reg; &#174; &#xAE; 등록 상표
&trade; &#8482; &#x2122; 상표
&euro; &#8364; &#x20AC; 유로 기호
&mdash; &#8212; &#x2014; 긴 줄표
&ndash; &#8211; &#x2013; 짧은 줄표

XSS 방어: 엔티티 인코딩이 사이트를 보호하는 방법

크로스 사이트 스크립팅(XSS)은 가장 흔한 웹 보안 취약점 중 하나입니다. 공격자가 다른 사용자가 보는 콘텐츠에 악성 스크립트를 주입할 때 발생합니다. HTML 엔티티 인코딩은 XSS에 대한 주요 방어 수단입니다.

전형적인 XSS 공격 예시

사용자의 검색어를 그대로 표시하는 기능을 생각해 봅시다:

<!-- 취약한 코드: 사용자 입력을 직접 HTML에 삽입 -->
<p>검색어: <?php echo $_GET['q']; ?></p>

공격자가 다음과 같은 URL을 만듭니다:

https://example.com/search?q=<script>document.cookie</script>

브라우저는 <script> 태그를 렌더링하고 공격자의 코드를 실행합니다. document.cookie로 세션 토큰을 탈취하거나 fetch()로 데이터를 공격자 서버로 전송할 수 있습니다.

해결책: 출력 시 인코딩

<!-- 안전한 코드: 모든 출력을 인코딩 -->
<p>검색어: <?php echo htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8'); ?></p>

이제 브라우저에는 다음이 표시됩니다:

<p>검색어: &lt;script&gt;document.cookie&lt;/script&gt;</p>

스크립트는 텍스트로만 표시되고 실행되지 않습니다.


실용적인 코드 예시

JavaScript: 안전한 DOM 조작

JavaScript에서 사용자 생성 콘텐츠를 삽입하는 가장 안전한 방법은 textContent를 사용하는 것입니다. textContent는 HTML을 전혀 해석하지 않습니다:

// 안전: textContent는 HTML을 해석하지 않음
const el = document.getElementById('output');
el.textContent = userInput; // 모든 것을 자동으로 이스케이프

// 위험: innerHTML은 HTML을 파싱하고 실행함
el.innerHTML = userInput; // 신뢰할 수 없는 입력에는 절대 사용 금지

JavaScript에서 HTML 문자열을 직접 만들어야 할 경우:

function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')   // & 를 반드시 먼저 처리
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

const safe = `<p>검색 결과: ${escapeHtml(userInput)}</p>`;

주의: &를 반드시 먼저 교체해야 합니다. <를 먼저 교체하면 &lt;&가 다시 &amp;lt;로 이중 인코딩됩니다.

PHP: htmlspecialchars()와 htmlentities()

PHP에는 HTML 인코딩을 위한 두 가지 주요 함수가 있습니다:

// htmlspecialchars: 5개의 핵심 문자만 인코딩
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

// htmlentities: 명명 엔티티가 있는 모든 문자를 인코딩
$safe = htmlentities($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

핵심 차이점: htmlspecialchars()는 5개의 위험 문자만 인코딩합니다. htmlentities()는 악센트 문자와 기호(예: é&eacute;)도 인코딩합니다. UTF-8 문서에서는 보통 htmlspecialchars()를 권장합니다. UTF-8은 모든 문자를 직접 표현할 수 있으므로 위험한 문자만 이스케이프하면 충분합니다.

반드시 ENT_QUOTES를 전달하여 작은따옴표와 큰따옴표를 모두 인코딩하고, 항상 'UTF-8'을 문자 집합으로 지정하세요.

Python: html.escape()

import html

# 기본 이스케이프
safe = html.escape(user_input)

# 작은따옴표도 이스케이프 (Python 3.2+에서 quote는 기본 True)
safe = html.escape(user_input, quote=True)

# 예시
user_input = '<script>alert("XSS")</script>'
print(html.escape(user_input))
# 출력: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

템플릿 엔진 (Jinja2, Django, Handlebars)

대부분의 현대 템플릿 엔진은 기본적으로 자동 이스케이프를 수행합니다:

<!-- Jinja2 / Django: 기본으로 자동 이스케이프 -->
<p>{{ user_comment }}</p>

<!-- 원시 HTML 렌더링 (사용자 콘텐츠에는 위험!) -->
<p>{{ user_comment | safe }}</p>

<!-- Handlebars: 이중 중괄호는 이스케이프, 삼중은 하지 않음 -->
<p>{{userComment}}</p>    <!-- 이스케이프됨 — 안전 -->
<p>{{{userComment}}}</p>  <!-- 원시 HTML — 위험! -->

실제 활용 사례

1. 기술 문서와 코드 블로그

HTML에 관해 글을 쓸 때 <, >, &가 포함된 코드 예시를 표시해야 하는 경우가 많습니다. 엔티티를 사용하면 페이지 구조를 깨지 않고 리터럴 문자로 표시할 수 있습니다:

<pre><code>
&lt;div&gt;와 &lt;/div&gt;로 섹션을 감쌉니다.
&amp; 문자는 HTML 엔티티의 시작을 나타냅니다.
</code></pre>

2. CMS와 사용자 생성 콘텐츠

사용자 생성 텍스트를 저장하고 표시하는 CMS는 페이지에 출력하기 전에 반드시 HTML 엔티티 인코딩을 수행해야 합니다. 블로그 댓글, 포럼 게시물, 상품 리뷰, SNS 콘텐츠가 모두 해당됩니다. 이를 간과하는 것이 실제 XSS 공격 사례의 대부분을 차지합니다.

3. HTML 이메일 템플릿

HTML 이메일 클라이언트의 렌더링 기준은 매우 다양합니다. 타이포그래피 문자에 명명 엔티티(&mdash;, &lsquo;, &rsquo;, &hellip;)를 사용하면 Gmail, Outlook, Apple Mail 등에서 일관된 렌더링을 기대할 수 있습니다.

4. 타이포그래피와 특수 기호

엔티티는 키보드로 입력하기 어려운 타이포그래피 문자에 안정적으로 접근하는 수단을 제공합니다:

<p>긴 줄표&mdash;부연 설명에 사용&mdash;는 하이픈보다 표현력이 풍부합니다.</p>
<p>그녀는 &ldquo;안녕하세요&rdquo;라고 말하며 미소 지었습니다.</p>
<p>가격: 29&nbsp;&euro;</p>
<!-- &nbsp;로 숫자와 통화 기호가 다른 줄로 분리되는 것을 방지 -->

5. 레거시 시스템의 국제화

UTF-8을 안정적으로 지원할 수 없는 레거시 시스템에서는 숫자 엔티티로 모든 Unicode 문자를 표현할 수 있습니다:

<!-- 한자 '용' (U+9F99)의 십진수 엔티티 -->
&#40857;

<!-- 한글 '가' (U+AC00) -->
&#44032;

명명 엔티티 vs 숫자 엔티티

측면 명명 (&lt;) 십진수 (&#60;) 십육진수 (&#x3C;)
가독성 높음 보통 낮음
범위 약 2,000자 전체 Unicode 전체 Unicode
HTML5 지원 완전 완전 완전
XML 지원 5개만 사전 정의 완전 완전
최적 용도 일반 문자 임의 Unicode 기술 문서/Unicode 참조

HTML과 XML의 엔티티 처리 차이

XML은 5개의 엔티티만 사전 정의합니다(&lt;, &gt;, &amp;, &quot;, &apos;). &copy;&nbsp; 같은 명명 엔티티는 DTD에서 선언되지 않는 한 XML에서는 정의되지 않습니다.

<!-- XML에서 유효하지 않음 (미정의 엔티티): -->
<p>저작권 &copy; 2026</p>

<!-- XML에서 유효함 (숫자 엔티티): -->
<p>저작권 &#169; 2026</p>

<!-- HTML5에서는 둘 다 유효 -->

XHTML이나 SVG를 작성할 때는 기본 5개 이외의 문자에 숫자 엔티티를 사용하거나, UTF-8 문자를 직접 사용하세요.


모범 사례

1. 전체 스택에서 UTF-8 사용

데이터베이스 정렬 규칙, HTTP Content-Type 헤더, HTML <meta charset> 태그 모두에서 UTF-8을 선언하여 비ASCII 문자를 엔티티로 인코딩할 필요성을 없애세요:

<meta charset="UTF-8">
header('Content-Type: text/html; charset=UTF-8');

2. 컨텍스트에 맞는 적절한 인코딩

인젝션 컨텍스트에 따라 다른 이스케이프 전략이 필요합니다:

  • HTML 본문: <, >, & 이스케이프
  • HTML 속성: <, >, &, ", ' 이스케이프
  • JavaScript 문자열: \uXXXX 이스케이프 또는 JSON 인코딩 사용
  • CSS 값: 다른 이스케이프 규칙 적용
  • URL: 퍼센트 인코딩 사용(%3C, &lt; 아님)

특정 컨텍스트를 위한 인코딩이 다른 컨텍스트에서 안전하다는 보장은 없습니다.

3. 입력 시가 아닌 출력 시에 인코딩

데이터베이스에는 원시 데이터를 저장하고, HTML로 출력할 때 인코딩합니다. 입력 시에 인코딩하면 출력 시 이중 인코딩이 발생할 수 있고, JSON API나 일반 텍스트 이메일 등 비HTML 컨텍스트에서 데이터를 올바르게 사용할 수 없습니다.

4. 신뢰할 수 없는 입력은 처리 전에 디코딩하지 말 것

보안 필터를 적용하기 전에 사용자가 제공한 엔티티를 디코딩하면 방어가 무력화됩니다. &#60;script&#62;를 디코딩하면 <script>가 되어 단순한 '꺾쇠 괄호 차단' 필터를 쉽게 우회할 수 있습니다.

5. 이중 인코딩 방지

이중 인코딩(&amp;lt;<가 아닌 &lt;로 표시됨)은 애플리케이션의 여러 레이어가 각각 독립적으로 인코딩할 때 발생하는 흔한 실수입니다. 인코딩 로직을 프레젠테이션 레이어의 한 곳에 집중시키세요.

6. HTML4에서의 &apos; 문제

&apos;(아포스트로피)는 XML과 XHTML에서 정의되지만 HTML4에서는 정의되지 않았습니다. HTML4 환경에서는 대신 &#39;를 사용하세요. HTML5는 &apos;를 명명 엔티티 목록에 공식 추가했습니다.


자주 묻는 질문

Q: 모든 특수 문자를 인코딩해야 하나요?

보안을 위해서는 최소한 5개의 핵심 문자(< > & " ')를 인코딩해야 합니다. 저작권 기호, 줄표, 통화 기호 등의 타이포그래피 문자는 UTF-8 문서에서 직접 UTF-8 문자를 사용해도 전혀 문제없습니다. 엔티티는 레거시 시스템이나 문자 인코딩을 보장할 수 없는 환경에서 더 중요합니다.

Q: &amp;&의 차이는 무엇인가요?

&는 리터럴 앰퍼샌드 문자이고, &amp;는 그 HTML 엔티티 표현입니다. HTML 소스에서 리터럴 &를 표시하려면 반드시 &amp;로 작성해야 합니다. 단어 앞에 그냥 &를 쓰면 브라우저가 엔티티 시작으로 해석하려 하여 렌더링이 잘못될 수 있습니다.

Q: &nbsp;는 왜 일반 공백과 다르게 동작하나요?

일반 공백(U+0020)은 '줄 바꿈 가능 공백'입니다. 브라우저가 이 위치에서 줄을 바꿀 수 있고, 연속된 여러 공백은 하나로 접힙니다. &nbsp;(줄바꿈 없는 공백, U+00A0)는 인접 문자 사이의 줄 바꿈을 방지하고 접히지 않습니다. "100 km"나 "홍 길동" 같은 값을 같은 줄에 유지하는 데 유용합니다.

Q: 이모지에 숫자 엔티티를 사용할 수 있나요?

네. 이모지에는 Unicode 코드 포인트가 있어 숫자 엔티티로 표현할 수 있습니다. 😀(U+1F600)는 십육진수로 &#x1F600;, 십진수로 &#128512;입니다. UTF-8 문서에서는 이모지를 직접 붙여넣을 수 있지만, 숫자 엔티티는 대안으로 사용할 수 있습니다.

Q: href 속성에 특유의 XSS 위험이 있나요?

네. href 속성에는 고유한 위험이 있습니다. URL은 javascript: 프로토콜을 사용할 수 있어 HTML 인코딩만으로는 충분하지 않습니다:

<!-- < 와 > 가 인코딩되어도 여전히 위험: -->
<a href="javascript:alert(1)">클릭</a>

<!-- 안전한 방법: 프로토콜 검증 -->
<?php
$url = $_GET['url'];
if (!preg_match('/^https?:\/\//i', $url)) { $url = '#'; }
echo '<a href="' . htmlspecialchars($url) . '">링크</a>';
?>

Q: 현대 JavaScript 프레임워크는 HTML 인코딩을 자동으로 처리하나요?

네 — React, Vue, Angular, Svelte 등의 현대 프레임워크는 기본적으로 출력을 이스케이프합니다. React의 JSX는 {}로 보간된 값을 자동으로 이스케이프합니다. 그러나 각 프레임워크는 이스케이프를 명시적으로 우회하는 방법(React의 dangerouslySetInnerHTML, Vue의 v-html)을 제공하며, 신뢰할 수 있는 콘텐츠에만 극도로 신중하게 사용해야 합니다.

Q: HTML 엔티티와 URL 인코딩의 차이는 무엇인가요?

HTML 엔티티는 HTML 문서 내에서 텍스트를 표현하기 위한 것입니다(&lt;<를 표시). URL 인코딩(퍼센트 인코딩)은 URL 내에서 특수 문자를 안전하게 전송하기 위한 것입니다(%3C<를 나타냄). 두 가지를 혼동하여 잘못된 컨텍스트에서 사용하면 보안 취약점이나 표시 문제가 발생합니다.


요약

HTML 엔티티는 웹 개발에 필수적인 메커니즘입니다:

  1. 보안<, >, &, ", '를 이스케이프하여 XSS 인젝션 방어
  2. 정확성 — HTML에서 예약된 의미를 가진 문자를 리터럴로 표시
  3. 호환성 — 레거시 또는 제한된 환경에서 임의의 Unicode 문자 표현
  4. 타이포그래피 — 긴 줄표, 줄바꿈 없는 공백, 통화 기호 등 특수 문자를 안정적으로 삽입

현대적인 UTF-8 스택에서는 주로 동적 콘텐츠를 HTML로 출력할 때 5개의 보안 핵심 문자를 인코딩해야 합니다. &nbsp;&mdash; 같은 명명 엔티티는 타이포그래피 측면에서 여전히 유용합니다. 명명 엔티티와 숫자 엔티티의 차이, HTML과 XML 규칙의 차이를 이해하면 더 효율적이고 보안 의식이 높은 웹 개발자가 될 수 있습니다.