32. Dom

DOM은 HTML 문서를 객체 트리로 표현한 구조로, 자바스크립트가 웹 페이지를 동적으로 조작

Index

① 요소 탐색과 DOM 노드 접근

② HTMLCollection과 NodeList의 차이

③ 텍스트 접근: nodeValue vs textContent

④ DOM 조작 최소화 기법과 DocumentFragment

⑤ HTML 어트리뷰트 vs DOM 프로퍼티


🧐 Q. Dom이란 무엇일까?

Dom(Document Object Model)은 HTML 문서의 계층적 구조와 정보를 표현하며 이를 제어할 수 있는 API이다.


1. 요청과 응답

  • 서버는 어떤 창고이다. 창고 안에는 index.html, style.css, logo.png같은 파일들이 선반에 잘 정리되어 있다.
  • 우리는(브라우저를 쓰는 사용자)은 그 창고에 “이 주소의 상품(파일) 주세요!”하고 요청을 보낸다.
  • 서버는 선반에서 그 파일을 꺼내서 우리한테 보여준다.



2. 요소 취득

2.1 id를 이용한 요소 노드 취득

id 값은 HTML 문서 내에서 유일해야 한다.class와 달리 공백 문자로 여러 값을 가질 수 없다.

중복된 id가 있더라도 에러는 발생하지 않는다. → HTML 문서 상에서 여러 요소가 동일한 id를 갖고 있어도 브라우저는 오류를 내지 않는다.

getElementById()는 항상 첫 번째 요소만 반환한다. → 동일한 id가 여러 개 있어도, 가장 먼저 나타나는 요소 노드 하나만 반환된다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>

    <script>
      // id 값이 'banana'인 요소 노드를 탐색하여 반환한다.
      // 두 번째 li 요소가 파싱되어 생성된 요소 노드가 반환된다.
      const $elem = document.getElementById("banana");

      // 취득한 요소 노드의 style.color 프로퍼티 값을 변경한다.
      $elem.style.color = "red";
    </script>
  </body>
</html>


2.2 태그 이름을 이용한 요소 노드 취득

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>

    <script>
      // 태그 이름이 'li'인 요소 노드를 모두 탐색하여 반환한다.
      // 탐색된 요소 노드들은 HTMLCollection 객체에 담겨 반환된다.
      // HTMLCollection 객체는 유사 배열 객체이면서 이터러블이다.
      const $elems = document.getElementsByTagName("li");

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      // HTMLCollection 객체를 배열로 변환하여 순회하며 color 값을 변경
      [...$elems].forEach((elem) => {
        elem.style.color = "red";
      });
    </script>
  </body>
</html>

getElementsByTagName('li')로 얻은 결과는 HTMLCollection 객체이고, 이는 유사 배열 객체(배열처럼 생겼지만 진짜 배열은 아님)입니다. 그래서 바로 forEach() 같은 배열 메서드는 사용할 수 없습니다. 하지만 HTMLCollection이터러블(iterable)이기 때문에, 전개 연산자 (...)를 사용해 배열로 변환할 수 있습니다.


2.3 class를 이용한 요소 노드 취득

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="fruit apple">Apple</li>
      <li class="fruit banana">Banana</li>
      <li class="fruit orange">Orange</li>
    </ul>

    <script>
      // class 값이 'fruit'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $elems = document.getElementsByClassName("fruit");

      // 취득한 모든 요소의 CSS color 프로퍼티 값을 변경한다.
      [...$elems].forEach((elem) => {
        elem.style.color = "red";
      });

      // class 값이 'fruit apple'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $apples = document.getElementsByClassName("fruit apple");

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      [...$apples].forEach((elem) => {
        elem.style.color = "blue";
      });
    </script>
  </body>
</html>
r

DOM에서 class로 요소 노드를 가져올 때 나오는 값은 배열처럼 생겼지만 실제 배열은 아닌 “유사 배열 객체”입니다.

const elements = document.getElementsByClassName("my-class");

console.log(elements); // HTMLCollection(3) [div, div, div]
console.log(elements.length); // 3
console.log(elements[0]); // <div class="my-class">...</div>

elements.forEach((e) => console.log(e)); // ❌ TypeError: forEach is not a function
/*
- length 속성이 있음
- 인덱스로 접근 가능 ([0], [1] 등)
- 하지만 진짜 배열(Array)이 아님 → forEach(), map(), filter() 같은 배열 메서드는 직접 사용할 수 없음
*/


2.4 css 선택자를 이용한 요소 노드 취득

CSS 선택자를 이용한 요소 노드 취득 내에는 **querySelector(또는 querySelectorAll)가 언급됩니다.

그 이유는, **querySelector(또는 querySelectorAll)가 바로 **CSS 선택자 문법을 그대로 사용할 수 있는 유일한 DOM 메서드이기 때문**입니다.

document.querySelector("div.my-class > span:first-child");

querySelectorCSS 선택자 문법을 그대로 사용할 수 있는 API입니다. 위처럼 class, id, 자식 선택자, 속성 선택자 등 CSS에서 쓰는 선택자들을 그대로 사용할 수 있는 유일한 DOM 메서드가 querySelector 계열입니다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>

    <script>
      // ul 요소의 자식 요소인 li 요소를 모두 탐색하여 반환한다.
      const $elems = document.querySelectorAll("ul > li");

      // 취득한 요소 노드들은 NodeList 객체에 담겨 반환된다.
      console.log($elems); // NodeList(3) [li.apple, li.banana, li.orange]

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      // NodeList는 forEach 메서드를 제공한다.
      $elems.forEach((elem) => {
        elem.style.color = "red";
      });
    </script>
  </body>
</html>



3. 살아있는객체, HTMLCollection

<!DOCTYPE html>
<html>
  <head>
    <style>
      .red {
        color: red;
      }
      .blue {
        color: blue;
      }
    </style>
  </head>
  <body>
    <ul id="fruits">
      <li class="red">Apple</li>
      <li class="red">Banana</li>
      <li class="red">Orange</li>
    </ul>

    <script>
      // class 값이 'red'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $elems = document.getElementsByClassName("red");

      // 이 시점에 HTMLCollection 객체에는 3개의 요소 노드가 담겨 있다.
      console.log($elems); // HTMLCollection(3) [li.red, li.red, li.red]

      // HTMLCollection 객체의 모든 요소의 class 값을 'blue'로 변경한다.
      for (let i = 0; i < $elems.length; i++) {
        $elems[i].className = "blue";
      }

      // HTMLCollection 객체의 요소가 3개에서 1개로 변경되었다.
      console.log($elems); // HTMLCollection(1) [li.red]
    </script>
  </body>
</html>

그리하여, 해결책은?

// 정방향 → ❌ 위험
for (let i = 0; i < $elems.length; i++) {
  $elems[i].className = "blue";
}

// 역방향 → ✅ 안전
for (let i = $elems.length - 1; i >= 0; i--) {
  $elems[i].className = "blue";
}

// 또는 진짜 배열로 복사해서 처리
const array = Array.from($elems);
array.forEach((elem) => (elem.className = "blue"));



4. NodeList

NodeList객체는 대부분의 경우 노드 객체의 상태 변경을 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작한다. 하지만 childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체와 같이 실시간으로 노드객체의 상태변경을 반영하는 live 객체로 동작하므로 주의가 필요하다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>

    <script>
      const $fruits = document.getElementById("fruits");

      // childNodes 프로퍼티는 NodeList(실시간 live)를 반환한다.
      const { childNodes } = $fruits;

      console.log(childNodes instanceof NodeList); // true

      // $fruits 요소의 자식 노드는 공백 텍스트 노드와 요소 노드를 포함함
      console.log(childNodes); // NodeList(5) [text, li, text, li, text]

      for (let i = 0; i < childNodes.length; i++) {
        // removeChild 메서드는 $fruits 요소의 자식 노드를 DOM에서 삭제한다.
        // removeChild 호출 시마다 NodeList가 실시간으로 변하여
        // 결국 첫 번째, 세 번째, 다섯 번째 요소만 삭제된다.
        $fruits.removeChild(childNodes[i]);
      }

      // 예상과 다르게 $fruits 요소의 모든 자식 노드가 삭제되지 않는다.
      console.log(childNodes); // NodeList(2) [li, li]
    </script>
  </body>
</html>



5. nodevalue, textnode 차이

Photo of nodeValue vs textContent

nodeValue vs textContent

JayTak

속성명 작동 대상 반환 내용 주로 사용 용도
nodeValue 텍스트 노드, 주석 노드 등 해당 노드 자체의 텍스트만 텍스트 노드 조작
textContent 요소 노드 (Element) 요소 내부 전체 텍스트 (하위 포함) 요소의 내용 전체 읽기/쓰기
<p id="greet">Hello <span>World</span>!</p>
const p = document.getElementById("greet");

console.log(p.textContent); // "Hello World!"
console.log(p.firstChild.nodeValue); // "Hello " ← 텍스트 노드만!



6. DOM 조작 최소화 기법

아래 예제는 DOM을 한 번만 변경하므로 성능에 유리하기는 하지만 다음과 같이 불필요한 컨테이너 요소(div)가 DOM에 추가되는 부작용이 있다. 이는 바람직 하지 않다.

const $container = document.createElement("div");

["Apple", "Banana", "Orange"].forEach((text) => {
  const $li = document.createElement("li");
  const textNode = document.createTextNode(text);
  $li.appendChild(textNode);
  $container.appendChild($li); // li들을 일단 container에만 붙임
});

$fruits.appendChild($container);


🔍 왜 DOM 조작을 최소화해야 할까?

웹 브라우저는 DOM이 변경될 때마다 다음과 같은 과정을 거칩니다:

  1. Reflow (레이아웃 계산): 요소의 위치와 크기를 다시 계산
  2. Repaint (화면 그리기): 변경된 부분을 다시 렌더링

➡️ 이런 작업이 많아지면 성능이 저하되고, 화면 깜빡임이나 느린 렌더링 현상이 발생할 수 있습니다.


<html>
  <body>
    <ul id="fruits"></ul>
  </body>
  <script>
    const $fruits = document.getElementById("fruits");

    // DocumentFragment 노드 생성
    const $fragment = document.createDocumentFragment();

    ["Apple", "Banana", "Orange"].forEach((text) => {
      // 1. 요소 노드 생성
      const $li = document.createElement("li");

      // 2. 텍스트 노드 생성
      const textNode = document.createTextNode(text);

      // 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
      $li.appendChild(textNode);

      // 4. $li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
      $fragment.appendChild($li);
    });

    // 5. DocumentFragment 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($fragment);
  </script>
</html>

먼저 DocumentFragment 노드를 생성하고 DOM에 추가할 요소 노드를 생성하여 DocumentFragment 노드에 자식 노드로 추가한 다음, DocumentFragment 노드를 기존 DOM에 추가한다.

이때 실제로 DOM 변경이 발생하는 것은 한 번뿐이며 리플로우와 리페인트도 한 번만 실행된다. 따라서 여러 개의 요소 노드를 DOM에 추가하는 경우 DocumentFragment 노드를 사용하는 것이 더 효율적이다.


🧐 Q. 왜 HTML 어트리뷰트는 변경이 불가능하고, DOM 프로퍼티는 변경이 가능한가?

1. HTML 어트리뷰트 (Attribute)

  • HTML 문서 안에서 정의되는 고정된 값
  • 브라우저가 처음 페이지를 파싱할 때만 읽습니다.
  • JavaScript에서 직접 바꿀 수는 있지만, 보통은 초기 설정 역할만 함.
<input id="user" value="JayTak" />
  • 여기서 value="JayTak"는 초기 어트리뷰트 값
  • 이건 HTML 소스 자체에 기록된 값이기 때문에, 변하지 않음 (문서를 다시 로드하지 않는 이상)


2. DOM 프로퍼티 (Property)

  • HTML 요소가 자바스크립트 객체로 변환될 때 생기는 속성
  • 사용자가 값을 입력하거나 자바스크립트로 변경 가능
<input id="user" value="JayTak" />
<script>
  const input = document.getElementById("user");

  // HTML 어트리뷰트 값 확인
  console.log(input.getAttribute("value")); // "JayTak"

  // DOM 프로퍼티 값 변경
  input.value = "changed";

  // DOM 프로퍼티 값 확인
  console.log(input.value); // "changed" ✅

  // HTML 어트리뷰트는 그대로
  console.log(input.getAttribute("value")); // "JayTak" ❌ 안 바뀜
</script>


3. 차이를 명확하게 이해해 봅시다!

3.1 동기화가 유지되는 경우

<input type="text" value="Hello" />
const input = document.querySelector("input");

console.log(input.getAttribute("value")); // "Hello"
console.log(input.value); // "Hello"
  • 이때는 HTML 어트리뷰트 값과 DOM 프로퍼티 값이 같음.

  • 브라우저가 HTML 파싱 후 DOM을 생성할 때, 어트리뷰트 값을 읽어 input.value 프로퍼티에 복사해줌.


3.2 동기화가 깨지는 경우 (JS나 사용자 입력으로 변경 후 )

input.value = "World"; // DOM 프로퍼티 변경

console.log(input.getAttribute("value")); // "Hello" (변함없음)
console.log(input.value); // "World"
  • input.value = "World"는 DOM 프로퍼티만 바꿈.

  • value 어트리뷰트는 여전히 "Hello" 그대로 남아 있음.

  • 즉, value 프로퍼티만 바꾸면 어트리뷰트는 바뀌지 않음 → 이게 동기화가 “깨진” 예시예요.


3.3 결론

Photo of getAttribute vs .value 저장위치

getAttribute vs .value 저장위치

JayTak

  • HTML 파싱 → 어트리뷰트를 읽어서 → DOM 프로퍼티에 복사함 (초기 동기화)

  • 이후 .value를 바꾸면, DOM 프로퍼티만 바뀌고 어트리뷰트는 남아 있음

  • 그래서 getAttribute('value')는 원래 HTML에 적힌 "Hello"를 계속 반환


🧐 **Q. 한번 더 나아가서, 그러면 자바스크립트는 왜 이렇게 설계했을까? **

1. HTML 어트리뷰트와 DOM 프로퍼티는 역할이 다름
Photo of HTML 어트리뷰트 vs DOM 프로퍼티 역할 비교

HTML 어트리뷰트 vs DOM 프로퍼티 역할 비교

JayTak


2. 웹 표준 철학: 마크업 구조와 로직의 분리

HTML, CSS, JS는 각기 다른 목적을 가진 기술입니다:

  • HTML → 구조와 초기값
  • CSS → 표현
  • JS → 동작과 상태 변화

이 셋을 혼합해서 하나가 다른 걸 계속 바꾼다면 의도 분리와 유지보수성이 떨어지기 때문에, DOM 프로퍼티와 어트리뷰트의 동기화는 초기만 유지하고 이후 분리되도록 설계된 것입니다.


추가) HTML 속성과 JavaScript 접근 방식 간 표기법 대응 정리
Photo of CSS/data 속성 JS 접근 방식

CSS/data 속성 JS 접근 방식

JayTak

🧐 포인트만 기억하기!

  1. 하이픈(-) 으로 연결된 CSS · data 속성 이름은 JS 안에서 하이픈 제거 + 다음 글자 대문자(camelCase)로 바뀐다.
  2. 스타일은 style. 뒤에, 커스텀 데이터는 dataset. 뒤에 붙여서 쓴다.




reference: 모던자바스크립트 Deep Dive 39장. Dom