어트리뷰트 노드와 attributes 프로퍼티
HTML 문서의 구성 요소인 HTML 요소는 여러개의 어트리뷰트를 가질 수 있다.
<input id="user" type="text" value="ungmo2">
어트리뷰트 역시 모든 HTML요소에서 공통적으로 사용할 수 있는 어트리뷰트와 특정 HTML요소에서만 한정적으로 사용할 수 있는 어트리뷰트로 나누어져 있다.
attributes 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티이며 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
// 요소 노드의 attribute 프로퍼티는 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.
const { attributes } = document.getElementById('user');
console.log(attributes);
// NamedNodeMap {0: id, 1: type, 2: value, id: id, type: type, value: value, length: 3}
// 어트리뷰트 값 취득
console.log(attributes.id.value); // user
console.log(attributes.type.value); // text
console.log(attributes.value.value); // ungmo2
</script>
</body>
</html>
HTML 어트리뷰트 조작
attributes 프로퍼티는 읽기 전용 접근자 프로퍼티이므로 HTML 어트리뷰트 값을 취득할 수 있지만 변경할 수는 없다.
Element.prototype.getAttribute/setAttribute 메서드를 사용하면 attributes 프로퍼티를 통하지 않고 요소 노드에서 메서드를 통해 직접 HTML 어트리뷰트 값을 취득하거나 변경할 수 있다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// value 어트리뷰트 값을 취득
const inputValue = $input.getAttribute('value');
console.log(inputValue); // ungmo2
// value 어트리뷰트 값을 변경
$input.setAttribute('value', 'foo');
console.log($input.getAttribute('value')); // foo
</script>
</body>
</html>
특정 HTML 어트리뷰트가 존재하는지 확인하고 싶은 경우에는 Element.prototype.hasAttribute 메서드를 , 삭제하려면 Element.prototype.removeAttribute 메서드를 활용한다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// value 어트리뷰트의 존재 확인
if ($input.hasAttribute('value')) {
// value 어트리뷰트 삭제
$input.removeAttribute('value');
}
// value 어트리뷰트가 삭제되었다.
console.log($input.hasAttribute('value')); // false
</script>
</body>
</html>
HTML 어트리뷰트 VS DOM 프로퍼티
요소 노드 객체에는 HTML 어트리뷰트에 대응하는 프로퍼티가 존재한다.
예를들어 id,type,value 어트리뷰트가 있다면 이에 대응하는 id,type,value 프로퍼티 또한 존재한다. 이 프로퍼티들은 HTML 어트리뷰트의 값을 초기값으로 가지고 있다.
DOM프로퍼티는 setter,getter 모두 존재하는 접근자 프로퍼티로 DOM 프로퍼티를 통해서 변경이 가능하다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// 요소 노드의 value 프로퍼티 값을 변경
$input.value = 'foo';
// 요소 노드의 value 프로퍼티 값을 참조
console.log($input.value); // foo
</script>
</body>
</html>
언뜻보면 HTML 어트리뷰트는 중복 관리되고 있는것처럼 보이지만 그렇지 않다.
HTML 어트리뷰트의 역할은 HTML 요소의 초기 상태를 지정하는 것이다. 즉 HTML 어트리뷰트 값은 HTML 요소 초기상태를 의미하며 이는 변하지 않는다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// attributes 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.getAttribute('value')); // ungmo2
// 요소 노드의 value 프로퍼티에 저장된 value 어트리뷰트 값
console.log($input.value); // ungmo2
</script>
</body>
</html>
위 예제에서 어트리뷰트 노드의 어트리뷰트 값과 요소 노드의 value 프로퍼티값은 맨처음 페이지가 렌더링 된 경우에는 일치한다. 하지만 사용자가 값을 입력하면 value 프로퍼티의 값이 달라진다.
그럼에도 어트리뷰트의 값은 변하지 않는다. 그래야 웹페이지를 처음 표시하거나 새로고침한 경우 초기상태를 표시할 수 있기 때문이다.
어트리뷰트 노드
HTML 어트리뷰트로 지정한 HTML 요소의 초기상태는 어트리뷰트 노드에서 관리한다. 이 어트리뷰트 값은 사용자의 입력에 상태가 변화되어도 변하지 않고 HTML 요소의 초기 상태를 그대로 유지한다. 이 값을 변경하려면 getAttribute / setAttribute 메서드를 사용한다.
즉 setAttribute 메서드는 어트리뷰트 노드에서 관리하는 HTML 요소에 지정한 어트리뷰트 값 즉 초기상태의 값을 변경한다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
// HTML 요소에 지정한 어트리뷰트 값, 즉 초기 상태 값을 변경한다.
document.getElementById('user').setAttribute('value', 'foo');
</script>
</body>
</html>
DOM 프로퍼티
사용자가 입력한 최신상태는 HTML 어트리뷰트에 대응하는 요소 노드의 DOM 프로퍼티가 관리한다. DOM 프로퍼티는 사용자의 입력에 의한 상태 변화에 반응하여 언제나 최신 상태를 유지한다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// 사용자가 input 요소의 입력 필드에 값을 입력할 때마다 input 요소 노드의
// value 프로퍼티 값, 즉 최신 상태 값을 취득한다. value 프로퍼티 값은 사용자의 입력에
// 의해 동적으로 변경된다.
$input.oninput = () => {
console.log('value 프로퍼티 값', $input.value);
};
// getAttribute 메서드로 취득한 HTML 어트리뷰트 값, 즉 초기 상태 값은 변하지 않고 유지된다.
console.log('value 어트리뷰트 값', $input.getAttribute('value'));
</script>
</body>
</html>
DOM 프로퍼티에 값을 할당하는 것은 HTML 요소의 최신 상태 값을 변경하는 것을 의미하며 사용자가 상태를 변경하는 행위와 같다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// DOM 프로퍼티에 값을 할당하여 HTML 요소의 최신 상태를 변경한다.
$input.value = 'foo';
console.log($input.value); // foo
// getAttribute 메서드로 취득한 HTML 어트리뷰트 값, 즉 초기 상태 값은 변하지 않고 유지된다.
console.log($input.getAttribute('value')); // ungmo2
</script>
</body>
</html>
HTML 어트리뷰트는 HTML 요소의 초기 상태 값을 관리하고 DOM 프로퍼티는 사용자의 입력에 의해 변경되는 최신 상태를 관리한다. 단 모든 DOM 프로퍼티가 사용자의 입력에 의해 변경된 최신 상태를 관리하는 것은아니다.
input 요소의 입력에 의한 상태 변화는 value 프로퍼티가 관리한다. 하지만 id 어트리뷰트에 대응하는 id 프로퍼티는 사용자의 입력과 관련이 없다.
따라서 사용자 입력에 의한 상태변화와 관계없는 id 어트리뷰트와 프로퍼티는 사용자 입력과 관계없이 항상 동일한 값을 유지하며 id어트리뷰트 값이 변하면 id 프로퍼티 값이 변하고 그 반대도 마찬가지가 된다.
<!DOCTYPE html>
<html>
<body>
<input id="user" type="text" value="ungmo2">
<script>
const $input = document.getElementById('user');
// id 어트리뷰트와 id 프로퍼티는 사용자 입력과 관계없이 항상 동일한 값으로 연동한다.
$input.id = 'foo';
console.log($input.id); // foo
console.log($input.getAttribute('id')); // foo
</script>
</body>
</html>
이처럼 사용자 입력한 상태변화와 관계있는 DOM 프로퍼티만 최신 상태 값을 관리하며 그 외의 경우 ODM 프로퍼티와 어트리뷰트 값은 항상 동일한 값을 연동한다.
HTML 어트리뷰트와 DOM 프로퍼티의 대응 관계
대부분 HTML 어트리뷰트는 HTML 어트리뷰트 이름과 동일한 DOM 프로퍼티와 1:1로 대응하지만 모든 경우는 아니다.
- id 어트리뷰트와 id 프로퍼티는 1:1 대응이며 동일하다.
- input 요소의 value 어트리뷰트는 value 프로퍼티와 1:1대응하며 어트리뷰트는 초기상태를 , 프로퍼티는 최신상태를 관리한다.
- class 어트리뷰트는 className,classList 프로퍼티와 대응한다.
- for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응한다.
- td 요소으 colspan 어트리뷰트는 대응하는 프로퍼티가 없다.
- textContent 프로퍼티는 대응하는 어트리뷰트가 없다.
- 어트리뷰트 이름은 대소문자를 구별하지 않지만 프로퍼티키는 카멜 케이스를 따른다.
DOM 프로퍼티 값의 타입
getAttrivute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다. 하지만 DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도있다. 예를들어 checkbox 요소의 checked 어트리뷰트값은 문자열이지만 프로퍼티 값은 불리언 타입이다.
<!DOCTYPE html>
<html>
<body>
<input type="checkbox" checked>
<script>
const $checkbox = document.querySelector('input[type=checkbox]');
// getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다.
console.log($checkbox.getAttribute('checked')); // ''
// DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도 있다.
console.log($checkbox.checked); // true
</script>
</body>
</html>
data 어트리뷰트와 dataset 프로퍼티
해당 어트리뷰트와 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 JS간 데이터를 교환할 수 있다.
data- 접두사 다음에 임의 이름을 붙여서 사용할 수 있다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621" data-role="admin">Lee</li>
<li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
</ul>
</body>
</html>
data 어트리뷰트의 값은 HTMLElement.dataset 프로퍼티로 취득할 수 있으며 dataset 프로퍼티는 HTML요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다. 이 객체는 data 어트리뷰트의 data-접두사 다음 이름을 카멜케이스로 변환한 프로퍼티를 가지고 있다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621" data-role="admin">Lee</li>
<li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children];
// user-id가 '7621'인 요소 노드를 취득한다.
const user = users.find(user => user.dataset.userId === '7621');
// user-id가 '7621'인 요소 노드에서 data-role의 값을 취득한다.
console.log(user.dataset.role); // "admin"
// user-id가 '7621'인 요소 노드의 data-role 값을 변경한다.
user.dataset.role = 'subscriber';
// dataset 프로퍼티는 DOMStringMap 객체를 반환한다.
console.log(user.dataset); // DOMStringMap {userId: "7621", role: "subscriber"}
</script>
</body>
</html>
data 어트리뷰트의 data-접두사 다음에 존재하지 않는 이름을 키롷 사용하여 dataset프로퍼티에 할당하면 HTML 요소에 새 어트리뷰트가 생성된다.
<!DOCTYPE html>
<html>
<body>
<ul class="users">
<li id="1" data-user-id="7621">Lee</li>
<li id="2" data-user-id="9524">Kim</li>
</ul>
<script>
const users = [...document.querySelector('.users').children];
// user-id가 '7621'인 요소 노드를 취득한다.
const user = users.find(user => user.dataset.userId === '7621');
// user-id가 '7621'인 요소 노드에 새로운 data 어트리뷰트를 추가한다.
user.dataset.role = 'admin';
console.log(user.dataset);
/*
DOMStringMap {userId: "7621", role: "admin"}
-> <li id="1" data-user-id="7621" data-role="admin">Lee</li>
*/
</script>
</body>
</html>
스타일
인라인 스타일 조작
HTMLElement.prototype.style 프로퍼티는 setter,getter 모두 존재하는 접근자 프로퍼티로 요소 노드의 인라인 스타일을 취득하거나 추가,변경할 수 있도록 도와준다.
<!DOCTYPE html>
<html>
<body>
<div style="color: red">Hello World</div>
<script>
const $div = document.querySelector('div');
// 인라인 스타일 취득
console.log($div.style); // CSSStyleDeclaration { 0: "color", ... }
// 인라인 스타일 변경
$div.style.color = 'blue';
// 인라인 스타일 추가
$div.style.width = '100px';
$div.style.height = '100px';
$div.style.backgroundColor = 'yellow';
</script>
</body>
</html>
style 프롶티를 참조하면 CSSStyleDeclaration 타입의 객체를 반환한다.
CSS 프로퍼티는 케밥 케이스를 따르며 이에 대응하는 CSSStyleDeclaration 객체의 프로퍼티는 카멜 케이스를 따른다.
CSS 프로퍼티 background-color 에 대응하는 cSSStyleDeclaration 객체의 프로퍼티는 backgroudColor가 되게 된다.
$div.style.backgroundColor = 'yellow';
단위 지정이 필요한 CSS 프로퍼티 값은 반드시 단위를 지정해야 하며 단위를 생략하면 CSS가 무시된다.
$div.style.width = '100px';
클래스 조작
className
Element.prototype.className 프로퍼티는 calss어트리뷰트 값을 취득하거나 변경한다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px; height: 100px;
background-color: antiquewhite;
}
.red { color: red; }
.blue { color: blue; }
</style>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 값을 취득
console.log($box.className); // 'box red'
// .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
$box.className = $box.className.replace('red', 'blue');
</script>
</body>
</html>
위 방법을 통해서 스타일을 변경하는 것도 가능하다.
classList
class어트리뷰트의 정보를 담은 DOMTokenList 객체를 반환한다.
<!DOCTYPE html>
<html>
<head>
<style>
.box {
width: 100px; height: 100px;
background-color: antiquewhite;
}
.red { color: red; }
.blue { color: blue; }
</style>
</head>
<body>
<div class="box red">Hello World</div>
<script>
const $box = document.querySelector('.box');
// .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 취득
// classList가 반환하는 DOMTokenList 객체는 HTMLCollection과 NodeList와 같이
// 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는(live) 객체다.
console.log($box.classList);
// DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]
// .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
$box.classList.replace('red', 'blue');
</script>
</body>
</html>
DOMTokenList 객체는 class 어트리뷰트의 정보를 나타내는 컬렉션 객체로 유사배열 객체이면서 이터러블이다.
DOMTokenList 객체는 아래 유용한 메서드들을 제공한다.
1. add(...className)
인수로 전달한 문자열을 class 어트리뷰트 값으로 추가한다.
$box.classList.add('foo'); // -> class="box red foo"
$box.classList.add('bar', 'baz'); // -> class="box red foo bar baz"
2. remove(...className)
클래스 이름을 삭제한다. 클래스 이름이 없으면 무시된다.
$box.classList.remove('foo'); // -> class="box red bar baz"
$box.classList.remove('bar', 'baz'); // -> class="box red"
$box.classList.remove('x'); // -> class="box red"
3. item(index)
인수로 전달한 index에 해당하는 클래스를 반환한다.
$box.classList.item(0); // -> "box"
$box.classList.item(1); // -> "red"
4. contains( className)
해당 클래스 이름이 있는지 판별한다.
$box.classList.contains('box'); // -> true
$box.classList.contains('blue'); // -> false
5. replace(oldClassName , newClassName)
클래스 이름을 변경한다.
$box.classList.replace('red', 'blue'); // -> class="box blue"
6. toggle(className[,force])
class 어트리뷰트에 인수로 전달한 문자열과 일치하는 클래스가 존재하면 제거하고 존재하지 않으면 추가한다.
$box.classList.toggle('foo'); // -> class="box blue foo"
$box.classList.toggle('foo'); // -> class="box blue"
두번째 인자로 조건식을 전달할 수 있다. true면 class 어트리뷰트에 강제로 문자열을 추가하고 그렇지 않으면 제거한다.
// class 어트리뷰트에 강제로 'foo' 클래스를 추가
$box.classList.toggle('foo', true); // -> class="box blue foo"
// class 어트리뷰트에서 강제로 'foo' 클래스를 제거
$box.classList.toggle('foo', false); // -> class="box blue"
요소에 적용되어 있는 CSS 스타일 참조
style 프로퍼티는 인라인 스타일반 반환한다. 따라서 클래스를 적용한 스타일이나 상속을 통해서 암묵적으로 적용된 스타일은 style 프로퍼티로 참조할 수 없다.
HTML 요소에 적용된 모든 CSS 스타일을 참조하려면 getComputedStyle 메서드를 활용하면 된다.
<!DOCTYPE html>
<html>
<head>
<style>
body {
color: red;
}
.box {
width: 100px;
height: 50px;
background-color: cornsilk;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="box">Box</div>
<script>
const $box = document.querySelector('.box');
// .box 요소에 적용된 모든 CSS 스타일을 담고 있는 CSSStyleDeclaration 객체를 취득
const computedStyle = window.getComputedStyle($box);
console.log(computedStyle); // CSSStyleDeclaration
// 임베딩 스타일
console.log(computedStyle.width); // 100px
console.log(computedStyle.height); // 50px
console.log(computedStyle.backgroundColor); // rgb(255, 248, 220)
console.log(computedStyle.border); // 1px solid rgb(0, 0, 0)
// 상속 스타일(body -> .box)
console.log(computedStyle.color); // rgb(255, 0, 0)
// 기본 스타일
console.log(computedStyle.display); // block
</script>
</body>
</html>
getComputedStyle 메서드의 두번째 인수로 :after , :before 와 같은 의사 요소를 지정하는 문자열을 전달할 수 있다.
<!DOCTYPE html>
<html>
<head>
<style>
.box:before {
content: 'Hello';
}
</style>
</head>
<body>
<div class="box">Box</div>
<script>
const $box = document.querySelector('.box');
// 의사 요소 :before의 스타일을 취득한다.
const computedStyle = window.getComputedStyle($box, ':before');
console.log(computedStyle.content); // "Hello"
</script>
</body>
</html>
DOM 표준
HTML 과 DOM 표준은 W3C와 WHATWG 두단체가 나름대로 협려갛면서 표준을 만들어 왔다.
그런데 어느쯤부터 두 단체가 서로 다른 결과물을 내놓기 시작했다. 따라서 2018년 4월 구글,애플,MS,모질라로 구성된 4개의 주류 브라우저 벤더사가 주도하는 WHATWG이 단일 표준을 내놓기로 두 단체가 합의했다.
따라서 DOM에는 현재 4개의 단계가 존재한다.
DOM 내용이 상당히 방대했는데 지금까지 DOM에 대해서 잘 몰랐다는 생각을 하게 되었다. 이번 기회에 DOM에 대해서 자세하게 이해할 수 있어서 좋았다.
'FrontEnd > Deep Dive' 카테고리의 다른 글
[JS] DeepDive(40) 이벤트(2) - 이벤트 전파 (1) | 2023.09.20 |
---|---|
[JS] DeepDive(40) 이벤트 (1) - event객체 (0) | 2023.09.20 |
[JS] DeepDive(39) DOM(2) - DOM 조작 (0) | 2023.09.19 |
[JS] DeepDive(39) DOM(1) - DOM,요소취득,요소타입 (0) | 2023.09.19 |
[JS] DeepDive(38) 브라우저의 렌더링 과정 (0) | 2023.09.19 |