[모던JS: 브라우저] 문서 Document (3)

KG·2021년 6월 11일
0

모던JS

목록 보기
29/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

속성과 프로퍼티

브라우저는 HTML 문서를 파싱하여 DOM 객체를 생성한다. 앞서 요소 노드의 대부분은 표준 HTML 속성(attribute)을 DOM 객체의 프로퍼티로 가지는 것을 살펴보았다.

이쯤에서 용어를 다시 한 번 재정립하고 넘어가자. 프로퍼티(property)는 속성으로 번역되기도 하는데 해당 파트에서는 프로퍼티와 속성을 구분한다.

  • 속성(attribute) : HTML 표준 속성으로 id, class와 같은 속성. 태그마다 고유한 속성을 가지는 경우도 있음.

  • 프로퍼티(property) : HTML을 파싱해 DOM 객체로 생성할 때 각 객체가 가지고 있는 프로퍼티. 대부분 HTML 표준 속성과 동일하지만 다른 경우도 있음.

이번 챕터에서는 속성과 프로퍼티가 어떤 연관관계를 가지고 있고, 어떤 상황에서 일대일로 매핑이 되는지, 또 언제 매핑되지 않는지에 대해 살펴보도록 하자.

1) DOM 프로퍼티

앞서 내장 DOM 프로퍼티를 살펴보며 많은 종류가 있다는 것을 보았다. 만약 이런 내장 프로퍼티만으로 충분하다고 판단되지 않는 경우엔 자신만의 커스텀 프로퍼티를 만들 수도 있다. DOM은 결국 자바스크립트 객체이기 때문에 객체 프로퍼티를 추가하는 방식으로 얼마든지 설정이 가능하다.

document.body.myData = {
  name: "Caesar',
  title: "Imperator",
};

console.log( document.body.myData.title ); // Imperator

물론 메서드 역시 추가할 수 있다.

document.body.sayTagName = function() {
  console.log(this.tagName);
}

document.body.sayTagName();	// BODY

DOM 프로퍼티로 지정되는 값은 대소문자를 구분하는 것에 주의하자. 이는 앞서 객체 파트에서부터 다뤄온 내용이기에 크게 낯설지 않을 것이다.

2) HTML 속성

HTML에서 태그는 복수의 속성을 가질 수 있다. 브라우저는 이러한 HTML을 파싱해서 DOM 객체를 만들 때 HTML 표준 속성을 인식하고, 이 표준 속성을 사용해 DOM 프로퍼티를 만들게 된다.

따라서 요소가 id와 같은 표준 속성으로만 구성되어 있는 경우엔 이에 해당되는 프로퍼티 역시 자동으로 생성된다. 그러나 표준이 아닌 속성이 사용된 경우에는 프로퍼티로 전환되지 않는다.

<body id='test' something='non-standard'>
  <script>
    console.log(document.body.id);	  // test
    console.log(document.body.something); // undefined
  </script>
</body>

또한 한 요소에서는 표준 속성이지만, 다른 요소에서는 표준이 아닌 경우도 있다. 예를 들어 type 속성은 <input> 요소에서는 표준 속성이지만 다른 태그에서는 표준이 아니다.

이처럼 표준 속성이 아닌 경우엔 이에 대해 일대일로 매핑되는 DOM 프로퍼티가 생성되지 않는다. 그렇지만 DOM 객체에서는 비표준 속성에 접근할 수 있는 메서드를 제공한다.

  • elem.hasAttribute(name) : 속성 존재 여부 확인
  • elem.getAttribute(name) : 속성 값 접근
  • elem.setAttribute(name, value) : 속성 값 설정
  • elem.removeAttribute(name) : 속성 값 제거
  • elem.attributes : 모든 속성 값을 내장 클래스 Attr 형태로 반환
<body something='non-standard'>
  <script>
    console.log(document.body.getAttribute('something');
  </script>
</body>

이때 HTML 속성은 DOM 프로퍼티와 달리 다음 특징을 보인다.

  • 대소문자를 가리지 않는다. idID는 동일한 값에 접근한다.
  • 값은 항상 문자열이다.

3) 프로퍼티-속성 동기화

표준 속성이 갱신되는 경우엔 대응하는 DOM 프로퍼티 역시 자동으로 갱신된다. 몇몇 경우를 제외하곤 프로퍼티가 변하는 경우에도 속성 역시 마찬가지로 같이 반영된다.

<input />

<script>
  let input = document.querySelector('input');
  
  // 속성 추가 => 프로퍼티 동시 갱신
  input.setAttribute('id', 'id');
  console.log(input.id);
  
  // 프로퍼티 변경 => 속성 동시 갱신
  input.id = 'newId';
  console.log(input.getAttribute('id'));
</script>

하지만 input.value와 같은 일부 프로퍼티는 동기화가 속성에서 프로퍼티 방향으로만 일어나는 예외상황도 존재한다.

<input />

<script>
  let input = document.querySelector('input');
  
  // 속성 추가 => 프로퍼티 동시 갱신
  input.setAttribute('value', 'text');
  console.log(input.value); // text
  
  // 프로퍼티 갱신 => 그러나 속성은 변경되지 않음
  input.value = 'newValue';
  console.log(input.getAttribute('value')); // text
</script>

이런 기능은 간혹 유용하게 사용될 수 있다. 유저의 어떤 행동으로 인해 value가 수정되었을 때 수정 전의 원래 값으로 복구하고 싶은 경우에 속성에 저장된 값을 가지고 오는 식으로 복구할 수 있기 때문이다.

4) DOM 프로퍼티 값의 타입

DOM 프로퍼티는 HTML 속성과 달리 항상 문자열이 아니다. 예를 들어 체크 박스 타입에서 사용되는 input.checked 프로퍼티의 경우 Boolean 타입의 값을 가지고 있다.

<input id="input" type="checkbox" checked> checkbox

<script>
  alert(input.getAttribute('checked')); // 속성 값: 빈 문자열
  alert(input.checked); // 프로퍼티 값: true
</script>

또한 DOM 객체의 style 프로퍼티 역시 문자열이 아닌 객체를 값으로 가지고 있다.

<div id="div" style="color:red; font-size:120%">Hello</div>

<script>
  // string
  alert(div.getAttribute('style')); // color:red;font-size:120%

  // object
  alert(div.style); // [object CSSStyleDeclaration]
  alert(div.style.color); // red
</script>

그렇지만 대부분의 프로퍼티 값은 HTML 속성과 같이 문자열을 가진다. 이때 동일하게 문자열을 공유하더라도 속성과 프로퍼티의 값이 다른 경우도 있다. 예를 들어 <a> 태그의 href 속성이 그러한 경우이다. href 속성이 상대 URL 주소이거나 #hash 처럼 해시링크를 이용하는 경우에도 DOM 객체의 href 프로퍼티는 항상 URL 전체가 저장된다.

<a id="a" href="#hello">link</a>
<script>
  // 속성
  alert(a.getAttribute('href')); // #hello

  // 프로퍼티
  alert(a.href ); 
  // 실행되는 사이트 주소에 따라 http://site.com/page#hello 형태로 값이 저장
</script>

5) 비표준 속성 dataset

HTML을 작성할 때는 대부분 표준 속성을 이용하여 작성한다. 그렇지만 표준이 아닌 속성도 존재하고, 비표준 속성은 개발자가 임의로 정의하여 사용할 수 있다.

이러한 비표준 속성은 보통 사용자가 직접 지정한 데이터를 HTML에서 자바스크립트로 바로 넘기고 싶은 경우나 자바스크립트를 사용해 조작할 HTML 요소를 표시하기 위해 사용한다.

<!-- 이름(name) 정보를 보여주는 div라고 표시 -->
<div show-info='name'></div>
<!-- 나이(age) 정보를 보여주는 div라고 표시 -->
<div show-info='age'></div>

<script>
  let user = {
    name: 'KG',
    age: 27,
  };
  
  for (let div of document.querySelectorAll('[show-info]')) {
    let field = div.getAttribute('show-info');
    div.innerHTML = user[field];
    // show-info 값을 기준으로 name엔 'KG', age엔 27이 삽입
  }
</script>

또한 비표준 속성은 요소에 스타일을 적용할 때도 사용이 가능하다.

<style>
  .order[order-state="new"] {
    color: green;
  }
  .order[order-state="pending"] {
    color: blue;
  }
  .order[order-state="canceled"] {
    color: red;
  }
</style>

<div class="order" order-state="new">
  A new order.
</div>

<div class="order" order-state="pending">
  A pending order.
</div>

<div class="order" order-state="canceled">
  A canceled order.
</div>

이렇게 커스텀 속성을 사용하는 것이 클래스를 통해 일일이 구분된 클래스명을 사용하는 것보다 더 선호되곤 한다. 왜냐하면 속성이 클래스보다 더 다루기 간편하기 때문이다. 속성의 상태는 새 클래스를 추가 또는 제거하는 것 보다 제공되는 메서드를 통해 손쉽게 변경할 수 있다. 물론 클래스를 통해 이러한 설정을 적용하는 것 역시 다른 메서드를 통해 가능하다.

div.setAttribute('order-state', 'canceled');

커스텀 속성에는 근본적으로 한 가지 문제점이 대두되는데, 바로 개발자가 임의로 지정한 속성이 추후 표준으로 등재될 수 있다는 점이다. HTML은 계속해서 표준안이 개정되기 때문에 내가 사용한 커스텀 속성이 언제든지 표준으로 변경될 가능성이 있다. 이런 경우에는 예기치 못한 부작용이 발생할 수 있다.

따라서 이런 충돌 상황을 방지하기 위해 HTML에는 기본적으로 data-* 라는 속성을 제공한다. 이 속성은 개발자가 용도에 맞게 사용할 수 있도록 별도로 예약된 키워드이다. 그리고 해당 속성은 DOM 객체에서 dataset 프로퍼티로 접근할 수 있다.

<body data-about='Elephants'>
  <script>
    console.log(document.body.dataset.about); // Elephants
  </script>
</body>

먄약 data-order-state와 같이 여러 단어로 구성된 속성은 자바스크립트 객체 내 프로퍼티에서는 카멜 표기법(Camel Case)로 자동으로 변환된다.

<style>
  .order[data-order-state="new"] {
    color: green;
  }
  .order[data-order-state="pending"] {
    color: blue;
  }
</style>

<div id="order" class="order" data-order-state="new">
  A new order.
</div>

<script>
  console.log(order.dataset.orderState);
  
  order.dataset.orderState = 'pending';
</script>

이처럼 비표준 커스텀 속성을 사용할 때는 data-* 속성을 통해 안전하고 유효하게 관리할 수 있다.

문서 수정하기

DOM 조작 메서드를 사용하여 HTML 문서에 새로운 요소를 추가 및 제거하거나 기존 콘텐츠를 수정하는 방법에 대해 살펴보자.

1) 요소 생성하기

DOM 노드를 만들때 사용하는 메서드는 두 가지이다.

document.createElement(tag)

태그 이름을 이용해 새로운 요소 노드를 만든다.

let div = document.createElement('div');

document.createTextNode(text)

주어진 텍스트를 사용해 새로운 텍스트 노드를 만든다.

let textNode = document.createTextNode('hello');

노드를 생성했다면 해당 노드에 DOM 프로퍼티를 지정해주어 HTML 속성값을 만들어줄 수 있다.

let div = document.createElement('div');

div.className = 'alert';

div.innerHTML = "<strong>hello!</strong>";

위 과정을 거치면 해당 내용과 클래스명을 가진 div 요소 노드가 하나 생성된다. 하지만 아직 이 요소 노드는 변수에만 담겨있을 뿐 브라우저 페이지에 추가되지 않은 상태이다.

2) 삽입 메서드

위에서 만들어준 div가 페이지에 나타나기 위해서는 document 어딘가에 해당 노드가 삽입되어야 한다. 이때 사용할 수 있는 요소 삽입 메서드로는 append가 있다. 해당 메서드를 사용하여 <body> 태그 안 쪽에 위에서 만들어준 요소 노드를 삽입해보자.

let div = document.createElement('div');
div.className = 'alert';
div.innerHTML = "<strong>hello!</strong>";

document.body.append(div);

이때 삽입하고 싶은 위치에 따라 <body> 태그가 아닌 다른 요소를 지정해 줄 수 있다. 자바스크립트에서 지원하는 노드 삽입 메서드는 다음과 같다. 적절한 메서드를 사용하면 직접 삽입 위치를 지정할 수 있다.

  • node.append(노드 또는 문자열) : node 끝 부분에 삽입
  • node.prepend(노드 또는 문자열) : node 맨 앞에 삽입
  • node.before(노드 또는 문자열) : node 이전에 삽입
  • node.after(노드 또는 문자열) : node 다음에 삽입
  • node.replaceWith(노드 또는 문자열) : node를 새로운 노드 또는 문자열로 대체

삽입 메서드에 문자열을 넘겨주는 경우에는 요소 노드가 아닌 자동으로 텍스트 노드가 만들어진다.

또한 해당 메서드들을 이용해 여러 개의 노드와 문자열을 한 번에 삽입하는 것 역시 가능하다.

<div id="div"></div>
<script>
  div.before('<p>hello</p>', document.createElement('hr'));
</script>

이때 문자열 '<p>hello</p>'의 경우엔 텍스트 노드로 변환된다는 점을 주의하자. 즉 이 경우는 elem.textContent를 사용할 때 처럼 순수 텍스트가 전달된다. 즉 태그로 지정된 <p></p>의 경우는 요소 노드가 아닌 텍스트로 지정된다. 따라서 최종 결과는 다음과 같아진다.

&lt;p&gt;hello&lt;/p&gt;
<hr>
<div id="div"></div>

만약 elem.innerHTML을 사용한 것 처럼 문자열로 태그를 전달하더라도 이를 정상적으로 파싱할 수 있게끔 하려면 다른 메서드를 사용해야 한다.

3) insertAdjacentHTML/Text/Element

elem.insertAdjacentHTML(where, html) 메서드를 사용하면 원하는 위치에 항상 HTML 그 자체를 삽입할 수 있다. 기준이 되는 elem 객체는 삽입 위치의 기준이 되는 대상이고, 이때 첫 번째 매겨변수 where은 항상 다음 값 중 하나여야 한다.

  • beforebegin : elem 바로 앞에 html 삽입
  • afterbegin : elem의 첫 번째 자식 요소 바로 앞에 html 삽입
  • beforeend : elem의 마지막 요소 바로 다음에 html 삽입
  • afterend : elem 바로 다음에 html 삽입

해당 메서드를 이용하면 문자열 형태로 HTML 구조를 전달하더라도 온전한 HTML 구조로 요소 노드를 만들어 삽입이 이루어진다.

<div id="div"></div>
<script>
  div.insertAdjacentHTML('beforebegin', '<p>hello</p>');
  div.insertAdjacentHTML('afterend', '<p>bye</p>');
</script>
<p>hello</p>
<div id="div"></div>
<p>bye</p>

소제목과 같이 insertAdjacentHTML 메서드는 형제 메서드가 있다. 각각의 형제 메서드의 이름에서 유추할 수 있듯이 insertAdjacentText는 문자열을 그대로 삽입하고, insertAdjacentElement는 요소만을 취급한다. 보통 요소나 문자열을 삽입하는 경우엔 append/prepend/before/after과 같은 메서드를 이용하므로 실무에선 대부분 insertAdjacentHTML 메서드를 사용한다.

4) 노드 삭제하기

node.remove()를 사용해서 생성한 노드 또는 기존 노드를 제거할 수 있다. 다음은 div 태그를 하나 생성하고 페이지에 추가한 뒤 1초후 다시 제거하는 코드이다.

<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>안녕하세요!</strong> 중요 메시지를 확인하셨습니다.";

  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
</script>

참고로 요소 노드를 다른 곳으로 옮길 때엔 기존에 있던 노드를 지울 필요가 없다. 모든 노드 삽입 메서드는 자동으로 기존에 있던 노드를 삭제 후에 새로운 곳으로 노드를 옮기기 때문이다.

5) cloneNode로 노드 복제하기

기존에 만들어진 노드를 복제하고, 복제한 노드의 내용을 수정하는 것 역시 가능하다. 기존에 노드를 생성하는 것을 함수로 만들어 활용할 수도 있지만, 만약 복제하려는 요소가 매우 크고 복잡한 경우에는 단순히 복제를 하는 방법이 더 빠르고 간단할 수 있다.

  • elem.cloneNode(true) : elem의 깊은 복제본을 만든다. 속성 전체와 후손 요소 전부 복사한다.
  • elem.cloneNode(false) : 후손 요소까지는 복사하지 않고 elem 자체만 복사한다.

6) DocumentFragment

DocumentFragment는 특별한 DOM 노드 타입으로, 여러 노드로 구성된 그룹을 감싸 다른 곳으로 전달하게 해주는 래퍼의 역할을 수행한다.

HTML의 태그는 중첩 구조로 계속 깊이가 깊어질 수 있는데 DocumentFragment를 사용해 감싸진 노드를 전달하면, 삽입 시점에서 DocumentFragment는 사라지고 전달된 노드만 오롯이 추가된다.

<ul id="ul"></ul>

<script>
function getListContent() {
  let fragment = new DocumentFragment();

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }

  return fragment;
}

ul.append(getListContent()); // (*)
</script>

getListContent() 함수가 실행되었을 때 만들어지는 HTML 문서의 구조는 아래와 같다.

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

DocumentFragment의 자체적인 기능은 사실 노드가 담긴 배열을 전개 연산자로 전달해 직접 반환하는 것으로도 구현이 가능하기 때문에 직접 사용하는 일은 흔치않다. 보통 template 요소 같이 DocumentFragment를 기반으로 하는 문법에서 사용되는 편이다.

React에서도 이와 유사한 역할을 하는 JSX 문법이 있다. React.Fragment 또는 단순히 <></>을 통해 이 같은 기능을 수행할 수 있는데 리액트에서는 deps가 깊어지는 것을 방지하기 위해 종종 사용되곤 한다.

7) 구식 삽입/삭제 메서드

하위 호환성을 유지하기 위해 남아있는 구식 조작 메서드도 있다. 요즘엔 앞서 언급한 메서드를 통해 조금 더 유연하게 DOM을 조작하기 때문에 이들은 잘 사용하지 않는다. 때문에 어떤 메서드가 있는지만 간단하게 짚고 넘어가자.

  • parentElem.appendChild(node) : parentElem의 마지막 자식으로 node를 추가

  • parentElem.insertBefore(node, nextSibling) : nodeparentElem안의 nextSibling 앞에 추가

  • parentElem.replaceChild(node, oldChild) : parentElem의 자식 노드 중 oldChildnode로 교체

  • parentElem.removeChild(node) : nodeparentElem의 자식 노드라는 가정하에 parentElem에서 node를 제거

해당 메서드들은 모두 그 기능을 수행하고 삽입 또는 삭제한 노드 자체를 반환한다. 그런데 이 반환된 값을 굳이 사용하는 경우가 없기 때문에 메서드를 그냥 실행만 하는 편이다.

8) document.write

document.write는 웹페이지에 무엇인가 더할 때 쓰이는 아주 오래된 메서드이다.

<p>페이지 어딘가...</p>
<script>
  document.write('<b>자바스크립트를 사용해 Hello 입력</b>');
</script>
<p></p>

document.write(html)을 호출하면 html이 페이지 그 자리에 즉시 추가된다. 문자열을 동적으로 만들어 사용할 수 있기 때문에 꽤나 유연한 동작을 가지고 있다.

하지만 document.writeDOM 객체도 없고 그 어떤 표준도 없던 머나먼 과거에 만들어진 메서드이다. 오늘날 표준에 정의되지 않았지만, 과거에 만들어진 웹 페이지의 하위 호환성을 위해 아직 살아남은 것 뿐이다. 때문에 근래에는 document.write를 사용해 작성된 코드를 만나기 어렵다.

그 이유 말고도 document.write는 치명적인 단점이 있는데, 바로 페이지를 불러오는 도중에만 작동한다는 점이다. 만약 페이지가 모두 로드를 마친 후에 document.write를 호출한다면 기존에 있던 문서 내용은 전부 사라지고 새롭게 내용을 쓰게 된다. 때문에 기존 내용에 요소를 추가만 하려는 경우에는 해당 메서드를 이용할 수 없다.

물론 이러한 특성을 이용할 수도 있다. 브라우저는 HTML을 파싱하는 도중에 document.write(HTML)을 만다게 되면 텍스트 형식의 HTML을 마치 원래 페이지에 있는 것처럼 해석한다. 중간에 DOM 조작을 하지 않기 때문에 그 속도가 매우 빠르다. 따라서 만약 엄청나게 많은 글자를 HTML에 동적으로 추가해야 하는데 아직 페이지를 불러오고 있는 중이고 속도가 중요한 상황에서는 document.write가 유용할 수 있다.

그렇지만 이러한 요구 상황은 일반적인 상황이 아닐뿐더러 보통 document.write가 쓰였다면 오래된 스크립트이기에 그럴 확률이 높다.

스타일과 클래스

요소에 스타일을 적용할 수 있는 방법은 크게 두 가지가 있다.

  1. CSS에 클래스를 만들고 (또는 id) 요소 자체에 속성을 추가
  2. <div style="...">처럼 프로퍼티를 style에 바로 기입

자바스크립트를 이용하면 클래스와 style 프로퍼티 둘 다 수정이 가능하다. 만약 두 방법 중 하나를 택해야 한다면 CSS 클래스를 수정하는 것을 더 우선시 하는 것이 좋다. style은 클래스를 다룰 수 없는 경우에 취급하는 편이 좋다.

예를 들어 아래와 같이 자바스크립트를 사용해 요소의 좌표를 동적으로 계산하고, 계산한 좌표를 직접 설정하고자 하는 경우에는 style을 사용하는 편이 낫다.

let top = /* 복잡한 계산식 */;
let left = /* 복잡한 계산식 */;

elem.style.left = left;
elem.style.top = top;

1) className과 classList

클ㄹ스 변경은 스크립트를 통해 자주하는 동작 중에 하나이다. 아주 오래전 자바스크립트엔 class와 같은 예약어가 객체의 프로퍼티가 될 수 없다는 제약이 있었는데, 그러한 이유로 className이라는 프로퍼티를 통해 클래스를 접근할 수 있었다. 지금은 이러한 제약이 사라졌음에도 불구하고 여전히 하위 호환성을 위해 className 프로퍼티를 사용한다.

비슷한 이유로 React에서도 JSX 내에서 class 대신 className을 사용한다.

elem.className은 HTML 속성 class에 대응한다.

<body class="main page">
  <script>
    alert(document.body.className); // main page
  </script>
</body>

이때 className에서 주의해야 할 점이있다. className에 무엇인가 대입하면 클래스 문자열 전체가 바뀐다. 따라서 클래스 전체를 바꾸는 것이 아니라 어떤 클래스명 하나만 추가한다거나 제거하는 경우에는 다른 프로퍼티를 사용하는 것이 더 간편하다.

elem.classList라는 프로퍼티를 사용하면 자체적으로 제공하는 add/toggle/remove 메서드 등을 통해 이 같은 조작을 간단하게 할 수 있다.

  • elem.classList.add("class") : class를 추가
  • elem.classList.remove("class") : class를 제거
  • elem.classList.toggle("class") : class가 존재하면 class를 제거, 없다면 추가
  • elem.classList.contains("class") : class 존재 여부에 따라 true/false 반환

또한 classList는 이터러블 객체를 반환하기 때문에 for...of를 통해 순회할 수 있는 특징이 있다.

만약 클래스 속성값 전체를 바꾸고 싶은 경우에는 classNames으로, 개별 클래스를 조작하고 싶은 경우에는 classList를 사용하면 된다.

2) 요소의 스타일

프로퍼티 elem.style은 속성 style에 쓰인 값과 대응되는 객체이다. elem.style.width='10px'style 속성값을 문자열 width='10px'로 설정한 것과 같다.

실제 CSS 스타일 속성과 달리 자바스크립트 또는 style 속성 내부에서는 여러 단어로 이루어진 스타일 속성을 모두 카멜 표기법으로 표현한다.

background-color  => elem.style.backgroundColor
z-index           => elem.style.zIndex
border-left-width => elem.style.borderLeftWidth

간혹 브라우저마다 CSS 스타일을 달리하는 경우가 있다. 이때는 스타일 속성에 -moz 또는 -webkit과 같은 접두어가 추가되는데 이러한 특정 브라우저 전용 프로퍼티 역시 모두 카멜 표기법으로 접근하거나 값을 쓸 수 있다.

button.style.MozBorderRadius = '5px';
button.style.WebkitBorderRadius = '5px';

3) style 프로퍼티 재지정

style 프로퍼티에 값을 할당했다가 시간이 지나 이를 제거해야할 경우가 종종 생길 수 있다. 예를 들어 어떤 요소 하나를 숨기기 위해 elem.style.display = "none"을 적용했다고 가정해보자. 해당 요소를 어느 시점에 다시 표시해야할 순간이 와서 되돌리고 싶다면 어떻게 해야할까?

자바스크립트 객체의 관점에서만 생각한다면 delete를 이용해 속성을 제거해야 한다고 생각할 수 있다. 그렇지만 delete elem.style.display 처럼 그 값을 제거하는 것이 아니라 elem.style.display = ""처럼 다시 빈 문자열을 할당해주는 것으로 원래 상태로 되돌릴 수 있다.

이처럼 style.display에 빈 문자열을 할당하면 브라우저는 마치 처음부터 style.display 프로퍼티가 없었던 것처럼 CSS 클래스와 브라우저 내장 스타일을 페이지에 적용한다.

개별 스타일 프로퍼티를 적용할 때는 보통 위와 같이 style.*를 사용한다. 그런데 사실 style은 객체이고 읽기 전용이기 때문에 복수의 옵션을 동시에 적용할 수 없다.

// 아래는 유효하지 않은 방식
div.style = "color: red; width: 100px"

만약 이렇게 복수의 옵션을 한번에 설정하고 싶다면 style.cssText 프로퍼티를 사용할 수 있다.

div.cssText=`color: red !important;
  width: 100px;
`;

style.cssText 프로퍼티를 사용하면 기존 스타일에 스타일을 추가하는 것이 아닌 전체를 교체하는 방식이기 때문에 이미 만들어진 요소에는 잘 적용하지 않는다. 그렇지만 새로운 요소를 만들고 스타일을 부여하는 상황에서는 기존 스타일이 없기 때문에 적용해봄직 하다.

또는 div.setAttribute('style', 'color: red...)과 같은 방식으로도 style.cssText과 동일한 기능을 할 수 있다.

4) 단위에 주의하기

자바스크립트를 사용해 스타일 값을 설정할 땐 단위를 항상 붙여주는 것에 주의해야 한다. 예를 들어 elem.style.top에 값을 설정할 때 10px이 아닌 10과 같이 숫자만 지정하는 경우엔 제대로 동작하지 않는다.

5) getComputedStyle을 통해 계산된 스타일 접근

style 프로퍼티로 값을 읽는 것은 오직 HTML 요소에 style 속성이 지정되어 그 값을 가져오는 것만 유효하다. 즉 style 프로퍼티를 통해서는 CSS 스타일링으로 지정된 종속값(CSS Cascade)을 다루지 못한다.

<head>
  <style> body { color: red; margin: 5px } </style>
</head>
<body>

  붉은 글씨
  <script>
    alert(document.body.style.color); // 빈 문자열
    alert(document.body.style.marginTop); // 빈 문자열
  </script>
</body>

위의 코드를 통해 CSS 스타일링을 통해 지정된 값은 style 프로퍼티로 가져오지 못하고 있는 것을 확인할 수 있다.

그렇다면 만약 현재 지정된 마진값보다 20px 만큼 더 큰 값을 부여하고 싶다면 어떻게 해야할까? 이 작업을 하기 위해서는 먼저 지정된 현재 마진값을 얻을 수 있어야 한다. 이때 getComputedStyle 메서드를 통해 값을 가져올 수 있다. 해당 메서드의 문법은 다음과 같다.

getComputedStyle(element, [pseudo]);
  • element : 값을 읽을 요소
  • pseudo : ::before같이 의사 요소가 필요한 경우 명시. 빈 문자열 또는 입력하지 않을 경우엔 요소 자체를 의미

getComputedStyle를 호출하면 elem.style과 같이 스타일 정보가 들어있는 객체가 반환되는데 여기엔 elem.style과는 달리 전체 CSS 클래스 정보도 함께 담기게 된다.

<head>
  <style> body { color: red; margin: 5px } </style>
</head>
<body>

  <script>
    let computedStyle = getComputedStyle(document.body);

    // 이제 마진과 색 정보를 얻을 수 있다.
    alert( computedStyle.marginTop ); // 5px
    alert( computedStyle.color ); // rgb(255, 0, 0)
  </script>

</body>

이때 getComputedStyle에는 프로퍼티 전체 이름이 필요하다. 즉 paddingLeft, marginTop, borderTopWidth와 같이 프로퍼티 이름 전체를 정확히 알고 있어야 한다. 그렇지 않은 경우에는 원하는 값을 얻을 수 없을 수도 있다. 예를 들어 paddingLeft에 값이 지정되어 있는데 getComputedStyle(elem).padding을 사용해 값을 얻으려고 하는 경우를 생각해보자.

이때엔 아무 값을 읽지 못할까 아니면 paddingLeft에 담긴 값이라도 가져올까? 이에 대한 표준은 아직 만들어진 것이 없다. 따라서 브라우저마다 상이한 결과를 나타낸다. 크롬 브라우저에서는 이 상황에서 paddingLeft의 값이라도 가져오지만 파이어폭스 브라우저에서는 빈 문자열을 출력한다.

CSS 속성과 관련된 두 가지 개념이 있다.

  • 계산값(Computed Style Value)
    • CSS 규칙과 상속이 모두 적용된 후의 값
    • height: 1rem 또는 font-size: 125% 등의 형태
  • 결정값(Resolved Style Value)
    • 요소에 최종적으로 적용되는 값
    • 상대값을 고정 단위를 사용하는 절대값으로 변환
    • height: 16px 또는 font-size: 12.5px 등의 형태

getComputedStyle 메서드는 계산값을 얻기 위해 만들어진 오래된 메서드이다. 그렇지만 계산값 보다는 결정값을 사용하는게 훨씬 유연하고 정확하기 때문에 표준이 개정되었다. 따라서 지금은 getComputedStyle를 호출하면 결정값을 반환한다.

getComputedStyle 메서드의 매개변수로 넘겨주는 의사 요소 중에 :visited CSS 의사 클래스 관련 스타일은 숨겨져 있다. 예를 들어 방문한 적이 있는 링크엔 :visited 속성을 통해 CSS 스타일링에서는 색을 입힐 수 있다. 그러나 getComputedStyle을 통해서는 해당 값에 접근할 수 없다. 이는 브라우저에 진입한 사용자가 해당 링크에 접근했는지 아닌지를 색상이 변화한 것에 따라 파악할 수 있기 때문에 이를 악용하려는 시도를 방지하기 위해 이러한 제약을 두고 있다.

References

  1. https://ko.javascript.info/document
profile
개발잘하고싶다

0개의 댓글