Javascript | js로 Dom을 조작하는 방법 비교하기

이동욱·2024년 1월 29일
0

최근 작업을 시작한 사내 관리자 페이지 작업에서 react가 아닌 순수 js로 작업을 하게 됐다.
초반에는 특별한 기능 없이 요소만 보여주면 되었기 때문에 dom element를 추가할때 insertAdjacentHTML를 활용했다.

하지만 프로젝트가 점점 커지고 요소들에 기능들이 추가되면서 위 방식으로는 유지 보수가 힘들었고 계속해서 기획이 수정과 함께 작업하는 상황에서 이러한 점은 작업의 효율성을 굉장히 떯어트렸다.

그래서 dom을 추가할 때 여러 방법을 비교해보고 어떤 방식으로 작업을 해야 좋을지 한번 비교해보았다.


1. innerHTML, insertAdjacentHTML

둘다 Javascript에서 HTML코드를 문자열 형태로 다뤄 파싱후 DOM을 조작하는데 이용하는 js 메서드다


1. innerHTML 특징

  • 내부 모든 코드를 지우고 새로운 HTML코드를 삽입한다
  • 성능이 후지다
  • 코드 관리가 쉽다

상황


실행코드

const button = document.querySelector("button");
const helloP = document.querySelector("#hello");
button.addEventListener("click", function (e) {
	helloP.innerHTML = "<span>또우 입니다.</span>";
});


결과

기존 <p></p> 내부 텍스트 '안녕하세요'가 사라지고, <span>또우 입니다.</span> 새로운 내용만 남았다.

2. insertAdjacentHTML 특징

  • (실행위치, stringHTML)인자를 받는다.
  • 내부 코드를 유지한채 특정 위치에 HTML코드를 추가할 수 있다.
  • innerHTML보다 성능이 좋다.
  • 코드 관리가 쉽다

실행코드

button.addEventListener("click", function (e) {
	//태그 직전
	helloP.insertAdjacentHTML("beforebegin", "<span>또우 입니다.</span>");
 	//태그 내 시작 직후
	helloP.insertAdjacentHTML("afterbegin", "<span>또우 입니다.</span>");
 	//태그 내 닫힘 직전 
  	helloP.insertAdjacentHTML("beforeend", "<span>또우 입니다.</span>");
 	//태그 직후   
	helloP.insertAdjacentHTML("afterend", "<span>또우 입니다.</span>");
});

기존 <p>안녕하세요</p>가 유지 되면서 첫번째 인자에 설정한 위치에 따라 <span>또우 입니다.</span>들이 삽입된걸 볼 수 있다.


리터럴(``)을 이용해 작업하면 JSX를 쓰는것과 비슷(2번 예시에서 보실 수 있습니다!)한 경험을 할 수 있고 코드의 가독성이 좋아 가장 먼저 작업했던 방식이었다.
하지만 XSS 공격의 위험으로부터 보안상 문제가 될수도 있는 점이 걸렸다.

💡 XSS공격이란?
스크립트 코드를 삽입해 개발자가 의도하지 않은 (악성)코드가 실행되게끔 하는 해킹 공격이다.
구글 XSS 연습 게임 - 간단한 게임으로 XSS에 대해 이해하기 좋다.

2. createElement(), appendChild()

appendChild는 돔에 직접 노드를 추가하는 방식으로 1번의 방법들과 같이 HTML을 파싱하여 새로운 노드를 생성하는 과정이 없기때문에 성능적으로 더 뛰어나다.
또한 사용자의 입력을 그대로 사용하지 않고 createElement를 이용하여 안정하게 노드를 생성하고 추가하기 때문에 보안이슈도 해결될 수 있었다.
하지만 위 API들의 문제는 가독성이 아주 떨어진다.

예시로 유저들의 정보를 받아 아래와 같이 유저 정보를 담은 card를 render한다고 가정해보자

<div id="userCardBox">
  <div class="userCard">
    <div class="row">
      <p>이름</p>
      <p>번호</p>
    </div>
    <div class="row">
      <p>정보1</p>
      <p>정보2</p>
    </div>
  </div>
</div>


1번 방법을 활용해 작업하면 다음과 같은 코드가 나온다.

function renderUserCode(user){
	const { name, num , info1, info2 } = user

	var cardHTML = `
	  <div class="userCard">
	    <div class="row">
	      <p>${name}</p>
	      <p>${num}</p>
	    </div>
	    <div class="row">
	      <p>${info1}</p>
	      <p>${info2}</p>
	    </div>
	  </div>
	`;

	return cardHTML
}


그러나 2번 방식을 사용하면 다음과 같이 굉장히 길고 복잡한 코드가 된다.

function renderUserCode(user) {
  	const { name, num, info1, info2 } = user;

    const cardDiv = document.createElement("div");
    cardDiv.className = "userCard";

    const row1 = document.createElement("div");
    row1.className = "row";

    const nameP = document.createElement("p");
    nameP.innerText = name;
    row1.appendChild(nameP);

    const numP = document.createElement("p");
    numP.innerText = num;
    row1.appendChild(numP);

    cardDiv.appendChild(row1);

    const row2 = document.createElement("div");
    row2.className = "row";

    const info1P = document.createElement("p");
    info1P.textContent = info1;
    row2.appendChild(info1P);

    const info2P = document.createElement("p");
    info2P.textContent = info2;
    row2.appendChild(info2P);

    cardDiv.appendChild(row2);

    return cardDiv;
}

현재 계속 기획이 바뀌어 수정이 잦은 상황에서 작업하기 너무 힘든 방식이었고, 이런 상황이 아니더라도 이후 유지 보수를 하기 너무 불리한 방법이었다.

3. template tag

<template>은 html에 로드 시키나 함께 렌더링 되지는 않는다. 다만 브라우저에 파싱되어 메모리에 저장되어 있기 때문에 js에서 <template>을 복제해 동적으로 사용할 수 있어 react와 같이 component재활용이 가능하다.

예시
2번에서 사용했던 html을 아래와 같이 미리 template태그로 감싸 정의해 주었다.

<template id="userCardTemplate">
  <div class="card">
    <div class="row">
      <p class="userName"></p>
      <p class="userNum"></p>
    </div>
    <div class="row">
      <p class="userInfo1"></p>
      <p class="userInfo2"></p>
    </div>
  </div>
</template>


그리고 js에서는 newCard에 템플릿의 정보를 복사해두었다.
그리고 newCard내 요소들에 필요한 정보들을 innerText로 이용해 Element내에 text값들만 변경해 주었다.

const listItemTemplate = document.getElementById("userCardTemplate");

function renderUserCard(user){
	const { name, num , info1, info2 } = user

	// template 복사
	const newCard = listItemTemplate.content.cloneNode(true);
    newCard.querySelector(".userName").innerText = name;
    newCard.querySelector(".userNum").innerText = num;
    newCard.querySelector(".userInfo1").innerText = info1;
    newCard.querySelector(".userInfo2").innerText = info2;

	return newCard
}

template으로 생성된 js에서 복사하면 document(dom)가 아닌 documentFragment가 생성되기 때문에 innerHTML이 아닌 appendChild()를 활용해서 요소를 추가해주어야한다.

xss공격으로부터도 안전하고 재활용면에서도 좋은 코드가 나왔다.
하지만 만약 해당 element에 js를 이용한 이벤트가 많이 들어간다면 template 만으로는 부족했다.

4. Custom Element

custom element는 직접 생성한 요소를 생성하고 document에 등록해 쓸 수 있다.
자체 스크립트와 스타일을 가질 수 있어, 모듈화되어 재사용성이 높다.
직접 user-card라는 custom-element를 만들어 보면 아래와 같다

export class UserCard extends HTMLElement {
  #user;

  constructor() {
    super();
  }

  set user(userInfo) {
    this.#user = userInfo;
  }

  connectedCallback() {
    if (!this.#user) {
      return;
    }

    const user = this.#user;

    this.#render();
    this.addEventListener("click", function () {
      console.log(`유저 : ${user.name} / 나이 : ${user.age}`);
    });
  }

  #render() {
  	const { name, num, info1, info2 } = this.#user;

    const row1 = document.createElement("div");
    row1.className = "row";

    const nameP = document.createElement("p");
    const numP = document.createElement("p");

    nameP.innerText = name;
    numP.innerText = num;

    row1.appendChild(nameP);
    row1.appendChild(numP);

    const row2 = document.createElement("div");
    row2.className = "row";

    const info1P = document.createElement("p");
    const info2P = document.createElement("p");

    info1P.textContent = info1;
    info2P.textContent = info2;

    row2.appendChild(info1P);
    row2.appendChild(info2P);

    this.append(row1, row2);
  }
}

customElements.define("user-card", UserCard);

커스텀 엘리먼트에 필요한 JavaScript 이벤트들을 connectedCallback에서 처리하고 하단에는 render코드를 따로 빼두어 렌더링에 관한 코드를 따로 분리했다. template에 비해 다소 초반 공수가 들어가지만 이벤트를 연동하거나 custom element클래스 내 생명주기를 활용하여 더 세부적인 JavaScript 작업도 가능해 한번 작업해두면 유지보수 측면에서도 활용성 측면에서도 아주 훌륭했다.


종합적으로, 현재 프로젝트에서는 주로 4번 방법을 활용하고 있다.

반복되는 UI 요소에 많은 JS 이벤트가 포함된 경우에는 커스텀 엘리먼트를 사용하지만 간단히 보여주는 용도에서는 <template> 활용하고 있다.

특히, 4번 방법에서는 #render에서 innerHTML을 활용하면 react코드와 유사한 구조로 기시감이 생겨 가독성이 아주 좋았다. 백엔드에서 이미 가공된 데이터를 사용하고 XSS 공격에 대한 우려가 없는 경우에는 innerHTML을 사용하며, 그렇지 않은 경우 createElementappend를 활용하고 있다.


참고
https://developer.mozilla.org/ko/docs/Web/HTML/Element/template
https://velog.io/@1106laura/insertAdjacentHTML
https://dkje.github.io/2020/08/18/createDomElement/

profile
프론트엔드

0개의 댓글