어트리뷰트
어트리뷰트 노드와 attributes 프로퍼티
HTML 문서의 구성 요소인 HTML 요소는 여러 개의 어트리뷰트를 가질 수 있다. HTML 요소의 동작을 제어하기 위한 추가적인 정보를 제공하는 HTML 어트리뷰트는 HTML 요소의 시작 태그에 어트리뷰트 이름과 값을 정의한다.
<input id="user" type="text" value="ungmo2">
글로벌 어트리뷰트와 이벤트 핸들러 어트리뷰트는 모든 HTML 요소에서 공통적으로 사용할 수 있지만, 특정 HTML 요소에만 사용 가능한 어트리뷰트도 있다.
HTML이 파싱될 때 요소의 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결되며 어트리뷰트당 하나의 어트리뷰트 노드가 생성된다.
요소 노드의 모든 어트리뷰트 노드는 요소 노드의 Element.prototype.attributes 프로퍼티로 취득할 수 있으며, attributes 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티이며, 요소 도느의 모든 어트리뷰트 노드의 참조가 담긴 NameNodeMap 객체를 반환한다.
<!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 프로퍼티는 getter만 존재하는 읽기 전용 접근자 프로퍼티이므로 HTML 어트리뷰트 값을 취득할 수 있지만 변경할 수는 없다.
Element.prototype.getAttributes/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.hasAttribue 메서드를, 삭제하려면 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 어트리뷰트에 대응하는 프로퍼티가 존재한다. 이 DOM 프로퍼티들은 HTML 어트리뷰트 값을 초기값으로 가지고 있다.
예를 들어, 요소가 파싱되어 생성된 요소 노드 객체에는 어트리뷰트에 대응하는 프로퍼티가 존재하며, 이 DOM 프로퍼티들은 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 어트리뷰트는 다음과 같이 DOM에서 중복 관리되고 있는 것처럼 보이지만 그렇지 않다. 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 프로퍼티의 값이 달라진다.
이처럼 요소 노드는 초기 상태와 최신 상태를 관리하며 요소 노드의 초기 상태는 어트리뷰트 노드가, 최신 상태는 DOM 프로퍼티가 관리한다.
어트리뷰트 노드
HTML 어트리뷰트로 지정한 HTML 요소의 초기 상태는 어트리뷰트 노드에 관리한다. 어트리뷰트 값은 사용자의 입력에 의해 상태가 변경되어도 변하지 않고 HTML 요소의 초기 상태를 그대로 유지한다. 이 값을 얻거나 변경하려면 getAttribute/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>
모든 DOM 프로퍼티가 사용자의 입력에 의해 변경된 최신 상태를 관리하는 것은 아니다.
id 어트리뷰트에 대응하는 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 대응하지만 value 어트리뷰트는 초기 상태를, value 프로퍼티는 최신 상태를 갖는다.
- class 어트리뷰트는 className, classList 프로퍼티와 대응한다.
- for 어트리뷰트는 htmlFor 프로퍼티와 1:1 대응한다.
- td 요소의 colspan 어트리뷰트는 대응하는 프로퍼티가 존재하지 않는다.
- textContent 프로퍼티는 대응하는 어트리뷰트가 존재하지 않는다.
- 어트리뷰트 이름은 대소문자를 구별하지 않지만 대응하는 프로퍼티 키는 카멜 케이스를 따른다.
DOM 프로퍼티 값의 타입
getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다. 하지만 DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도 있다. 예를 들어, checkbox 요소의 checked 어트리뷰트 값은 문자열이지만 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 프로퍼티
data 어트리뷰트와 dataset 프로퍼티를 사용하면 HTML 요소에 정의한 사용자 정의 어트리뷰트와 JS 간에 데이터를 교환할 수 있다.
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>
</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 프로퍼티는 setter와 getter 모두 존재하는 접근자 프로퍼티로서 HTML 요소의 class 어트리뷰트 값을 취득하거나 변경한다.
<!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
Element.prototype.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)
인수로 전달한 문자열 형태의 값과 같은 클래스를 class 어트리뷰트에서 삭제한다.
$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)
인수로 전달한 문자열과 일치하는 클래스가 class 어트리뷰트에 있는지 확인
$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 어트리뷰트에 강제로 문자열을 추가하고, false면 제거한다.
// 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 메서드의 두 번째 인수(pseudo)로 :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와 WHATWB라는 두 단체가 나름대로 협력하면서 공통된 표준을 만들어 왔다.
하지만 시간이 지나며 두 단체가 다른 결과문을 만들기 시작했다. 2018년 4월부터는 구글, 애플, 마소, 모질라로 구성된 4개의 주류 브라우저 벤더사가 주도하는 WHATWG이 단일 표준을 내놓기로 두 단체가 합의했다.
'JavaScript > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글
[Deep Dive] 이벤트 - 2 (3) | 2024.10.08 |
---|---|
[Deep Dive] 이벤트 - 1 (2) | 2024.09.24 |
[Deep Dive] DOM - 2 (1) | 2024.09.10 |
[Deep Dive] DOM - 1 (1) | 2024.09.05 |
[Deep Dive] 브라우저의 렌더링 과정 (0) | 2024.08.23 |