Index
① 요소 탐색과 DOM 노드 접근
- 1.1 요청과 응답
- 1.2 id를 이용한 요소 노드 취득
- 1.3 태그 이름을 이용한 요소 노드 취득
- 1.4 class를 이용한 요소 노드 취득
- 1.5 CSS 선택자를 이용한 요소 노드 취득
② 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");
querySelector는 CSS 선택자 문법을 그대로 사용할 수 있는 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 차이
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이 변경될 때마다 다음과 같은 과정을 거칩니다:
- Reflow (레이아웃 계산): 요소의 위치와 크기를 다시 계산
- 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 결론
getAttribute vs .value 저장위치
JayTak
-
HTML 파싱 → 어트리뷰트를 읽어서 → DOM 프로퍼티에 복사함 (초기 동기화)
-
이후
.value를 바꾸면, DOM 프로퍼티만 바뀌고 어트리뷰트는 남아 있음 -
그래서
getAttribute('value')는 원래 HTML에 적힌"Hello"를 계속 반환
🧐 **Q. 한번 더 나아가서, 그러면 자바스크립트는 왜 이렇게 설계했을까? **
1. HTML 어트리뷰트와 DOM 프로퍼티는 역할이 다름
HTML 어트리뷰트 vs DOM 프로퍼티 역할 비교
JayTak
2. 웹 표준 철학: 마크업 구조와 로직의 분리
HTML, CSS, JS는 각기 다른 목적을 가진 기술입니다:
- HTML → 구조와 초기값
- CSS → 표현
- JS → 동작과 상태 변화
이 셋을 혼합해서 하나가 다른 걸 계속 바꾼다면 의도 분리와 유지보수성이 떨어지기 때문에, DOM 프로퍼티와 어트리뷰트의 동기화는 초기만 유지하고 이후 분리되도록 설계된 것입니다.
추가) HTML 속성과 JavaScript 접근 방식 간 표기법 대응 정리
CSS/data 속성 JS 접근 방식
JayTak
🧐 포인트만 기억하기!
- 하이픈(-) 으로 연결된 CSS · data 속성 이름은 JS 안에서 하이픈 제거 + 다음 글자 대문자(camelCase)로 바뀐다.
- 스타일은
style.뒤에, 커스텀 데이터는dataset.뒤에 붙여서 쓴다.