Document Object Model (DOM)/Attribute reflection

김동현·2026년 4월 7일

속성 getter/setter (Attribute getter/setter)

먼저 속성을 가져오고(get) 설정하는(set) 기본 매커니즘부터 살펴볼게요. 이 방법은 해당 속성이 (프로퍼티로) 반영(reflect)되든 안 되든 언제나 사용할 수 있답니다.
속성값을 가져오려면 Element 인터페이스의 getAttribute() 메서드를 호출하면서 속성 이름을 넘겨주면 돼요.
속성값을 설정할 때는 setAttribute() 메서드를 호출하면서 속성 이름과 새로운 값을 함께 넘겨주면 된답니다.

다음 HTML 코드를 예로 들어볼게요.

<input placeholder="Original placeholder" />

placeholder 속성을 가져오고 설정하는 방법은 이렇습니다.

const input = document.querySelector("input");

// placeholder 속성 가져오기
let attr = input.getAttribute("placeholder");

// placeholder 속성 설정하기
input.setAttribute("placeholder", "Modified placeholder");

반영되는 속성들 (Reflected attributes)

<input> 요소의 경우, placeholder 속성(attribute)은 HTMLInputElement.placeholder 프로퍼티(property)에 그대로 반영(reflect)돼요.
아까와 같은 HTML을 다시 볼게요.

<input placeholder="Original placeholder" />

placeholder 프로퍼티를 사용하면 아까와 똑같은 작업을 훨씬 더 자연스럽게 할 수 있답니다.

const input = document.querySelector("input");

// placeholder 속성 가져오기
let attr = input.placeholder;

// placeholder 속성 설정하기
input.placeholder = "Modified placeholder";

여기서 반영된 속성(attribute)의 이름과 프로퍼티(property)의 이름이 placeholder로 완전히 똑같다는 점을 눈여겨보세요.
하지만 항상 그런 건 아니에요. 프로퍼티 이름은 보통 카멜 케이스(camelCase) 규칙을 따르거든요.
특히 속성 이름이 여러 단어로 되어 있거나, 하이픈(-)처럼 프로퍼티 이름에는 쓸 수 없는 문자가 포함되어 있을 때 더 그래요.
예를 들어 aria-checked 속성은 ariaChecked 프로퍼티에 반영된답니다.

반영되는 불리언 속성 (Boolean reflected attributes)

불리언 속성(Boolean attributes)은 이름과 값을 무조건 같이 적어줄 필요가 없다는 점에서 다른 속성들과는 약간 달라요.
예를 들어, 아래의 체크박스 <input> 요소는 checked 속성을 가지고 있어서, 화면에 표시될 때 체크된 상태로 나온답니다.

<input type="checkbox" checked />

Element.getAttribute() 메서드를 사용해보면, 이 인풋 요소가 체크되어 있을 땐 ""(빈 문자열)을 반환하고 체크되어 있지 않을 땐 null을 반환해요.
이와 짝을 이루는 HTMLInputElement.checked 프로퍼티는 체크 상태에 따라 직관적으로 truefalse를 반환하죠.
이 점을 제외하면, 불리언 반영 속성들도 다른 일반적인 반영 속성들과 똑같이 동작한답니다.

반영되는 열거형 속성 (Enumerated reflected attributes)

HTML에서 열거형 속성(enumerated attributes)이란, 미리 정해진 몇 가지 텍스트 값들만 가질 수 있도록 제한된 속성을 말해요. 예를 들어 전역 HTML 속성인 dir은](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir)은) 오직 ltr, rtl, auto 이렇게 세 가지 값만 가질 수 있답니다.

<p dir="rtl">Right to left</p>

HTML 태그 이름과 마찬가지로, HTML의 열거형 속성값들도 대소문자를 구별하지 않아요. 그래서 LTR, RTL, AUTO라고 적어도 똑같이 잘 동작하죠.

<p dir="RTL">Right to left</p>

하지만 IDL에 반영된 프로퍼티인 HTMLElement.dir은](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dir)은), 스펙에 정의된 대로 항상 정규화된(canonical) 값(이 예제에서는 소문자 값)만을 반환해요. 프로퍼티에 값을 직접 할당할 때도, 알아서 정규화된 형태로 변환(serialize)되어 저장된답니다.

const pElement = document.querySelector("p");
console.log(pElement.dir); // "rtl"
pElement.dir = "RTL";
console.log(pElement.dir); // "rtl"

만약 이게 싫다면 Element 인터페이스의 getAttribute() 메서드를 사용하시면 돼요. 이 메서드는 HTML에 적혀있는 속성값을 그 어떤 변형도 없이 그대로 가져와 준답니다.

const pElement = document.querySelector("p");
console.log(pElement.getAttribute("dir")); // "RTL"

반영되는 요소 참조 (Reflected element references)

참고:
이 섹션의 내용은 요소 참조를 포함하고 있는 반영된 ARIA 속성들에 적용되는 내용이에요.
앞으로 요소 참조를 반영하는 다른 속성들이 추가된다면, 아마 그 속성들에도 동일한 규칙이 적용될 거예요.

어떤 속성들은 값으로 다른 요소에 대한 참조(references)를 받기도 해요. 요소의 id 값을 하나만 받거나, 스페이스바(공백)로 구분된 여러 개의 id 값들을 문자열로 받죠.
id 값들은 해당 속성과 어떤 연관이 있거나, 그 속성이 필요로 하는 정보를 담고 있는 다른 요소들을 가리킨답니다.
이러한 속성들은 짝을 이루는 프로퍼티에 반영되는데, 이때 해당 id 값과 일치하는 HTMLElement 기반 객체 인스턴스들의 배열 형태로 나타나요. (다만 몇 가지 주의할 점이 있어요)

예를 들어볼게요. aria-labelledby 속성은, 어떤 요소의 접근성 이름(accessible name)을 내부 텍스트로 가지고 있는 다른 요소들의 id 값 목록을 받아요.
아래 HTML 코드를 보면, <input> 요소가 label_1, label_2, label_3라는 id를 가진 세 개의 <span> 요소들로 자신의 라벨을 정의하고 있는 걸 볼 수 있죠.

<span id="label_1">(Label 1 Text)</span>
<span id="label_2">(Label 2 Text)</span>
<input aria-labelledby="label_1 label_2 label_3" />

이 속성은 Element.ariaLabelledByElements 프로퍼티에 반영되는데, 이 프로퍼티는 각각의 id 값에 해당하는 요소들의 배열을 반환해요.
이 속성과 프로퍼티를 확인하는 코드는 다음과 같아요.

const inputElement = document.querySelector("input");

console.log(inputElement.getAttribute("aria-labelledby"));
// "label_1 label_2 label_3"

console.log(inputElement.ariaLabelledByElements);
// [HTMLSpanElement, HTMLSpanElement]

위 코드에서 가장 먼저 눈에 띄는 점은 속성(attribute)이 가지고 있는 항목 수와 프로퍼티(property)가 반환하는 요소의 개수가 다르다는 거예요. label_3라는 참조값에 해당하는 실제 요소가 존재하지 않기 때문에, 프로퍼티가 속성의 내용을 완벽하게 그대로 반영하지는 못하는 거죠.
이처럼 참조값이 일치하지 않는 경우는 해당 id가 요소의 유효 범위(scope)를 벗어났기 때문일 수도 있어요.
예를 들어 참조 대상 요소가 현재 요소와 같은 DOM이나 섀도우(shadow) DOM 안에 있지 않다면 이런 일이 발생해요. 왜냐하면 id는 오직 그것이 선언된 스코프 안에서만 유효하거든요.

우리는 프로퍼티가 반환한 배열 속 요소들을 하나씩 순회하면서 그 내부 텍스트를 가져와 접근성 이름을 만들 수 있어요. (속성을 사용해서 id 문자열을 먼저 가져오고, 그걸로 요소를 일일이 찾아서 텍스트를 추출하는 것보다 이 방법이 훨씬 자연스러워요. 게다가 현재 스코프 안에서 실제로 접근 가능한 확실한 요소들만 다루면 되니까요.)

const inputElement = document.querySelector("input");
const accessibleName = inputElement.ariaLabelledByElements
  .map((e) => e.textContent.trim())
  .join(" ");
console.log(accessibleName);
// (Label 1 Text) (Label 2 Text)

프로퍼티와 속성 설정하기 (Setting the property and attribute)

일반적인 반영 프로퍼티들의 경우, 프로퍼티 값을 바꾸면 연결된 속성(attribute)값도 같이 바뀌고 그 반대도 마찬가지예요.
하지만 요소 참조를 반영하는 프로퍼티에서는 동작 방식이 다릅니다.
여기서는 프로퍼티에 값을 설정하면 속성이 아예 비워져 버려서(초기화되어서), 더 이상 프로퍼티와 속성이 서로를 반영하지 않는 상태가 돼요.
다음 HTML을 예로 들어 설명해 볼게요.

<span id="label_1">(Label 1 Text)</span>
<span id="label_2">(Label 2 Text)</span>
<input aria-labelledby="label_1 label_2" />

처음에 aria-labelledby 속성값은 "label_1 label_2"였지만, DOM API를 사용해서 프로퍼티에 값을 설정하면 속성값은 ""(빈 문자열)로 초기화돼 버립니다.

const inputElement = document.querySelector("input");

let attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// "label_1 label_2"

// 반영된 프로퍼티를 이용해 값 설정하기
inputElement.ariaLabelledByElements = document.querySelectorAll("span");

attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// ""

왜 이렇게 만들어졌을까요? 만약 이렇게 하지 않으면, id가 아예 없는 요소들을 프로퍼티에 마구 할당했을 때 이 요소들을 문자열 속성에 어떻게 표현해야 할지 알 수 없기 때문이에요. 그래서 이게 합리적인 동작 방식이랍니다.

물론, 속성(attribute) 쪽에 값을 직접 다시 설정해 주면 속성과 프로퍼티 사이의 연결고리가 다시 복구돼요.
아까 예제 코드에 이어서 살펴보시죠.

inputElement.setAttribute("aria-labelledby", "label_1");

attributeValue = inputElement.getAttribute("aria-labelledby");
console.log(attributeValue);
// "label_1"

// 다시 연결된 프로퍼티 확인하기
console.log(inputElement.ariaLabelledByElements);
// [HTMLSpanElement] - `label_1`에 해당하는 요소만 나옴

여기서 주의할 점은, 프로퍼티가 반환하는 배열은 정적(static)인 복사본이라는 거예요. 즉, 반환된 배열에 요소를 추가하거나 뺀다고 해서 원본 속성에 변경 사항이 반영되지 않아요.
프로퍼티에 새로운 배열을 통째로 할당할 때만 내부적으로 복사가 이루어지기 때문에, 그 이후에 속성값이 바뀌더라도 이전에 이미 반환받았던 프로퍼티 배열은 예전 상태 그대로 남아있게 됩니다.

요소 id 참조의 유효 범위 (Element id reference scope)

속성을 통한 요소 참조는 오직 같은 DOM이나 같은 섀도우 DOM(Shadow DOM) 안에 있는 다른 요소들만 가리킬 수 있어요. 요소의 id는 그것이 선언된 스코프(scope) 내에서만 의미가 있거든요.

다음 코드에서 이를 명확히 확인할 수 있어요.
<input> 요소의 aria-labelledby 속성은 label_1, label_2, label_3라는 id를 가진 요소들을 참조하려고 하고 있죠.
하지만 여기서 label_3<input> 요소와 다른 스코프에 선언되어 있어서 이 상황에서는 쓸모없는 id가 돼요.
결과적으로 라벨은 오직 label_1label_2 요소에서만 가져오게 된답니다.

<div id="in_dom">
  <span id="label_3">(Label 3 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_1">(Label 1 Text)</span>
    <input aria-labelledby="label_1 label_2 label_3" />
    <span id="label_2">(Label 2 Text)</span>
  </template>
</div>

반영된 요소 참조의 유효 범위 (Reflected element reference scope)

aria-labelledby를 위한 Element.ariaLabelledByElements 같이, ARIA 요소 참조를 통해 반영되는 인스턴스 프로퍼티들을 사용할 때는 스코프(유효 범위) 규칙이 살짝 달라져요.
참조 대상이 스코프 안에 들어오려면, 그 대상 요소가 참조를 요청하는 요소와 아예 같은 DOM에 있거나, 아니면 부모 DOM에 있어야 한답니다.
다른 DOM에 있는 요소들, 예를 들면 참조를 요청하는 DOM의 자식이나 형제뻘 되는 섀도우 DOM 속에 있는 요소들은 스코프 밖이라서 접근할 수 없어요.

아래 예제는 부모 DOM에 있는 요소(label_3)와 같은 섀도우 루트 안에 정의된 요소들(label_1, label_2)을 타겟으로 설정하는 모습을 보여줘요.
모든 타겟 요소들이 참조를 요청하는 요소의 스코프 안에 포함되기 때문에 이 코드는 완벽하게 동작한답니다.

<div id="in_dom">
  <span id="label_3">(Label 3 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_1">(Label 1 Text)</span>
    <input id="input" />
    <span id="label_2">(Label 2 Text)</span>
  </template>
</div>
const host = document.getElementById("host");
const input = host.shadowRoot.getElementById("input");
input.ariaLabelledByElements = [
  host.shadowRoot.getElementById("label_1"),
  host.shadowRoot.getElementById("label_2"),
  document.getElementById("label_3"),
];

반대로, DOM에 있는 요소가 섀도우 DOM에 깊숙이 들어가 있는 요소를 참조하려고 한다면 이 코드는 먹히지 않아요. 중첩된 섀도우 DOM 안에 있는 타겟 요소들은 스코프 밖으로 취급되기 때문이죠.

<div id="in_dom">
  <span id="label_1">(Label 1 Text)</span>
  <input id="input" />
  <span id="label_2">(Label 2 Text)</span>
</div>
<div id="host">
  <template shadowrootmode="open">
    <span id="label_3">(Label 3 Text)</span>
  </template>
</div>
const host = document.getElementById("host");
const input = document.getElementById("input");
input.ariaLabelledByElements = [
  host.shadowRoot.getElementById("label_3"),
  document.getElementById("label_1"),
  document.getElementById("label_2"),
];

참고로, 어떤 요소가 원래는 "스코프 안"에 있었다가 나중에 깊은 섀도우 루트 안쪽으로 옮겨져서 "스코프 밖"으로 밀려날 수도 있어요.
이렇게 되면 속성(attribute) 문자열 안에는 해당 요소의 참조가 여전히 남아있겠지만, 프로퍼티가 반환하는 배열 목록에서는 쏙 빠지게 된답니다.
물론 그 요소가 다시 스코프 안으로 돌아오면, 프로퍼티가 반환하는 배열에 다시 그 요소가 나타날 거예요.

속성과 프로퍼티의 관계 요약 (Summary of the attribute/property relationship)

요소 참조를 담고 있는 속성(attribute)과 그에 대응하는 프로퍼티(property) 사이의 관계를 한눈에 알아보기 쉽게 요약해 드릴게요.

  • 속성 안의 요소 id 참조는 타겟 요소가 자신과 같은 DOM이나 섀도우 DOM에 선언되어 있을 때만 유효한 스코프(in-scope)로 인정받아요.
  • ARIA 요소 참조를 반영하는 프로퍼티들은 자신과 같은 스코프에 있거나 부모 스코프에 있는 요소들을 타겟으로 삼을 수 있어요. 단, 더 깊숙이 중첩된 스코프에 있는 요소들에는 접근할 수 없답니다.
  • 프로퍼티에 값을 설정하면 기존 속성값이 지워지고, 둘 사이의 반영 관계가 끊어져요.
    이때 Element.getAttribute()로](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute)로) 속성을 읽어보면 ""(빈 문자열)이 나온답니다.
  • Element.setAttribute()로](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)로) 속성에 다시 값을 설정해 주면, 프로퍼티에도 값이 적용되면서 끊어졌던 "반영 관계"가 회복돼요.
  • 속성에 값을 잘 참조시켜 두었다가, 나중에 해당 요소가 스코프 밖으로 이동해 버리면 프로퍼티 배열에서 그 요소가 사라져요.
    하지만 속성(문자열) 자체에는 참조값이 그대로 남아있기 때문에, 요소가 다시 스코프 안으로 들어오면 프로퍼티 배열에 다시 그 요소가 포함된답니다 (관계가 복구되는 거죠).
profile
프론트에_가까운_풀스택_개발자

0개의 댓글