DOM으로 HTML 다루기 - innerHTML, insertAdjacentHTML, createContextualFragment

2025. 9. 9. 15:04·FrontEnd/JavaScript

"HTML 문자열을 어디에, 어떻게 넣을까" 정리해 두면
성능이랑 보안 둘 다 덜 털린다.

처음에 DOM 다룰 때는 거의 무조건 이렇게 시작한다.

target.innerHTML = "<p>뭐라도 나오게 해봐...</p>";

빠르고 편하다. 근데 규모가 커지면 갑자기 이런 고민이 튀어나온다.

  • innerHTML += 로 계속 추가해도 되나?
  • 특정 위치에만 쏙 끼워 넣고 싶을 땐 뭐 쓰지?
  • XSS 같은 보안 이슈는 어떻게 피하지?

이번 글에서는 HTML 삽입/조작할 때 자주 쓰는 3가지 DOM 메서드를
내가 실제로 써본 기준으로 정리해 본다.

  • innerHTML
  • insertAdjacentHTML
  • document.createRange().createContextualFragment

결론 먼저

한 줄씩만 꽂으면 이렇게 정리할 수 있다

  • 전체 교체 → innerHTML로 간단하게
  • 지정 위치 삽입 → insertAdjacentHTML로 효율적으로
  • 보안 + 세밀 조작 → createContextualFragment로 안전하게

조금만 더 풀면

  • innerHTML: 요소 안의 내용을 통째로 덮어쓰는 용도
  • insertAdjacentHTML: 요소 앞/뒤/안쪽 앞/안쪽 뒤에 문자열로 HTML 삽입
  • createContextualFragment: HTML 문자열을 실제 DOM Fragment로 파싱해서,
    appendChild, insertBefore, replaceChild 같은 노드 단위 조작에 쓰기 좋음

1. innerHTML - 통으로 갈아엎을 때

역할

  • 요소의 전체 HTML을 문자열로 통째로 교체
  • += 로 쓰면 추가처럼 보이지만, 실제로는 전체를 다시 그린다

특징

  • 직관적이고 코드가 짧다
  • 하지만 내부적으로 전체 DOM을 재파싱하기 때문에
    • 내용이 많을수록 CPU/메모리 부담이 커진다
    • 이벤트 핸들러, 스크롤 상태 등도 날아갈 수 있다

예시

const target = document.getElementById("box");

// 전체 교체
target.innerHTML = "<p>새로운 내용</p>";

// 추가하는 것처럼 보이지만... 실제로는 전체를 다시 파싱한다
// 큰 영역에 반복해서 쓰면 성능이 서서히 망가질 수 있음
target.innerHTML += "<p>추가 내용</p>";

언제 쓸까?

  • 초기 UI를 한 번에 렌더링할 때
  • 간단한 실험/데모 코드
  • "이 안쪽은 그냥 몽땅 갈아엎어도 된다"는 확신이 있을 때

실무에서 계속 innerHTML += 패턴으로 붙이다 보면
어느 순간 불필요한 재렌더링 때문에 성능이 미묘하게 느려지는 걸 체감하게 된다.


2. insertAdjacentHTML - 지정 위치에 문자열로 꽂기

역할

  • 기존 DOM은 그대로 두고, 특정 위치에만 HTML 문자열을 삽입
  • innerHTML보다 좀 더 정밀하게, 효율적으로 넣을 수 있다

시그니처

element.insertAdjacentHTML(position, htmlString);

position 옵션

  • "beforebegin": 요소 바로 앞 (형제 노드)
  • "afterbegin": 요소 안쪽 맨 앞 (첫 번째 자식)
  • "beforeend": 요소 안쪽 맨 뒤 (마지막 자식)
  • "afterend": 요소 바로 뒤 (형제 노드)

예시

const target = document.getElementById("box");

// 박스 안쪽 맨 앞에 삽입
target.insertAdjacentHTML("afterbegin", "<p>맨 앞 삽입</p>");

// 박스 안쪽 맨 뒤에 삽입
target.insertAdjacentHTML("beforeend", "<p>맨 뒤 삽입</p>");

// 박스 바로 앞에 수평선 추가
target.insertAdjacentHTML("beforebegin", "<hr>");

// 박스 바로 뒤에 수평선 추가
target.insertAdjacentHTML("afterend", "<hr>");

특징

  • 기존 내용 전체를 건드리지 않고, 필요한 위치만 수정
  • innerHTML보다 일반적으로 성능 측면에서 유리
  • 하지만 여전히 HTML 문자열을 파싱해야 하고,
    외부 입력을 그대로 넣으면 XSS 취약점이 생길 수 있다

언제 쓸까?

  • 기존 DOM 구조는 유지하면서 앞/뒤/중간에만 살짝 끼워 넣고 싶을 때
  • 리스트의 맨 앞/맨 뒤에 아이템을 추가하는 UI
  • 토스트 메시지, 알림 배지처럼 특정 위치에 쓱 추가하는 UI

3. createContextualFragment - Fragment로 안전하게 다루기

이제 조금 더 "세밀하게" DOM을 만지고 싶을 때 쓰는 도구

역할

  • HTML 문자열을 **DocumentFragment**로 파싱해 준다
  • 이 Fragment를 가지고
    • appendChild
    • insertBefore
    • replaceChild
    • document.body.appendChild 등과 조합해서
      노드 단위로 안전하게 조작할 수 있다

기본 사용 패턴

const target = document.getElementById("box");

const range = document.createRange();
const fragment = range.createContextualFragment("<p>파싱된 노드</p>");

// 맨 뒤에 삽입
target.appendChild(fragment);

예시: 여러 위치에 응용해 보기

const target = document.getElementById("box");
const range = document.createRange();

// 1. Fragment 만들기
let fragment = range.createContextualFragment("<p>파싱된 노드</p>");

// 맨 뒤 삽입
target.appendChild(fragment);

// 다시 쓰고 싶으면 Fragment를 새로 만들어야 한다
fragment = range.createContextualFragment("<p>다른 위치</p>");

// 맨 앞 삽입
target.insertBefore(fragment, target.firstChild);

// 특정 자식 앞에 삽입
const secondChild = target.children[1];
fragment = range.createContextualFragment("<p>특정 위치</p>");
target.insertBefore(fragment, secondChild);

// 노드 교체
const toReplace = target.querySelector("p");
fragment = range.createContextualFragment("<p>교체된 노드</p>");
target.replaceChild(fragment, toReplace);

// 다른 곳으로 옮기기
fragment = range.createContextualFragment("<p>다른 영역</p>");
document.body.appendChild(fragment);

특징

  • 파싱 결과가 **Fragment(실제 DOM 노드 묶음)**라서
    • 노드 단위 API와 자연스럽게 어울린다
    • 기존 노드를 교체/이동/재사용하기 좋다
  • <script> 실행이 기본적으로 되지 않아서
    • 직접 innerHTML로 넣는 것보다 안전한 패턴을 만들 수 있다
  • 다만 코드 양이 늘어나고,
    • "간단히 한 줄로 때우고 싶다"는 욕구와는 거리가 있다 😅

언제 쓸까?

  • 성능/보안을 조금 더 신경 써야 하는 UI
  • 재사용 가능한 DOM 조각을 만들어서 이리저리 옮길 때
  • 프레임워크 없이 커스텀 컴포넌트 시스템을 구축할 때

선택 가이드 (어떤 상황에 뭘 쓸까?)

목적 추천 메서드 성능/보안 관점 메모
요소 안 전체 내용을 갈아엎기 innerHTML 전체 재파싱 → 너무 자주 쓰면 성능 저하
기존 내용은 유지 + 특정 위치 삽입 insertAdjacentHTML 문자열 파싱 비용 있음, XSS 주의
세밀한 노드 조작 + 안전한 처리 createRange().createContextualFragment 코드 조금 복잡하지만, 유연하고 재사용에 좋음

 


내 체크리스트

실제로 코드 짤 때는 대충 이렇게 고른다.

  • 진짜 그냥 통째로 바꿔도 되는 영역인가?
    • → 그렇다면 innerHTML 한 방으로 끝내도 된다
    • 단, innerHTML +=로 반복 추가하는 패턴은 지양
  • 기존 DOM은 유지하면서 앞/뒤/중간에만 살짝 넣고 싶나?
    • → insertAdjacentHTML
  • 외부 입력(유저 입력, 서버 응답 텍스트 등)을 신뢰할 수 없나?
    • → 가능하면 직접 문자열 합치기를 줄이고,
      Fragment + DOM API 조합을 고려.
  • 성능이 신경 쓰이기 시작하는 규모인가?
    • → 큰 리스트를 매번 innerHTML로 갈아엎는 코드는 한 번쯤 리팩터링 타이밍.
  • 앞으로 DOM 조작 패턴을 컴포넌트화하고 싶나?
    • → Fragment + appendChild/insertBefore/replaceChild 조합이 훨씬 유리.

오늘 내용 한 줄씩 다시 정리

  • 간단 교체: innerHTML로 빠르게 처리하되, += 패턴 남발은 피하자
  • 효율 삽입: insertAdjacentHTML로 기존 DOM은 유지한 채 필요한 위치에만 꽂자
  • 안전 조작: createContextualFragment + DOM 메서드 조합으로 세밀하게 다루자

결론은 하나다.

HTML을 "어떻게 넣을지"만 고민하지 말고,
어디까지 갈아엎을지, 성능/보안을 얼마나 챙길지 같이 고민하면
DOM 코드 퀄리티가 한 단계 올라간다.

저작자표시 변경금지 (새창열림)

'FrontEnd > JavaScript' 카테고리의 다른 글

JavaScript 합성(Composition) - 상속보다 유연하게 객체 설계하기  (0) 2025.09.09
ES6 클래스 vs 생성자 함수 - 요즘 자바스크립트에서 뭘 써야 할까?  (0) 2025.09.09
JavaScript Function.call / apply / bind - this  (0) 2025.09.09
[실무 기록] 폼 실시간 공유 3편 – Yjs Awareness로 상대 커서,선택 상태 보여주기  (0) 2025.09.08
[실무 기록] 폼 실시간 공유 2편 - WebSocket 서버 구조랑 네임스페이스 적응기  (0) 2025.09.08
'FrontEnd/JavaScript' 카테고리의 다른 글
  • JavaScript 합성(Composition) - 상속보다 유연하게 객체 설계하기
  • ES6 클래스 vs 생성자 함수 - 요즘 자바스크립트에서 뭘 써야 할까?
  • JavaScript Function.call / apply / bind - this
  • [실무 기록] 폼 실시간 공유 3편 – Yjs Awareness로 상대 커서,선택 상태 보여주기
프론트엔드 개발자 jbeat
프론트엔드 개발자 jbeat
프론트엔드 개발자 블로그인데 일상도 쪼그으믐
  • 프론트엔드 개발자 jbeat
    jbeat 님의 블로그
    프론트엔드 개발자 jbeat
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • FrontEnd (43)
        • TypeScript (6)
        • JavaScript (18)
        • Next.js (3)
        • React (1)
        • Testing (2)
        • Third Party (1)
        • web (10)
        • Tooling (1)
        • coding test (0)
        • A.I (1)
      • 일상 (1)
        • wedding (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 태그

    Android
    Next.js
    Utility
    타입스크립트
    배열
    TypeScript
    WebSocket
    playwright
    이터러블
    javascript
    고차함수
    omit
    pick
    preconnect
    주니어
    코테
    CRDT
    CrossOrigin
    yjs
    컬렉션
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
프론트엔드 개발자 jbeat
DOM으로 HTML 다루기 - innerHTML, insertAdjacentHTML, createContextualFragment
상단으로

티스토리툴바