HTML 속성(Attributes)과 DOM 프로퍼티(Properties)의 차이점
(jakearchibald.com)- 같은 요소에 붙어 있어도 HTML 속성과 DOM 프로퍼티는 별도 저장 공간처럼 동작해, 한쪽 변경이 다른 쪽에 자동 반영되지 않을 수 있음
- 속성은 HTML 직렬화와 개발자 도구 요소 패널에 보이지만, 프로퍼티는 DOM 전용 상태라
outerHTML같은 결과에는 나타나지 않음 - 속성 값은 항상 문자열이고 이름은 대소문자를 구분하지 않지만, 프로퍼티는 객체 등 다양한 타입을 가질 수 있으며 이름 대소문자를 구분함
id,className,htmlFor,ariaLabel처럼 속성을 반영하는 프로퍼티가 있는 반면,input.value처럼 현재 상태와 기본 설정이 분리된 예외도 있음- 프레임워크마다 설정 대상이 달라 Preact와 VueJS는 대체로 프로퍼티를, React는 대체로 속성을 우선하며, Lit은
.value문법으로 둘을 명시적으로 구분함
속성과 프로퍼티는 같은 이름이어도 다를 수 있음
- 속성(attributes) 과 프로퍼티(properties) 는 서로 다른 저장 공간처럼 동작할 수 있음
<div foo="bar">에서getAttribute('foo')는'bar'를 반환하지만,div.foo는undefined일 수 있음- 이후
div.foo = 'hello world'를 설정해도foo속성 값은'bar'로 남음
- 프레임워크 템플릿에서
<input className="…" type="…" aria-label="…" value="…" />처럼 속성처럼 보이는 문법을 써도, 내부에서는 경우에 따라 프로퍼티를 설정할 수 있음- 어떤 경우에 프로퍼티를 설정하는지는 프레임워크마다 다름
- 어떤 설정은 프로퍼티와 속성을 모두 바꾸는 부수 효과를 만들 수 있음
HTML 직렬화에 남는 것은 속성
- 속성은 HTML과 DOM 양쪽에 존재하지만, 프로퍼티는 DOM에만 존재함
div.setAttribute('foo', 'bar')와div.hello = 'world'를 모두 실행해도div.outerHTML에는<div foo="bar"></div>처럼 속성만 나타남- 브라우저 개발자 도구의 요소 패널은 요소의 속성을 보여주며, 프로퍼티 전체를 표시하지는 않음
값 타입과 대소문자 처리
- 속성 값은 직렬화된 형식에서 동작해야 하므로 항상 문자열임
- 객체를
setAttribute로 넣으면 문자열"[object Object]"로 저장됨 - 같은 객체를 프로퍼티에 넣으면 객체 타입 그대로 유지될 수 있음
- 객체를
- 속성 이름은 대소문자를 구분하지 않음
HeLlO="world"는 DOM에서hello속성으로 정규화될 수 있음setAttribute('FOO', 'bar')도foo속성으로 나타남
- 프로퍼티 이름은 대소문자를 구분함
div.TeSt = 'value'를 설정해도div.test는undefined일 수 있음
- 속성 이름과 달리 속성 값은 대소문자를 구분함
속성을 반영하는 프로퍼티
- 일부 프로퍼티는 특정 속성을 반영(reflect) 함
div.id는id속성을 읽고 쓰는 getter/setter로 동작함div.id = 'bar'를 실행하면id속성도'bar'로 바뀜
- 반영 관계에서는 속성이 데이터의 원천임
- 프로퍼티를 설정하면 속성을 업데이트함
- 프로퍼티를 읽으면 속성 값을 읽음
- 명세에 정의된 속성은 편의를 위해 대응 프로퍼티가 만들어지는 경우가 많음
- 임의의
foo속성은 명세에 정의된 속성이 아니므로, 대응하는foo프로퍼티가 자동으로 생기지 않음
- 임의의
<ol>명세는 “Content attributes”에서 속성을, “DOM interface”에서 프로퍼티를 정의함reversed와typeIDL 속성은 같은 이름의 콘텐츠 속성을 reflect해야 함
이름이 달라지는 반영 관계
- 어떤 프로퍼티는 반영하는 속성과 이름이 다름
- JavaScript 프로퍼티 스타일에 맞추기 위해 이름이 달라지는 경우가 있음
<img>의el.crossOrigin은crossorigin속성을 반영함- 모든 요소의
el.ariaLabel은aria-label속성을 반영함 - ARIA 반영 프로퍼티는 2023년 말에 크로스 브라우저가 됐고, 그 전에는 속성만 사용할 수 있었음
- 과거 JavaScript 예약어 때문에 이름이 달라진 경우도 있음
- 모든 요소의
el.className은class속성을 반영함 <label>의el.htmlFor는for속성을 반영함
- 모든 요소의
검증, 타입 변환, 기본값
- 프로퍼티에는 검증과 기본값이 붙을 수 있지만, 속성 자체에는 그런 동작이 없음
input.type은 속성과 프로퍼티의 차이를 잘 보여줌input.getAttribute('type')이null이어도input.type은 기본값'text'를 반환함input.type = 'number'는 속성과 프로퍼티 모두에서'number'처럼 보임input.type = 'foo'는 속성에는'foo'를 넣지만, 프로퍼티 getter는 유효하지 않은 값을 보고'text'를 반환함
- 일부 프로퍼티는 타입 변환을 수행함
<details open>에서details.getAttribute('open')은 빈 문자열이고details.open은true임details.open = false는open속성을 제거함details.open = 'hello'는 문자열을 그대로 속성에 넣지 않고 boolean으로 변환해open속성이 존재하는 상태가 됨
img.height같은 프로퍼티는 속성 값을 숫자로 변환함- setter는 들어온 값을 숫자로 바꾸고, 음수는 0으로 처리함
input.value는 현재값과 기본값을 분리함
input에는value프로퍼티와value속성이 있지만,value프로퍼티는value속성을 반영하지 않음value속성을 반영하는 프로퍼티는defaultValue임defaultValuegetter는value속성 또는 빈 문자열을 반환함defaultValuesetter는value속성을 문자열로 설정함
value프로퍼티는 처음에는defaultValue를 따르다가, JavaScript나 사용자 상호작용으로 설정되면 내부 값을 사용함- 초기
<input type="text" value="default" />에서value,defaultValue,getAttribute('value')는 모두'default'임 input.defaultValue = 'new default'를 실행하면 속성과value가 함께'new default'처럼 보임- 이후
input.value = 'hello!'를 실행하면value는'hello!'가 되지만, 속성과defaultValue는'new default'로 남음 setAttribute('value', 'another new default')를 실행해도 현재value는'hello!'로 유지됨
- 초기
- 폼이 리셋되면 내부
value상태가 초기화되는 방식으로 동작할 수 있음
속성은 설정, 프로퍼티는 상태에 더 자연스러움
- 속성은 설정(configuration) 에, 프로퍼티는 상태(state) 에 쓰는 구분이 자연스러움
<input value>는 이름을 제외하면 이 구분에 맞는 예임value속성은 기본값을 설정함value프로퍼티는 현재 상태를 제공함
- 프로퍼티를 읽거나 쓸 때 검증이 적용되고, 속성을 읽거나 쓸 때는 검증이 적용되지 않는 구분도 이 방식과 맞음
<details>와<dialog>는 열린 상태를open속성으로 표현하며, 브라우저가 사용자 상호작용에 따라 이 속성을 직접 추가하거나 제거함- 이 설계는 속성이 설정을 위한 것이라는 구분을 깨뜨림
- DOM을 유지관리하는 프레임워크나 일반 JavaScript 코드가 DOM이 스스로 바뀌는 상황에 대비해야 함
- 대안 설계는
<details defaultopen>같은 기본 설정 속성, 현재 상태를 읽고 쓰는details.open프로퍼티, 상태 타깃팅용 CSS 의사 클래스로 나눌 수 있음 - Simon Peters가 이와 관련한 초기 설계 논의를 찾아냄
contenteditable도 이 구분을 깨뜨리지만, 많은 깨짐을 감수하는 opt-in으로 취급됨
프레임워크별 처리 방식
- Preact와 VueJS는 일부 사전 정의된 예외를 제외하면
propName in element가 참일 때 프로퍼티를 설정하고, 그렇지 않으면 속성을 설정함- 기본적으로 속성보다 프로퍼티를 선호함
- 문자열 렌더링 메서드는 반대로 동작하며, 프로퍼티 전용 항목은 무시함
- 관련 구현: Preact의
setProperty, VueJS의shouldSetAsProp
- React는 일부 사전 정의된 예외를 제외하면 속성을 설정함
- lit-html은 속성과 프로퍼티의 차이를 문법으로 유지함
<input type="…" .value="…" />처럼 이름 앞에.을 붙이면 속성이 아니라 프로퍼티를 설정함- 관련 문서: Lit expressions
댓글과 토론
Hacker News 의견들
-
data- 속성*을 쓰면 글에서 다룬 방식들 사이의 괜찮은 절충안이 됨
이 속성들은 JavaScript에서dataset의 읽기/쓰기 속성으로 자동 노출되어document.getElementById('myDiv').dataset.payload == "something"처럼 접근 가능함
다만 HTML 속성은kebab-case, JavaScript는camelCase라서data-my-cool-data-attribute가dataset.myCoolDataAttribute가 되는 점을 조심해야 함- 대소문자 표기 자동 변환은 정말 싫음
camelCase를 쓰는 여러 생태계 전반에서 대문자 약어를 어떻게 다룰지 확립된 관례가 없어,dataset.myID같은 경우가 애매해짐 kebab-case와camelCase변환을 피하려고Element.hasAttribute('data-thing'),Element.getAttribute('data-thing'),Element.setAttribute('data-thing', '...')같은 메서드를 씀
DOM에서 검사하거나 디버깅할 때 어색한 변환을 거치지 않아도 되어 의도가 훨씬 명확함- MDN의 data attributes 문서가 HTML 문법, JavaScript 접근, CSS 접근, 접근성 관련 주의점을 다룸: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
또 https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...에서는data-*전역 속성을 “스크립트가 HTML과 DOM 표현 사이에서 독자적인 정보를 교환하게 해주는 사용자 정의 데이터 속성”이라고 설명함 - 이 속성들은 어느 정도 CSS에서도 접근 가능함
- JavaScript 실행 문맥 사이에서 통신하는 또 다른 흥미로운 방식이기도 함
여기서 말하는 문맥은 꼭 서로 다른iframe을 뜻하는 게 아니라, 브라우저 확장처럼 같은 DOM에 접근하는 별도 JavaScript 실행 문맥을 만드는 경우를 말함
개발자 도구 프로토콜 용어로는 이런 것을 isolated worlds라고도 부름
- 대소문자 표기 자동 변환은 정말 싫음
-
글에서 다룬 것들은 삶에 별 보탬이 없는 복잡성과 동작을 나타냄
이런 동작은 장점 없이 단점만 있고, 명세는 단순히 DOM 객체와 JavaScript 객체를 같은 것으로 정의해 속성과 프로퍼티도 같은 개념으로 만들었어야 함- 그런 제한이 “단순히” 있었다면 대부분의 현대 UI 프레임워크는 전혀 동작하지 않았을 것임
DOM 노드에 문자열이 아닌 값을 저장할 수 있다는 건 분명한 장점이고, 너무 가볍게 넘길 얘기는 아님 - “단순히”라는 부분만 빼면 동의함
모든 프로퍼티가 문자열만 될 수 있다고 요구하지 않는 한 전혀 단순하지 않음
숫자와의 변환, 객체를 설정할 수 있는 프로퍼티, 속성과 대응되지 않는 프로퍼티까지 고려해야 함
그래도 100% 고정된 관례나 규칙이 있었다면 더 나았겠지만, 그게 단순한 일은 아님 - 그렇게 하면 DOM 인터페이스에 문자열이 아닌 값을 둘 수 없고, 메서드도 포함되므로 DOM 기능 대부분이 사라짐
- 속성과 프로퍼티를 서로 대칭으로 만들기는 기존 DOM API에서는 어려움
addEventListener같은 메서드는 개별 DOM 요소의 프로토타입 프로퍼티지만, 문자열 속성으로 보면 말이 안 됨
그러면 DOM 요소 메서드는HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));같은 자유 함수가 되어야 할 텐데, 취향 문제이긴 해도 JavaScript에서 객체 메서드 호출을 쓸 수 있는 편이 좋음 - 왜 둘이 달라야 하는지 잘 모르겠음
DOM 요소에는setAttribute로 상태를 바꾸는 매우 구체적인 인터페이스가 있고, 그 인터페이스로 DOM에 메시지를 보내려고 만든 JavaScript 객체에 그냥 프로퍼티를 붙였을 때 왜 자동으로 라우팅되리라 기대해야 하는지 의문임
그런 API가 더 좋은 경험일 수는 있겠지만 몇 가지 난제가 있고, 그래도 더 깔끔하긴 할 것 같음
백엔드 코드를 너무 오래 써서 그런지 이런 인터페이스를 보면 내부에 이상한 일이 많고, 과거의 친절한 개발자가 가능한 한 가장 안전한 래퍼를 준 것처럼 느껴짐
- 그런 제한이 “단순히” 있었다면 대부분의 현대 UI 프레임워크는 전혀 동작하지 않았을 것임
-
이해에 도움이 되는 관점은 좁은 의미의 속성이 DOM 노드, 즉
Attr노드라는 점임
이 노드는name,value,ownerElement,prefix같은 프로퍼티를 가질 수 있고, 전체로는 배열이 아닌NamedNodeMap형태로 소유 요소의attributes프로퍼티 값으로 접근 가능함
그래서getAttribute(name)이라는 이름은 오해의 소지가 있고, 실제로는 속성 노드 자체가 아니라 해당 속성 노드의value프로퍼티를 반환하므로getAttributeValue(name)에 더 가까움
참고: https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Attr- 그 설명은 맞지만,
setAttribute,getAttribute,getAttributeNames,hasAttribute로 못 하고 속성 노드나el.attributes로만 할 수 있는 실용적인 일이 있는지는 모르겠음
속성 노드를 요소 사이에서 옮길 수도 없고, 요소 안에서 속성 노드 순서를 바꿀 수도 없음
- 그 설명은 맞지만,
-
Jake, 그러면 이건 어떡함: https://jakearchibald.com/2023/against-self-closing-tags-in-...
- “제 블로그에서 이 문법을 보게 될 텐데, 그건 Prettier가 그렇게 하기 때문이고 저는 Prettier를 정말 좋아합니다”
-
프로퍼티를 갱신하지 말자는 논리는 이해하지만, CSS로 대상을 지정할 수 있다는 점은 유용함
- 그래서 입력 요소의
:checked와 비슷하게 가상 클래스도 있어야 한다고 한 것임 - 나도 다른 모든 이유보다 이 경우가 큼
디버거를 거치는 것보다 DOM을 즉시 확인하는 편이 디버깅하기 훨씬 쉬움
- 그래서 입력 요소의
-
아마 가장 중요한 차이는 지속성인데, 글에서는 다루지 않음
속성은 페이지를 떠나지 않는 등 해당 DOM 트리의 같은 루트 객체에 접근 가능한 동안 유지됨
객체 프로퍼티는 해당 객체가 가비지 컬렉션되기 전까지만 유지됨- 객체는 속성을 가진 DOM 노드가 존재하는 동안 존재하므로 수명 주기는 같음
DOM 노드가 복제되거나 직렬화/역직렬화를 거치면 같은 속성을 가진 완전히 다른 DOM 노드가 되므로, 다른 노드의 기본값이 아닌 프로퍼티는 갖지 않게 됨 - 여기서는 속성과 프로퍼티의 기존 차이 말고는 실질적인 동작 차이가 없음
- 그 차이가 실제로 어떻게 드러나는지 궁금함
- 객체는 속성을 가진 DOM 노드가 존재하는 동안 존재하므로 수명 주기는 같음
-
흥미로운 글임
JavaScript의 DOM 요소를 늘 프록시처럼 봤음
실제 DOM 요소에 대해 작업하게 해주지만, 동시에 그냥 JavaScript 객체이므로 실제 요소와 상호작용하는 함수들을 쓰는 것 외에도 값을 할당하고 원하는 일을 할 수 있다고 느꼈음 -
예제와 함께 잘 설명되어 보기 좋고, 특히 종종 놓치기 쉬운 세부사항이 여럿 있어서 많은 사람에게 도움이 될 만함
-
DOM 프로퍼티는 IDL attributes라고도 불림
- 맞지만 일부러 그 용어를 피했음
명세 작성자 밖에서는IDL attributes라고 부르는 사람이 드물고, 둘 다 속성이라고 부르면 매우 헷갈림
- 맞지만 일부러 그 용어를 피했음
-
좋은 글이고, 더 나은 표현 방식이 어떤 모습일지 궁금해짐
HTML이 문자열이 아닌 속성을 가질 수 있고, 그 값이 스코프가 있는script태그의 로컬 값을 참조하는 식이면 흥미로울 것 같음
속성을 기본 설정으로 보는 관점은 좀 이상했음
예를 들어class속성이 기본값만 보여준다면 클래스를 토글할 때 DOM 검사기나 문자열화된 HTML에서 활성 클래스가 안 보이거나 비활성 클래스가 보이는 식으로 꽤 이상해질 것임- 기본 설정이라고 한 게 아니라 그냥 설정이라고 한 것임
class속성은 이 체계 안에서 잘 동작함
문서의 소유자인 내가class속성을 설정하고, 시간이 지나며 그 설정을 바꿀 수 있지만 그건 나만 하는 일임
대안적 체계라면 브라우저가 hover나 focus 상태에 반응해 스스로 클래스를 추가·제거할 텐데, 다행히 그러지 않고 가상 클래스가 사용됨
- 기본 설정이라고 한 게 아니라 그냥 설정이라고 한 것임