6P by GN⁺ | ★ favorite | 댓글 3개
  • 같은 요소에 붙어 있어도 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.fooundefined일 수 있음
    • 이후 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.testundefined일 수 있음
  • 속성 이름과 달리 속성 은 대소문자를 구분함

속성을 반영하는 프로퍼티

  • 일부 프로퍼티는 특정 속성을 반영(reflect)
    • div.idid 속성을 읽고 쓰는 getter/setter로 동작함
    • div.id = 'bar'를 실행하면 id 속성도 'bar'로 바뀜
  • 반영 관계에서는 속성이 데이터의 원천임
    • 프로퍼티를 설정하면 속성을 업데이트함
    • 프로퍼티를 읽으면 속성 값을 읽음
  • 명세에 정의된 속성은 편의를 위해 대응 프로퍼티가 만들어지는 경우가 많음
    • 임의의 foo 속성은 명세에 정의된 속성이 아니므로, 대응하는 foo 프로퍼티가 자동으로 생기지 않음
  • <ol> 명세는 “Content attributes”에서 속성을, “DOM interface”에서 프로퍼티를 정의함
    • reversedtype IDL 속성은 같은 이름의 콘텐츠 속성을 reflect해야 함

이름이 달라지는 반영 관계

  • 어떤 프로퍼티는 반영하는 속성과 이름이 다름
  • JavaScript 프로퍼티 스타일에 맞추기 위해 이름이 달라지는 경우가 있음
    • <img>el.crossOrigincrossorigin 속성을 반영함
    • 모든 요소의 el.ariaLabelaria-label 속성을 반영함
    • ARIA 반영 프로퍼티는 2023년 말에 크로스 브라우저가 됐고, 그 전에는 속성만 사용할 수 있었음
  • 과거 JavaScript 예약어 때문에 이름이 달라진 경우도 있음
    • 모든 요소의 el.classNameclass 속성을 반영함
    • <label>el.htmlForfor 속성을 반영함

검증, 타입 변환, 기본값

  • 프로퍼티에는 검증과 기본값이 붙을 수 있지만, 속성 자체에는 그런 동작이 없음
  • 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.opentrue
    • details.open = falseopen 속성을 제거함
    • details.open = 'hello'는 문자열을 그대로 속성에 넣지 않고 boolean으로 변환해 open 속성이 존재하는 상태가 됨
  • img.height 같은 프로퍼티는 속성 값을 숫자로 변환함
    • setter는 들어온 값을 숫자로 바꾸고, 음수는 0으로 처리함

input.value는 현재값과 기본값을 분리함

  • input에는 value 프로퍼티와 value 속성이 있지만, value 프로퍼티는 value 속성을 반영하지 않음
  • value 속성을 반영하는 프로퍼티는 defaultValue
    • defaultValue getter는 value 속성 또는 빈 문자열을 반환함
    • defaultValue setter는 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는 일부 사전 정의된 예외를 제외하면 속성을 설정함
    • 이 방식은 문자열 렌더링 메서드와 비슷한 로직을 만듦
    • 커스텀 요소의 프로퍼티는 React의 사전 정의 목록에 없기 때문에 속성으로 설정되고, 프로퍼티 전용 API는 동작하지 않을 수 있음
    • React 19에서는 커스텀 요소에 대해 Preact/VueJS 모델로 전환될 예정임
    • React는 className이라는 프로퍼티 이름을 속성처럼 보이는 문법으로 대중화했지만, 내부적으로는 class 속성을 설정함
    • 관련 구현: React의 setProp
  • 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-attributedataset.myCoolDataAttribute가 되는 점을 조심해야 함

    • 대소문자 표기 자동 변환은 정말 싫음
      camelCase를 쓰는 여러 생태계 전반에서 대문자 약어를 어떻게 다룰지 확립된 관례가 없어, dataset.myID 같은 경우가 애매해짐
    • kebab-casecamelCase 변환을 피하려고 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가 더 좋은 경험일 수는 있겠지만 몇 가지 난제가 있고, 그래도 더 깔끔하긴 할 것 같음
      백엔드 코드를 너무 오래 써서 그런지 이런 인터페이스를 보면 내부에 이상한 일이 많고, 과거의 친절한 개발자가 가능한 한 가장 안전한 래퍼를 준 것처럼 느껴짐
  • 이해에 도움이 되는 관점은 좁은 의미의 속성이 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 노드가 되므로, 다른 노드의 기본값이 아닌 프로퍼티는 갖지 않게 됨
    • 여기서는 속성과 프로퍼티의 기존 차이 말고는 실질적인 동작 차이가 없음
    • 그 차이가 실제로 어떻게 드러나는지 궁금함
  • 흥미로운 글임
    JavaScript의 DOM 요소를 늘 프록시처럼 봤음
    실제 DOM 요소에 대해 작업하게 해주지만, 동시에 그냥 JavaScript 객체이므로 실제 요소와 상호작용하는 함수들을 쓰는 것 외에도 값을 할당하고 원하는 일을 할 수 있다고 느꼈음

  • 예제와 함께 잘 설명되어 보기 좋고, 특히 종종 놓치기 쉬운 세부사항이 여럿 있어서 많은 사람에게 도움이 될 만함

  • DOM 프로퍼티는 IDL attributes라고도 불림

    • 맞지만 일부러 그 용어를 피했음
      명세 작성자 밖에서는 IDL attributes라고 부르는 사람이 드물고, 둘 다 속성이라고 부르면 매우 헷갈림
  • 좋은 글이고, 더 나은 표현 방식이 어떤 모습일지 궁금해짐
    HTML이 문자열이 아닌 속성을 가질 수 있고, 그 값이 스코프가 있는 script 태그의 로컬 값을 참조하는 식이면 흥미로울 것 같음
    속성을 기본 설정으로 보는 관점은 좀 이상했음
    예를 들어 class 속성이 기본값만 보여준다면 클래스를 토글할 때 DOM 검사기나 문자열화된 HTML에서 활성 클래스가 안 보이거나 비활성 클래스가 보이는 식으로 꽤 이상해질 것임

    • 기본 설정이라고 한 게 아니라 그냥 설정이라고 한 것임
      class 속성은 이 체계 안에서 잘 동작함
      문서의 소유자인 내가 class 속성을 설정하고, 시간이 지나며 그 설정을 바꿀 수 있지만 그건 나만 하는 일임
      대안적 체계라면 브라우저가 hover나 focus 상태에 반응해 스스로 클래스를 추가·제거할 텐데, 다행히 그러지 않고 가상 클래스가 사용됨

attribute 와 property를 같은 "속성" 으로 번역해 버렸네요..