← 모두의 툴

HTML 엔티티 디코더, 왜 정규식 대신 브라우저 파서를 쓰는가

가이드 · 2026-08-19 최종 확인

&amp;&로, &lt;<로 바꾸는 건 정규식 replace 몇 줄로 될 것 같지만, HTML5 표준에는 &copy;, &hearts;, &alpha;처럼 이름 붙은 엔티티가 2,000개 넘게 정의돼 있습니다. 이걸 전부 하드코딩하지 않고도 정확히 디코딩하는 방법이 있는데, 브라우저 자신의 HTML 파서를 그대로 빌려 쓰는 것입니다. 이 가이드는 그 트릭의 원리와, 왜 <div>가 아니라 하필 <textarea>를 쓰는지 설명합니다.

1. 정규식만으로는 왜 부족한가

HTML 엔티티는 크게 세 종류입니다. &lt; 같은 이름 엔티티(named entity), &#169; 같은 10진수 숫자 엔티티, &#x00A9; 같은 16진수 숫자 엔티티입니다. 숫자 엔티티는 코드 포인트를 그대로 계산하면 되니 정규식으로도 어렵지 않지만, 이름 엔티티는 WHATWG HTML5 표준에 정의된 전체 이름 목록(2,231개, 대소문자 구분 포함하면 더 많음)을 알아야만 정확히 변환할 수 있습니다. &amp;, &lt;, &gt;, &quot; 몇 개만 손으로 매핑한 디코더는 &copy;(©), &trade;(™), &euro;(€), &alpha;(α) 같은 나머지 수천 개 엔티티를 만나면 그냥 원문 그대로 방치하거나 틀리게 처리합니다.

2. 트릭: 브라우저의 HTML 파서를 그대로 빌려쓰기

이 문제를 우회하는 방법은 직접 매핑표를 만드는 대신, 브라우저가 이미 완벽하게 구현해둔 HTML 파서에게 일을 시키는 것입니다. 디코딩할 문자열을 DOM 요소의 innerHTML로 대입한 뒤, 그 요소의 텍스트 콘텐츠를 다시 읽어오면 브라우저의 표준 엔티티 테이블 전체가 적용된 결과를 그대로 얻을 수 있습니다. 브라우저는 스펙에 정의된 모든 이름 엔티티, 숫자 엔티티, 16진수 엔티티를 완벽하게 알고 있으므로, 별도의 매핑 데이터를 유지보수할 필요가 없습니다.

3. 왜 하필 <div>가 아니라 <textarea>인가

여기서 중요한 선택이 하나 있습니다. 같은 트릭을 <div>에 적용해도 엔티티 디코딩 자체는 똑같이 됩니다. 문제는 안전성입니다. HTML5 파싱 스펙은 요소마다 다른 "콘텐츠 모델"을 정의하는데, <div>는 일반 콘텐츠 모델이라 innerHTML에 들어간 문자열 속에 <script><img onerror=...> 같은 실제 태그가 있으면 브라우저가 그걸 진짜 자식 DOM 요소로 파싱해버립니다 — 이후 그 결과를 화면에 다시 렌더링하면 스크립트가 실행될 수 있는 XSS 경로가 열립니다. 반면 <textarea>는 HTML5 스펙에서 RAWTEXT 콘텐츠 모델로 분류됩니다. RAWTEXT 요소는 닫는 태그를 만나기 전까지 안의 모든 내용을 순수 텍스트로만 취급하고, 그 안에 있는 <script> 같은 문자열도 절대 별도의 살아있는 DOM 요소로 파싱하지 않습니다. 즉 엔티티는 정확히 디코딩되면서도, 그 결과에 우연히 태그처럼 생긴 문자열이 섞여 있어도 실행 가능한 요소로 둔갑하지 않는다는 안전장치가 구조적으로 내장돼 있는 것입니다.

이 사이트의 실제 구현: html-decoder.html은 정확히 이 방식을 씁니다. const ta = document.createElement('textarea'); ta.innerHTML = text; const decoded = ta.value; — 임시 textarea를 만들어 innerHTML로 원문을 넣고, .value로 디코딩된 순수 텍스트만 다시 꺼냅니다. 이 임시 요소는 DOM에 붙지도 않고 화면에 렌더링되지도 않으므로 부작용이 전혀 없습니다.

4. 실무에서 흔히 놓치는 것: 디코딩 이후가 더 중요하다

여기서 꼭 짚어야 할 점은, "엔티티 디코딩 자체"는 위 방식으로 완전히 안전하다는 것입니다. 위험은 디코딩된 결과를 다시 어떻게 쓰느냐에서 생깁니다. 디코딩된 텍스트를 화면에 표시할 때 textContent로 넣으면 항상 안전하지만, 만약 그 결과를 다시 innerHTML로 삽입한다면 이번엔 진짜로 태그가 파싱·실행될 수 있습니다. 즉 "디코딩 함수"는 안전해도 "디코딩한 값을 어디에 넣는지"는 별개의 보안 결정이라는 점을 기억해야 합니다.

5. 숫자 엔티티의 자잘한 예외

HTML5 파싱 스펙은 숫자 참조 중 일부를 그대로 통과시키지 않습니다. 예를 들어 &#0;처럼 널(null) 코드 포인트를 가리키는 참조나, 서로게이트 범위(U+D800~U+DFFF)에 해당하는 값은 파싱 오류로 취급돼 유니코드 대체 문자(U+FFFD, �)로 치환됩니다. 이런 경계 케이스는 브라우저 파서에 맡기면 스펙에 정의된 대로 자동 처리되지만, 직접 정규식으로 숫자 엔티티를 디코딩하는 코드를 짠다면 이런 예외를 스스로 챙겨야 합니다.

자주 묻는 질문

Q. 이름 엔티티를 전부 하드코딩하면 안 되나요?

A. 가능은 하지만 2,000개가 넘는 표준 엔티티(그리고 대소문자를 구분하는 변형까지 포함하면 더 많음)를 유지보수해야 하고, HTML5 표준이 갱신되면 다시 손봐야 합니다. 브라우저 파서를 빌리면 이 유지보수 부담이 아예 사라집니다.

Q. div 대신 textarea를 쓰면 정말 실행 위험이 없나요?

A. 네. textarea는 HTML5 스펙상 RAWTEXT 콘텐츠 모델이라, innerHTML로 들어간 내용 중 무엇도 실제 자식 DOM 노드로 파싱되지 않습니다. script 태그 문자열이 들어가도 실행되지 않고 그냥 텍스트로 취급됩니다.

Q. 디코딩 결과를 innerHTML로 다시 넣어도 되나요?

A. 권장하지 않습니다. 디코딩 자체는 안전하지만, 그 결과 문자열에 우연히 태그가 들어있었다면 innerHTML로 삽입하는 순간 진짜 DOM 요소로 파싱되어 실행될 수 있습니다. 화면에 텍스트로만 표시할 목적이라면 항상 textContent를 쓰세요.

Q. 이중 인코딩된 문자열은 어떻게 처리하나요?

A. 이 방식은 한 번의 패스로 한 겹의 인코딩만 풉니다. 원문이 두 번 인코딩됐다면(예: &lt;가 다시 인코딩돼 &amp;lt;가 된 경우) 디코더를 두 번 연속 실행해야 완전히 원래 텍스트로 돌아갑니다.