jQuery / HTML 태그 바꾸기
jQuery로 HTML 태그를 바꾸려면 원하는 태그의 새 요소를 만든 뒤 기존 요소를 교체합니다. 기존 요소의 tagName을 수정하는 방식은 사용할 수 없습니다. 자식 요소의 입력값과 이벤트를 유지하려면 내부 HTML을 문자열로 복사하지 말고 실제 자식 노드를 새 요소로 먼저 옮깁니다.
태그 이름 수정과 요소 교체의 차이
DOM(Document Object Model)은 HTML 문서를 노드 객체로 다루는 구조입니다. Element.tagName은 요소의 태그 이름을 읽는 전용 속성입니다. .attr("tagName", "section")은 태그 종류를 바꾸는 명령이 아니고, .prop("tagName", "SECTION")도 읽기 전용 속성을 바꾸는 해결책이 아닙니다.
document.createElement()로 새 section을 만들고 .replaceWith()로 기존 div를 교체합니다. 겉으로는 태그 이름만 달라진 것처럼 보여도 바깥 요소의 객체는 새로 만들어집니다. 기존 div를 변수로 보관한 코드는 자동으로 새 section을 가리키지 않으며, 새 요소를 다시 참조해야 합니다.
아래 예제는 제목을 갖춘 독립 영역으로 바꾸는 완성된 페이지 설계가 아니라, div 컨테이너를 section 컨테이너로 교체할 때 상태를 비교하는 실습입니다. 실제 문서에서 section을 사용할지는 영역의 의미와 제목 구조를 보고 결정합니다. 모양만 바꾸려면 태그 대신 class와 CSS를 바꾸는 편이 적합할 수 있습니다.
자식 노드를 보존하는 전체 예제
전체 코드를 UTF-8의 tag-demo.html로 저장해 브라우저에서 엽니다. 인터넷으로 공식 jQuery 파일 4.0.0을 불러옵니다. 별도의 서버나 패키지 설치는 필요하지 않습니다. 동일한 핵심 동작을 Windows Edge에서 jQuery 3.7.1과 4.0.0으로 실행해 확인했습니다.
div 안에는 텍스트, 주석, 입력란과 버튼을 넣었습니다. 버튼에는 직접 이벤트, 바깥 main에는 위임 이벤트를 등록하고 기존 div에는 별도 이벤트와 jQuery 데이터를 둡니다. 화면의 카운터로 자식과 바깥 요소의 처리 결과를 구분합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Replace a container tag, keep its children</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; margin: 20px; max-width: 760px; }
button, input { font: inherit; max-width: 100%; box-sizing: border-box; }
button { margin: 4px; }
.card { border: 3px solid teal; padding: 16px; margin: 16px 0; }
#result { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<button type="button" id="change">Change div to section</button>
<button type="button" id="reset">Reset</button>
<main id="board"></main>
<pre id="result" aria-live="polite"></pre>
<script>
$(function () {
let original, originalInput, originalNodes;
let childClicks = 0, wrapperClicks = 0, delegatedClicks = 0;
function report() {
const card = document.getElementById("card");
const sameChildren = originalNodes.every((node, i) => card.childNodes[i] === node);
$("#result").text(`tag=${card.localName}\nsame wrapper=${card === original}\nsame children=${sameChildren}\nsame input=${card.querySelector("input") === originalInput}\ninput=${originalInput.value}\nold data=${$(original).data("lesson")}\nchild=${childClicks}; wrapper=${wrapperClicks}; delegated=${delegatedClicks}`);
}
function reset() {
$("#board").empty();
const card = document.createElement("div");
card.id = "card"; card.className = "card";
card.setAttribute("aria-label", "Editable note");
card.append(document.createTextNode("Keep this text. "), document.createComment("keep this comment"));
const input = document.createElement("input");
input.type = "text"; input.value = "First note"; input.setAttribute("aria-label", "Note");
const button = document.createElement("button");
button.type = "button"; button.className = "inspect"; button.textContent = "Inspect";
card.append(input, button); document.getElementById("board").append(card);
original = card; originalInput = input; originalNodes = Array.from(card.childNodes);
childClicks = wrapperClicks = delegatedClicks = 0;
$(card).data("lesson", "old wrapper").on("click", function () { wrapperClicks++; });
$(button).on("click", function () { childClicks++; });
$(input).on("input", report);
$("#change").prop("disabled", false); report();
}
$("#board").on("click.tagLesson", ".inspect", function () { delegatedClicks++; report(); });
$("#change").on("click", function () {
const old = document.getElementById("card");
const replacement = document.createElement("section");
for (const name of ["id", "class", "aria-label"]) {
if (old.hasAttribute(name)) replacement.setAttribute(name, old.getAttribute(name));
}
// Move existing children BEFORE jQuery removes the old wrapper.
$(replacement).append($(old).contents());
$(old).replaceWith(replacement);
$(this).prop("disabled", true); report();
});
$("#reset").on("click", reset);
reset();
});
</script>
</body>
</html>
처음 열었을 때의 결과입니다. same children은 요소만이 아니라 텍스트와 주석까지 같은 객체·순서로 남았는지 확인합니다.
tag=div
same wrapper=true
same children=true
same input=true
input=First note
old data=old wrapper
child=0; wrapper=0; delegated=0
- 입력란의 First note를 원하는 값으로 바꿉니다. 예를 들어
<em>Edited note</em>를 적어도 입력값일 뿐 HTML 요소로 실행하지 않습니다. - Inspect를 한 번 누르면 child=1, wrapper=1, delegated=1이 됩니다. 자식 버튼, 기존 div, 바깥 main의 처리기가 각각 실행된 결과입니다.
- Change div to section을 누릅니다. tag=section, same wrapper=false, same children=true, same input=true가 됩니다. 편집한 입력값은 그대로이고 old data는 undefined입니다.
- Inspect를 다시 누르면 child=2, wrapper=1, delegated=2입니다. 버튼의 직접 이벤트와 main의 위임 이벤트는 계속 동작하지만, 기존 div의 이벤트는 새 section으로 옮겨지지 않았습니다.
- Reset을 누르면 div와 초기 입력값으로 되돌아가며 카운터도 0이 됩니다. 리셋은 새로운 실습 상태를 만드는 동작으로, 편집한 값은 지웁니다.
보존되는 내용과 새로 설정할 내용
핵심은 $(replacement).append($(old).contents())를 교체보다 먼저 실행하는 순서입니다. .contents()는 직접 자식인 요소·텍스트·주석 노드를 가져옵니다. 새 요소에 넣으면 이 노드들이 복제되지 않고 이동하므로 입력란의 현재 value와 자식에 등록한 직접 이벤트를 유지합니다. .children()만 옮기면 요소 바깥 텍스트와 주석은 빠집니다.
이동 후 .replaceWith()가 제거하는 대상은 비워진 기존 div입니다. jQuery는 제거하는 요소의 jQuery 데이터와 이벤트를 정리합니다. 자식들을 기존 div에 둔 채 먼저 교체하고 나중에 복구하려 하면 정리 시점에 자식의 상태를 잃을 수 있으므로 순서를 바꾸지 않습니다. 교체는 페이지의 모든 상태를 보존하는 일반 기능이 아니며, 포커스·텍스트 선택·플러그인 내부 참조는 별도 검토가 필요합니다.
id, class, aria-label은 예제에서 명시적으로 복사했습니다. 속성이 자동으로 모두 옮겨지는 것은 아닙니다. 임의의 속성을 전부 복사하면 새 태그에 유효하지 않은 속성, 오래된 역할, 인라인 이벤트까지 따라갈 수 있습니다. 필요한 속성만 선택하고 실제 접근성 의미도 다시 확인합니다. 기존 요소의 jQuery .data()와 HTML의 data-* 속성도 같은 저장소가 아닙니다.
.replaceWith()의 반환값은 새 요소가 아니라 제거된 원래 jQuery 집합입니다. 새 태그를 추가 조작할 때는 예제의 replacement를 사용하거나 현재 문서에서 다시 선택합니다. 기존 요소 자체의 이벤트가 필요하다면 새 요소에 필요한 처리기를 명시적으로 등록하거나, 예제처럼 유지되는 상위 요소에 기능을 위임합니다.
적용 범위와 실패하기 쉬운 지점
$(old).html()을 읽어 새 요소의 .html()에 넣는 방법은 내부 HTML 문자열을 다시 해석합니다. 같은 모습이어도 새 자식 노드가 만들어지므로 기존 자식 객체, 직접 이벤트와 입력란의 현재 상태를 그대로 보존하는 방법이 아닙니다. 사용자가 입력한 문자열을 HTML로 연결해 넣는 방식도 피합니다.
이 실습은 일반 HTML 컨테이너 사이의 교체입니다. 자식을 가질 수 없는 input·img 같은 요소, 표의 특수 구조, 폼 컨트롤, SVG, 사용자 정의 요소를 같은 함수로 무조건 변환하지 않습니다. p를 div로 바꿔도 되는지, button을 a로 바꾸면 키보드 동작과 기본 역할이 어떻게 달라지는지처럼 새 태그의 허용 자식과 의미를 먼저 확인합니다.
교체 후 스타일이 달라지면 div.card처럼 태그에 종속된 CSS와 기존 태그를 찾는 선택자를 확인합니다. id는 문서에서 유일해야 하며 연결된 레이블·앵커도 유지돼야 합니다. 입력값만 확인하고 끝내지 말고 직접 이벤트, 위임 이벤트, 포커스, 화면 읽기 의미와 되돌리기까지 해당 화면에서 확인한 후 적용합니다.




