자바스크립트 문서 조작

woolee의 기록보관소·2022년 12월 17일
0

FE개념정리

목록 보기
28/35

브라우저 환경과 다양한 명세서

웹 브라우저 호스트 환경

자바스크립트는 원래 웹 브라우저에서 사용하려고 만든 언어이다.

자바스크립트가 돌아가는 플랫폼은 호스트(host)라고 불리며, 각 플랫폼들은 각각의 호스트 환경(host environment)을 갖는다.

호스트 환경은 플랫폼에 특정되는 객체와 함수를 제공한다. 웹브라우저는 웹페이지를 제어하기 위한 수단을 제공하고, Node.js는 서버 사이드 기능을 제공한다.

위 그림은 호스트 환경이 웹 브라우저일 때, 사용할 수 있는 기능들을 보여준다.

최상단에 있는 window는 루트 객체이다.

window 객체의 2가지 역할은 다음과 같다.
1. window 객체는 자바스크립트 코드의 전역 객체이다.
2. 브라우저 창(browser window)을 대변하고, 이를 제어할 수 있는 메서드를 제공한다.

문서 객체 모델 (DOM)

문서 객체 모델(Document Object Model, DOM) 웹페이지 내 모든 콘텐츠를 객체로 보여주며, 이 객체는 수정 가능하다.

document 객체는 페이지의 기본 진입점 역할을 한다.
관련 프로퍼티와 메서드는 DOM Living Standard에서 찾아볼 수 있다.

DOM이 브라우저만을 위한 모델은 아니다. 일부 스크립트에서도 DOM의 일부를 사용하기도 한다.

스타일링을 위해서는 CSS 객체 모델(CSS Object Model, CSSOM)가 존재한다. 물론 CSS 규칙은 정적이므로 CSSOM을 자주 쓰지는 않으며, 자바스크립트를 사용해서 CSS 규칙을 제어하는 경우에만 CSSOM을 사용한다.

브라우저 객체 모델 (BOM)

브라우저 객체 모델(Browser Object Model, BOM)은 문서 이외에 모든 것들을 제어하기 위해 브라우저(호스트환경)이 제공하는 추가 객체를 말한다.

navigator 객체는 브라우저와 운영체제에 대한 정보를 제공한다.
location 객체는 현재 URL을 읽을 수 있게 해주고, 새로운 URL로 변경(redirect)할 수 있게 해준다.

alert, confirm, prompt 역시 BOM의 일부이다. 문서와 직접 연결되어 있지는 않지만, 사용자와 브라우저 간 소통을 도와주는 메서드이다.

BOM 명세가 따로 있지는 않고 HTML Living Standard에서 찾을 수 있다.

DOM 트리

HTML을 지탱하는 건 태그(tag)이며, 모든 HTML 태그는 객체이다.
태그 하나가 감싸고 있는 자식 태그는 중첩 태그(nested tag)라고 불리며, 태그 내 문자(text) 역시 객체이다.

document.body는 body 태그를 객체로 나타낸 것이다.
(참고로 document.documentElement는 html 객체, window는 window 객체, document.head는 head 객체이다)

HTML의 모든 것은 DOM을 구성한다.

태그는 요소 노드이고, 트리 구조를 형성한다. <html>이 루트 노드가 된다. 요소 내 문자는 텍스트(text) 노드이다. 주석도 주석 노드이다.

HTML 문서 최상단에 위치하는 <!DOCTYPE...> 지시자 또한 DOM 노드가 된다.

문서 전체를 나타내는 document 객체 또한 DOM 노드이다.
노드 타입은 12가지이지만, 실무에선 4가지를 주로 다룬다.

  1. DOM의 진입점이 되는 문서(document) 노드
  2. HTML 태그에서 생성되며, DOM 트리를 구성하는 블록인 요소 노드
  3. 텍스트 노드
  4. 주석 노드

자동 교정

기형적인 HTML을 만나면 브라우저는 DOM 생성과정에서 자동으로 교정한다.

예를 들어, DOM 명세서에서는 테이블에 반드시 <tbody>가 있어야 한다고 하지만, 종종 빼먹고 작성하기도 한다. 이때 브라우저는 자동으로 <tbody>를 만들어준다.

DOM 구조 직접 보기

브라우저 개발자 도구를 열어 DOM을 탐색할 수 있다.

개발자 도구를 열고 Elements 패널에서 확인할 수 있다.
물론 개발자 도구로 DOM을 탐색해보면 생략된 노드들이 존재한다.

Elements 패널의 여러 하위 패널들이 존재한다.

  • Styles : 현재 선택한 요소에 적용된 CSS 규칙을 일률적으로 보여준다. Styles에서 실시간으로 수정할 수도 있다.
  • Computed : 현재 선택한 요소에 적용된 CSS 규칙을 프로퍼티 기준으로 보여준다. 상속을 통해 적용된 규칙도 볼 수 있다.
  • Event Listeners : DOM 요소에 붙은 이벤트 리스너들을 볼 수 있다.
    ...

기타 패널과 기능들은 Chrome 개발자 도구에서 확인할 수 있다.

DOM 탐색하기

DOM에 수행되는 모든 연산은 document 객체에서 시작한다. document 객체는 DOM에 접근하기 위한 진입점이다.

트리 상단의 documentElement와 body

<html> = document.documentElement
<body> = document.body
<head> = document.head

document.body는 null일 수 있다. 스크립트를 읽는 도중에는 스크립트에서 해당 요소에 접근할 수 없으므로 null을 반환한다.
DOM에서 null은 '(해당 노드가) 존재하지 않음'을 의미한다.

childNodes, firstChild, lastChild로 자식 노드 탐색하기

자식 노드(child node, children)는 바로 아래의 자식 요소를 의미한다.
후손 노드(descendants)는 중첩 관계에 있는 모든 요소를 의미한다.

childNodes 컬렉션은 텍스트 노드를 포함한 모든 자식 노드를 담고 있다.
firstChildlastChild 프로퍼티를 사용하면 첫번째, 마지막 자식노드에 빠르게 접근할 수 있다.
자식노드를 갖고 있는지 확인하려면 elem.hasChildNodes()를 사용하면 된다.

DOM 컬렉션

위에서 본 childNodes 컬렉션은 배열이 아니라 반복 가능한(iterable) 유사 배열 객체인 컬렉션(collection)이다.

childNodes은 컬렉션이므로

  1. for..of를 사용할 수 있다. (이터러블이므로 Symbol.iterator 프로퍼티가 구현되어 있다)
  2. 배열 메서드를 사용할 수 없다.
    배열 메서드를 사용하고 싶다면, Array.from으로 감싸거나 Array.from(document.body.childNodes), 전개 연산자를 사용할 수 있다 [...document.body.childNodes]

DOM 컬렉션 특징

  1. DOM 컬렉션은 읽는 것만 가능하다. DOM을 변경하려면 다른 메서드를 사용해야 한다.
  2. DOM 컬렉션은 DOM의 현재 상태를 반영한다. 실시간으로 변동될 수 있다.
  3. DOM 컬렉션에서 for..in 반복문을 사용하면 추가 프로퍼티까지 전부 순회하므로 사용하지 않아야 한다.

형제와 부모 노드

같은 부모를 가진 노드들을 형제(sibling) 노드라고 부른다.

다음 형제 노드에 대한 정보는 nextSibling,
이전 형제 노드에 대한 정보는 previousSibling 프로퍼티에서 찾을 수 있다.

부모 노드에 대한 정보는 parentNode 프로퍼티를 이용해 참조할 수 있다.

요소 간 이동

위에서 설명한 childNodes 컬렉션은 모든 종류의 노드를 참조한다. 하지만 텍스트 노드, 주석 노드 같은 녀석들은 불필요하다.

실무에서는 웹 페이지를 구성하는 태그의 분신인 요소 노드를 조작하는 작업이 대다수이다. 실무에서는 주로 아래와 같은 메서드를 사용해 탐색한다.

parentElement 프로퍼티는 부모 요소 노드를 반환한다. 반면 parentNode 프로퍼티는 종류에 상관없는 부모 노드를 반환한다.

document.documentElement.parentElement // null
document.documentElement.parentNode // document

document.documentElement의 부모는 document이지만, document는 요소 노드가 아니다.

요약하자면 탐색 프로퍼티는 크게 2개의 집합으로 나뉜다.

  1. 모든 노드에 적용 가능한 parentNode, childNodes, firstChild, lastChild, previousSibling, nextSibling
  2. 요소 노드에만 적용 가능한 parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling

위와 같은 DOM 탐색의 기본 프로퍼티에 더해, 일부 DOM 요소는 추가 프로퍼티로 DOM 탐색을 수월하게 한다.
예를 들어, <table> 요소의 여러 추가 프로퍼티들(tabular data).
HTML form의 추가 프로퍼티들.

getElement, querySelector로 요소 검색하기

요소들이 자식/부모 관계가 아닌 경우에 접근하려면?
상대 위치를 사용하지 않고 웹 페이지 내 원하는 요소 노드에 접근하게 할 수 있는 메서드들이다.

document.getElementById

document.getElementById(id)

이때 id는 중복되면 안 된다.
getElementByIddocument 객체를 대상으로 해당 id를 가진 요소 노드를 찾아주므로 문서 노드가 아닌 다른 노드(anyNode.getElementById)에서 호출하면 안 된다.

querySelectorAll

elem.querySelectorAll(css)는 elem의 자식 요소 중 주어진 CSS 선택자(가상 클래스도 가능하다)에 대응하는 모든 요소를 반환한다.

querySelector

elem.querySelector(css)는 주어진 CSS 선택자에 대응하는 요소 중 첫번째 요소를 반환한다.

elem.querySelectorAll(css)[0]과 똑같지만 elem.querySelector(css)가 첫번째 요소를 검색하고 멈추는 반면,
elem.querySelectorAll(css)[0]는 모든 요소를 검색한 후에 첫번째 요소를 반환하므로 elem.querySelector(css)가 훨씬 빠르다.

matches

document.getElementById, querySelectorAll, querySelector가 DOM 검색에 쓰인다면, elem.matches(css)는 elem 요소가 주어진 CSS 선택자와 일치하는지 여부(true/false)를 반환한다.

요소가 담겨 있는 배열을 순회해 원하는 요소만 걸러낼 때 사용한다.

closest

부모 요소, 부모 요소의 부모 요소 등 DOM 트리에서 특정 요소의 상위에 있는 요소들은 조상(ancestor) 요소라고 한다.

elem.closest(css)는 elem 자기 자신을 포함해 CSS 선택자와 일치하는 가장 가까운 조상 요소를 반환한다.

closet 메서드는 해당 요소부터 시작해 DOM 트리를 한 단계씩 거슬러 올라가면서 원하는 요소를 찾는다. CSS 선택자와 일치하는 요소를 찾으면 검색을 멈추고 해당 요소를 반환한다.

이벤트 위임 시에 closet을 유용하게 사용할 수 있다.

getElementsBy*

태그나 클래스 등을 이용해 원하는 노드를 찾아주는 메서드지만 자주 사용하지 않는다.

elem.getElementsByTagName(tag), elem.getElementsByTagName(*)

elem.getElementsByClassName(className)

document.getElementsByName(name) name은 속성값을 말한다.

살아있는 컬렉션

getElementsBy로 시작하는 모든 메서드는 살아있는 컬렉션을 반환한다. 문서에 변경이 있을 때마다 컬렉션이 '자동 갱신'되어 최신 상태를 유지한다.

반면 querySelectorAll은 정적인 컬렉션을 반환한다.

주요 노드 프로퍼티

DOM 노드 클래스

DOM 노드는 종류에 따라 각각 다른 프로퍼티를 지원한다.
다만 모든 DOM 노드는 공통 조상으로부터 만들어지므로 노드 종류는 다르지만, 모든 DOM 노드는 공통된 프로퍼티와 메서드를 지원한다.

DOM 노드는 종류에 따라 대응하는 내장 클래스가 다르다.

계층 꼭대기에는 EventTarget이 있는데, Node는 EventTarget을, 다른 DOM 노드들은 Node 클래스를 상속받는다.

EventTarget은 루트에 있는 추상 클래스로, 이 클래스에 대응하는 객체는 실제로 만들어지지 않는다. 모든 DOM 노드의 베이스에 EventTarget이 있으므로 DOM 노드에서 '이벤트'를 사용할 수 있다.
즉, 이벤트 관련 기능을 제공하는 클래스이다.

Node 또한 추상 클래스이다. DOM 노드의 베이스 역할이다. getter 역할을 하는 parentNode, nextSibling, childNodes 등의 주요 트리 탐색 기능을 제공한다. Node 클래스의 객체는 절대 생성되지 않지만 Node 클래스를 상속받는 여러 클래스들(Text, Element, Comment)이 존재한다.
즉, 공통 DOM 노드 프로퍼티를 제공하는 클래스이다.

Element는 DOM 요소를 위한 베이스 클래스이다. nextElementSibling, children 이나 getElementsByTagName, querySelector와 같은 요소 전용 탐색을 도와주는 프로퍼티나 메서드가 이를 기반으로 한다. 브라우저는 HTML뿐만 아니라 XML, SVG도 지원하는데 Element 클래스는 이와 관련된 SVGElement, XMLElement, HTMLElement 클래스의 베이스 역할을 합니다
즉, 요소 노드 메서드를 제공하는 클래스이다.

HTMLElement는 HTML 요소 노드의 베이스 역할을 하는 클래스이다. HTMLInputElement, HTMLBodyElement, HTMLAnchorElement와 같은 클래스들은 실제 HTML 요소에 대응하고 HTMLElement를 상속받는다.
즉, HTML 요소 메서드와 getter, setter를 제공하는 클래스이다.

객체는 기본적으로 constructor 프로퍼티를 갖는다. 이를 사용해서 DOM 노드 클래스의 이름을 확인해볼 수 있다.

document.body.constructor.name // HTMLBodyElement
document.body // [object HTMLBodyElement]

혹은 instanceof를 사용해서 상속 여부를 확인할 수도 있다.

document.body instanceof HTMLBodyElement // true
document.body instanceof HTMLElement // true
document.body instanceof Element // true
document.body instanceof Node // true
document.body instanceof EventTarget // true

DOM 노드는 프로토타입을 기반으로 상속 관계를 갖는 일반 자바스크립트 객체이다. console.dir(elem)로 이런 관계를 쉽게 확인해볼 수 있다.

elem이 DOM 요소일 때,
console.log(elem)은 요소의 DOM 트리를 출력한다
console.dir(elem)는 요소를 DOM 객체처럼 취급해서 출력한다.

console.log(document.body)
console.dir(document.body

명세서에서는 DOM 클래스를 IDL(Interface description language)로 설명한다.

// HTMLInputElement 정의 시작
// 콜론(:)은 HTMLInputElement가 HTMLElement로 부터 상속되었다는 것을 의미합니다.
interface HTMLInputElement: HTMLElement {
  // <input> 요소와 관련된 프로퍼티와 메서드가 나열되기 시작합니다.

  // 'DOMString'은 프로퍼티 값이 문자열이라는 것을 의미합니다.
  attribute DOMString accept;
  attribute DOMString alt;
  attribute DOMString autocomplete;
  attribute DOMString value;

  // 불린 값(true/false)을 가지는 프로퍼티
  attribute boolean autofocus;
  ...
  // 'void'는 메서드의 리턴값이 없음을 의미합니다.
  void select();
  ...
}

‘nodeType’ 프로퍼티

nodeType 프로퍼티는 DOM 노드의 타입을 알아낼 때 사용하는 구식 프로퍼티이다. nodeType 프로퍼티는 타입을 확인하기만 할 수 있고, 수정은 불가능하다.

elem.nodeType == 1 // 요소 노드 
elem.nodeType == 3 // 텍스트 노드
elem.nodeType == 9 // 문서 객체

모던 자바스크립트에서는 노드 타입을 확인할 때,
instanceof나 클래스 기반으로 테스트를 한다.

nodeName과 tagName으로 태그 이름 확인하기

nodeName이나 tagName 프로퍼티를 사용하면 DOM 노드의 태그 이름을 확인할 수 있다.

tagName 프로퍼티는 요소 노드에만 존재한다.
nodeName 프로퍼티는 모든 노드에 존재한다.

document.body.nodeName // BODY
document.body.tagName // BODY

태그 이름은 XML 모드를 제외하고 항상 대문자이다.

  • 브라우저에서 HTML과 XML을 처리하는 모드가 다르다. 웹페이지는 대개 HTML 모드로 처리된다. 헤더가 Content-Type: application/xml+xhtml인 XML 문서를 받으면 XML 모드로 문서를 처리한다. HTML 모드에선 tagName과 nodeName이 모두 대문자로 변경된다. <body><BoDy>든 BODY가 된다. XML 모드에선 케이스가 ‘그대로’ 유지된다. 물론 XML 모드는 요즘엔 거의 사용되지 않는다.

innerHTML, outerHTML

innerHTML 프로퍼티를 사용하면 요소 안의 HTML을 문자열 형태로 받아올 수 있고, 요소 안 HTML을 수정할 수도 있다.

‘innerHTML+=’elem.innerHTML+="추가 html"을 의미하지만, 기존 내용을 완전히 삭제한 후에 밑바닥부터 다시 내용을 쓰므로 이미지 등의 리소스도 전부 재로딩된다.

outerHTML 프로퍼티에는 요소 전체 HTML이 담겨 있다. innerHTML과 달리 outerHTML을 사용해서 HTML을 쓸 땐 요소 자체가 바뀌지 않는다. 대신 outerHTML은 DOM 안의 요소를 교체한다.

nodeValue/data로 텍스트 노드 내용 조작하기

innerHTML 프로퍼티는 요소 노드에만 사용할 수 있다.

텍스트 노드와 같은 다른 타입 노드에는 innerHTML와 유사한 역할을 하는 프로퍼티인 nodeValue와 data를 사용해야 한다.

<body>
  텍스트입니다
  <!-- 주석입니다 -->
  <script>
    let text = document.body.firstChild
    console.log(text.data) // 텍스트입니다

    let comment = text.nextSibling
    console.log(comment.data) // 주석입니다
  </script>
</body>

textContent로 순수한 텍스트만

textContent를 사용하면 요소 내의 텍스트에만 접근할 수 있다.

<div id="text">
  <h1>텍스트</h1>
  <p>입니다</p>
</div>

<script>
  console.log(news.textContent) // 텍스트 입니다
</script>

hidden 프로퍼티

hidden 속성과 hidden 프로퍼티는 요소를 보여줄지 말지 지정할 때 사용한다. HTML 안에서 쓸 수도 있고 자바스크립트에서도 쓸 수 있다.

hidden은 기술적으로 style="display: none;"와 동일하다.

기타 프로퍼티

이외에도 DOM 요소엔 다양한 프로퍼티가 존재한다. 대표적으로 value, href, id 등이 있다.

특정 클래스에서 지원하는 프로퍼티를 알고 싶다면 명세서를 읽으면 되지만, 명세서를 읽지 않더라도 console.dir(elem)을 입력하면 해당 요소에서 지원하는 프로퍼티 목록을 쉽게 확인할 수 있다. 혹은 개발자 도구의 Elements 패널의 하위 패널 중 Properties를 선택해도 동일한 목록을 확인할 수 있다.

속성과 프로퍼티

브라우저는 웹페이지를 만나면 HTML을 읽어서(parsing) DOM 객체를 생성하낟. 요소 노드에서 대부분의 표준 HTML 속성(attribute)은 DOM 객체의 프로퍼티(property)가 된다.

하지만 속성과 프로퍼티는 항상 일대일로 매핑되지 않는다.

DOM 프로퍼티

DOM 내장 프로퍼티의 종류는 많지만, 자신만의 프로퍼티를 만들 수도 있다.

DOM 프로퍼티와 메서드는 일반 자바스크립트 객체처럼 다룰 수 있다. (어떤 값이든 가질 수 있고, 대소문자를 구분한다)

document.body.mydata = {
  name: wejaan,
  color: purple, 
}
console.log(document.body.mydata.name) // wejaan
console.log(document.body.mydata.color) // purple

document.body.myTagName = function() {
  console.log(this.tagName)
}
document.body.myTagName() // BODY 

Element.prototype 과 같은 내장 프로토타입을 수정해 모든 요소 노드에서 내가 만든 메서드를 사용하게 할 수도 있다.

Element.prototype.myTagName = function() {
  console.log(this.tagName)
}
document.documentElement.myTagName() // HTML
document.body.myTagName() // BODY

HTML 속성

HTML에서 태그는 여러 개의 속성을 가질 수 있다.

브라우저는 HTML을 파싱해 DOM 객체를 만들 때, HTML 표준 속성을 인식해서 DOM 프로퍼티를 만든다. 이때 표준이 아닌 속성을 만나면 달라진다.
표준이 아닌 속성을 만나면 프로퍼티로 전환되지 않으므로 undefined를 반환한다.

참고로 한 요소에서 표준 속성이더라도 다른 요소에선 표준이 아닐 수 있다.
예를 들어 input 요소의 type은 HTMLInputElement에선 표준이지만 HTMLBodyElement에선 표준이 아니다.

비표준 속성에 접근하려면 다음의 메서드를 사용해야 한다.

elem.hasAttribute(name) – 속성 존재 여부 확인
elem.getAttribute(name) – 속성값을 가져옴
elem.setAttribute(name, value) – 속성값을 변경함
elem.removeAttribute(name) – 속성값을 지움
elem.attributes - 모든 속성값

<body id="test" something="non-standard">
  <script>
    console.log(document.body.id); // test
    // 비표준 속성은 프로퍼티로 전환되지 않습니다.
    console.log(document.body.something); // undefined
	console.log(document.body.getAttribute('something')) // none-standard 
  </script>
</body>

HTML 속성은 대소문자를 가리지 않는다(id와 ID는 동일한 값이다). 값은 항상 문자열이다.

HTML 속성은 아래처럼 다룰 수 있다. elem.attributes가 반환하는 컬렉션은 iterable하다. 컬렉션에 담긴 각 객채의 name, value 프로퍼티를 사용하면 속성 전체에 접근할 수 있다.

<body>
  <div id="elem" about="Elephant"></div>

  <script>
    alert( elem.getAttribute('About') ); // (1) 'Elephant', 속성 읽기

    elem.setAttribute('Test', 123); // (2) 속성 추가하기

    alert( elem.outerHTML ); // (3) 추가된 속성 확인하기

    for (let attr of elem.attributes) { // (4) 속성 전체 나열하기
      alert( `${attr.name} = ${attr.value}` );
    }
  </script>
</body>

프로퍼티 - 속성 동기화

표준 속성이 변하면 대응하는 프로퍼티는 자동으로 갱신된다. 몇몇 경우를 제외하고는 프로퍼티가 변하면 속성 역시 마찬가지로 갱신된다.

예를 들어 input 요소의 id 속성과 프로퍼티는 양방향 갱신이 되지만, value는 양방향 갱신에 해당되지 않는다.

갱신이 안 된다는 건, 원본값을 유지할 수 있다는 의미이므로 유용할 수 있다.

<input>

<script>
  let input = document.querySelector('input');

  // 속성 추가 => 프로퍼티 갱신
  input.setAttribute('id', 'id');
  alert(input.id); // id (갱신)

  // 프로퍼티 변경 => 속성 갱신
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId (갱신)
  
  // 속성 추가 => 프로퍼티 갱신
  input.setAttribute('value', 'text');
  alert(input.value); // text (갱신)

  // 프로퍼티를 변경해도 속성이 갱신되지 않음
  input.value = 'newValue';
  alert(input.getAttribute('value')); // text (갱신 안됨!)
</script>

DOM 프로퍼티 값의 타입

DOM 프로퍼티는 항상 문자열이 아니다.

예를 들어
체크박스에 사용되는 input.checked 프로퍼티는 boolean 값을 갖는다. style 속성의 경우 문자열이지만, style 프로퍼티는 객체이다.

물론 그럼에도 대부분 프로퍼티의 값은 문자열이다.
드물긴 하지만 DOM 프로퍼티가 문자열임에도 속성값과 다른 경우도 있다. 예를 들어 href 속성은 상대 URL이나 #hash 이더라도 href DOM 프로퍼티엔 항상 URL 전체가 저장되는 경우가 있다.

HTML 내에 명시된 href 속성의 값을 정확하게 얻고 싶다면 getAttribute를 사용하면 된다.

비표준 속성, dataset

HTML을 작성할 때 대부분 표준 속성을 사용하지만 비표준 속성을 사용할 수도 있다. 비표준 속성은 사용자가 직접 데이터를 HTML에서 자바스크립트로 넘기고 싶거나 자바스크립트를 이용해 조작할 HTML 요소를 표시하기 위해 사용할 수 있다.

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

<script>
  // 표시한 요소를 찾고, 그 자리에 원하는 정보를 보여주는 코드
  let user = {
    name: "Pete",
    age: 25
  };

  for(let div of document.querySelectorAll('[show-info]')) {
    // 원하는 정보를 필드 값에 입력해 줌
    let field = div.getAttribute('show-info');
    div.innerHTML = user[field]; // Pete가 'name'에, 25가 'age'에 삽입됨
  }
</script>

비표준 속성은 요소에 스타일을 적용하기 위해 사용되기도 한다. 클래스를 사용하는 것보다 커스텀 속성을 사용하는 게 다루기 편리하기 때문이다.

<style>
  /* 스타일이 커스텀 속성 'order-state'에 따라 변합니다. */
  .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>

<script>
  // 새 클래스를 추가하거나 지우는 것보다 더 쉽게 상태(state)를 바꿀 수 있습니다
  div.setAttribute('order-state', 'canceled');
</script>

이렇게 비표준 속성을 사용해 코드를 작성했는데 나중에 그 비표준 속성이 표준으로 등록될 수도 있다. HTML은 개발자들의 요구에 맞게 계속해서 발전되기 때문이다.

이를 방지하기 위해 data-*를 사용한다.

커스텀 속성 name 앞에 data-를 붙여서 사용한다.

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

<script>
  // 읽기
  alert(order.dataset.orderState); // new

  // 수정하기
  order.dataset.orderState = "pending"; // (*)
</script>

data-* 속성은 커스텀 데이터를 안전하고 유효하게 전달해준다.
data-* 속성을 사용하면 읽기 뿐만 아니라 수정도 가능하다.

속성과 프로퍼티 비교

속성은 HTML 안에서 쓰인다.
프로퍼티는 DOM 객체 안에서 쓰인다.

(프로퍼티의 타입과 이름)
프로퍼티는 모든 타입이 가능하고, 각 표준 프로퍼티의 타입은 명세서에 설명되어 있다. 이름은 대소문자가 구분된다.

(속성의 타입과 이름)
속성은 문자열 타입이다. 속성의 이름은 대소문자가 구분되지 않는다.

문서 수정하기

요소 생성하기

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

  1. 태그 이름을 사용해 새로운 요소 노드를 만든다.
    document.createElement(tag)
  2. 주어진 텍스트를 사용해 새로운 텍스트 노드를 만든다.
    document.createTextNode(text)

태그 이름으로 요소를 만들고, 만든 요소에 클래스를 넣고, 내용을 채워넣으면 요소를 만들 수 있다.

let div = document.createElement('div');
div.className = "active";
div.innerHTML = "<div class="inner"> .inner는 자식 요소입니다.</div>";

이렇게 3단계를 거쳐 요소를 만들 수 있지만, 이 요소는 아직 div 라는 이름을 가진 변수에 불과하므로 페이지에 나타나지 않는다.

삽입 메서드

생성한 요소를 페이지에 나타나게 하려면, document 내 어딘가에 생성한 요소를 넣어줘야 한다.

자바스크립트에서 지원하는 노드 삽입 메서드는 다음과 같다.

node.append(노드나 문자열) – 노드나 문자열을 node 끝에 삽입합니다.
node.prepend(노드나 문자열) – 노드나 문자열을 node 맨 앞에 삽입합니다.
node.before(노드나 문자열) – 노드나 문자열을 node 이전에 삽입합니다.
node.after(노드나 문자열) – 노드나 문자열을 node 다음에 삽입합니다.
node.replaceWith(노드나 문자열) – node를 새로운 노드나 문자열로 대체합니다.

노드가 아니라 문자열을 인자로 넘기면,
HTML이 아닌 문자열 형태 그대로 삽입된다.

문자열 형태의 HTML 그 자체를 삽입하려면?

insertAdjacentHTML/Text/Element

문자열 형태의 HTML 그 자체를 삽입하려면,
elem.insertAdjacentHTML(where, html)를 사용하면 된다.

첫번째 매개변수 where에는 elem을 기준으로 하는 상대 위치가 들어간다.

beforebegin' – elem 바로 앞에 html을 삽입합니다.
'afterbegin' – elem의 첫 번째 자식 요소 바로 앞에 html을 삽입합니다.
'beforeend' – elem의 마지막 자식 요소 바로 다음에 html을 삽입합니다.
'afterend' – elem 바로 다음에 html을 삽입합니다.

elem.insertAdjacentHTML(where, html)은 2가지 형제 메서드를 갖는다.

  1. elem.insertAdjacentText(where, text)는 문법은 insertAdjacentHTML과 같지만, HTML 대신 text를 '문자 그대로' 삽입한다.
  2. elem.insertAdjacentElement(where, elem)도 문법은 동일하지만, 요소를 삽입한다.

노드 삭제하기

node.remove()를 사용하면 노드를 삭제할 수 있다.

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

cloneNode로 노드 복제하기

기존에 있던 노드를 복제할 수 있다.

elem.cloneNode(true)을 호출하면 elem의 ‘깊은’ 복제본이 만들어진다. 속성 전부와 자손 요소 전부가 복사됩니다. elem.cloneNode(false)을 호출하면 후손 노드 복사 없이 elem만 복제된다.

DocumentFragment

DocumentFragment는 특별한 DOM 노드 타입으로, 여러 노드로 구성된 그룹을 감싸서 다른 곳으로 전달해주는 wrapper처럼 동작한다.

구식 삽입·삭제 메서드

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

parentElem.insertBefore(node, nextSibling)
node를 parentElem안의 nextSibling앞에 추가한다.

parentElem.replaceChild(node, oldChild)
parentElem의 자식 노드 중 oldChild를 node로 교체한다.

parentElem.removeChild(node)
node가 parentElem의 자식 노드라는 가정하에 parentElem에서 node를 삭제한다.

document.write(html)는 웹페이지에 뭔가를 더할 때 쓰는 구식 메서드이다. 호출하는 순간 그 자리에 즉시 삽입되므로 유연하지만,
document.write(html)는 페이지를 불러오는 도중에만 동작한다.

스타일과 클래스

요소에 스타일을 적용할 수 있는 방법은 2가지이다.
1. CSS에 클래스를 만들고, 요소에 클래스를 추가하거나 <div class="...">
2. 프로퍼티를 style에 바로 작성하거나 <div style="...">

보통은 1번 방법을 우선해야 한다.

className과 classList

elem.className에 무언가를 대입하면, 클래스 문자열 전체가 바뀐다.

속성값 전체를 바꾸는 게 아니라 클래스 일부를 추가하거나 제거하려면 elem.classList를 사용한다.

elem.classList에는 클래스 하나만 조작하게 해주는 메서드(add/remove/toggle)가 존재한다.

elem.classList.add/remove("class") – class를 추가하거나 제거
elem.classList.toggle("class") – class가 존재할 경우 class를 제거하고, 그렇지 않은 경우엔 추가
elem.classList.contains("class") – class 존재 여부에 따라 true/false를 반환

classList는 이터러블 객체이므로 for..of를 사용해 클래스를 나열할 수 있다.

요소의 스타일

프로퍼티 elem.style는 속성 "style"에 쓰인 값에 대응되는 객체이다.

여러 단어를 이어서 만든 프로퍼티는 카멜 표기법을 따라야 한다.

-webkit-, -moz- 등 브라우저 관련 접두사가 붙은 경우 역시 카멜 표기법을 따른다.

button.style.WebkitBorderRadius = '5px';

style 프로퍼티 재지정하기

style 프로퍼티에 값을 할당했다가 시간이 지나 이를 제거해야 할 때가 있을 수 있다. 이때는 프로퍼티를 별도로 삭제하는 대신, elem.style.display = "" 과 같이 빈 문자열을 할당해주어야 한다.

style.cssText로 완전히 다시 쓰기

개별 스타일 프로퍼티를 적용할 때는 위에서처럼 style.*를 사용한다. 하지만 style은 객체이고 읽기 전용이므로 여러 개를 적용할 수 없다.

여러 개를 적용하려면,style.cssText를 사용해야 한다.

style.cssText를 사용하면 기존 스타일에 스타일을 추가하는 게 아니라 전체를 교체해버리기 때문에 잘 쓰이지 않는다. 잘 사용하고 있는 스타일이 실수로 지워진다는 위험이 있다. 그렇지만 요소를 새로 만들고, 여기에 스타일을 적용할 때는 기존 스타일이 없기 때문에 style.cssText를 사용할 수 있다.

div.setAttribute('style', 'color: red...')를 사용해 속성을 설정해도 style.cssText과 같은 효과를 볼 수 있다.

참고로 자바스크립트를 사용해 스타일 값을 지정할 때는 단위를 반드시 붙여줘야 한다.

getComputedStyle로 계산된 스타일 얻기

style 프로퍼티는 "style" 속성의 값을 읽을 때만 사용할 수 있다. style 프로퍼티만으론 CSS 종속(CSS cascade) 값을 다루지 못한다.

이때는 getComputedStyle 메서드를 사용해야 한다.

getComputedStyle(element, [pseudo])
첫번째 인자 element는 값을 읽을 요소가 들어가고
두번째 인자 [pseudo]는 의사 요소가 필요한 경우에만 넘겨준다.

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

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

    let computedStyle = getComputedStyle(document.body);

    // 이제 마진과 색 정보를 얻을 수 있습니다.

    alert( computedStyle.marginTop ); // 5px
    alert( computedStyle.color ); // rgb(255, 0, 0)
  </script>

</body>

CSS에는 속성과 관련된 2가지 개념이 있다.
1. 계산 값(computed style value) – CSS 규칙과 CSS 상속이 모두 적용된 후의 값을 의미합니다. 값의 형태는 height:1em나 font-size:125% 같이 생겼다.
2. 결정 값(resolved style value) – 요소에 최종적으로 적용되는 값을 의미합니다. 계산 값에서 사용한 1em나 125%은 상대 단위를 사용하는 상댓값인데, 브라우저는 계산 값을 받아 단위를 전환해 height:20px나 font-size:16px같이 고정 단위를 사용하는 값(절댓값)으로 값을 변환합니다. 기하 관련 프로퍼티의 결정 값에는 width:50.5px같이 소수점 단위가 있을 수 있습니다.

getComputedStyle은 계산 값을 얻기 위해서 만들어진 아주 오래된 메서드지만, 계산 값보다는 결정 값을 사용하는 게 훨씬 편리하기 때문에 표준이 개정되었다.

따라서 지금은 getComputedStyle을 호출하면 프로퍼티의 결정 값이 반환된다. 기하 프로퍼티의 경우 주로 px가 단위로 사용된다.

getComputedStyle을 사용할 때는 paddingLeft, marginTop, borderTopWidth같이 프로퍼티 이름 전체를 정확히 알고 있어야 한다. 그렇지 않으면 원하는 값을 얻을 수 없는 경우가 생긴다.

방문한 적이 있는 링크엔 :visited라는 CSS 의사 클래스를 사용해 색을 입힐 수 있다. 그런데 getComputedStyle을 사용해서 이 색을 얻을 수 없다.

참고

브라우저 환경과 다양한 명세서 - 모던 JavaScript 튜토리얼
DOM 트리 - 모던 JavaScript 튜토리얼
DOM 탐색하기 - 모던 JavaScript 튜토리얼
getElement*, querySelector*로 요소 검색하기 - 모던 JavaScript 튜토리얼
주요 노드 프로퍼티 - 모던 JavaScript 튜토리얼
속성과 프로퍼티 - 모던 JavaScript 튜토리얼
문서 수정하기 - 모던 JavaScript 튜토리얼
스타일과 클래스 - 모던 JavaScript 튜토리얼

profile
https://medium.com/@wooleejaan

0개의 댓글