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

KG·2021년 6월 10일
1

모던JS

목록 보기
27/47
post-thumbnail

Intro

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

브라우저 환경과 다양한 객체 모델

자바스크립트는 본래 웹 브라우저에서 사용하기 위해 만들어진 언어다. 이후 여러 진화를 거쳐 다양한 사용처와 플랫폼을 지원하는 언어로 탈바꿈을 하였지만 주 사용처는 브라우저를 목표로 출발했다.

자바스크립트는 이후 브라우저 외에도 다양한 플랫폼을 지원하는 방향으로 발전하게 되었고, 각각의 플랫폼은 호스트(Host)라고 불리게 되었다.

각 호스트 환경은 해당 플랫폼에 특정되는 기능을 제공하는데, 자바스크립트 코어(ECMAScript)에 더하여 플랫폼 특정 객체와 함수를 지원한다. 예를 들어 브라우저는 웹 페이지를 제어하기 위한 추가적인 수단을 제공하고, Node.js 서버 환경에서는 서버 사이드에서 필요한 기능들을 지원한다.

이처럼 호스트 환경에 따라 자바스크립트 코어 기능 외에 각각 제공하는 기능이 서로 상이하다. 대표적인 예로 우리는 이전 챕터에서 브라우저 환경은 전역객체로 window를 사용하는 것을 살펴보았다. 그러나 노드의 경우엔 브라우저 호스트 환경이 아니기 때문에 window를 전역객체로 사용할 수 없다.

자바스크립트는 브라우저 호스트 환경을 제일 먼저 고려하였기 때문에 이와 관련된 기능들 역시 그 역사가 깊다. 다음은 호스트 환경이 웹 브라우저일 때 사용할 수 있는 기능을 개괄적으로 보여준다.

앞서 살펴본 것과 같이 브라우저 호스트 환경에서는 최상단에 window 라 불리는 루트 전역 객체가 있다. window 전역객체는 2가지 역할을 수행한다.

  1. 자바스크립트 코드의 전역 객체의 기능 수행
  2. 브라우저 창을 대변하고, 이를 제어할 수 있는 메서드 제공

브라우저를 제어하는 여러 메서드와 프로퍼티에 대해서는 다음 챕터에서 자세히 살펴보도록 하자.

1) 문서 객체 모델(DOM)

문서 객체 모델(Document Object Model, DOM)은 웹 페이지 내의 모든 콘텐츠를 객체로 나타낸다. 그리고 이 객체는 수정 가능한 객체이다.

해당 모델의 주축이 되는 객체는 document 객체인데, 해당 객체는 페이지의 기본 진입점 역할을 수행한다. 우리는 document 객체를 이용해 브라우저 페이지 내에 있는 어떤 것이든 변경과 조작이 가능하다.

// 배경색 설정
document.body.style.backgroundColor = 'red';

문서 객체 모델은 이 외에도 매우 다양한 기능을 제공한다. 이 역시 다음 챕터에서 더 자세히 살펴보도록 하자.

DOM은 브라우저만을 위한 모델은 아니다. 가령 HTML 페이지를 다운로드하고 이를 가공해주는 서버 사이드 스크립트에서도 DOM을 사용한다. 물론 이러한 경우엔 DOM 명세서에 기재된 일부만 지원하게된다.

그리고 CSS 규칙과 스타일시트를 통한 스타일링을 위한 객체 모델 구조인 CSSOM 또한 존재한다. 이는 HTML과 다른 구조를 띄기 때문에 별도로 CSS 규칙과 스타일시트를 객체로 나타내기 위한 구조가 필요하다.

DOMCSSOM은 브라우저가 화면을 렌더링을 하기 위해 필수적으로 요구하는 요소이다. 브라우저는 두 모델을 통해서 최종적으로 화면에 그려지는 요소들을 표현한다. 정확히는 이 두 가지를 이용해 렌더 트리를 구축하고, 리플로우(Reflow)와 리페인트(Repaint)라는 과정을 통해 요소가 그려지게 된다. 이에 대한 자세한 설명은 다른 카테고리에서 추후에 포스팅 할 예정이다.

2) 브라우저 객체 모델(BOM)

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

대표적으로 다음과 같은 항목들이 있다.

  • navigator 객체 : 브라우저와 운영체제에 대한 정보 제공
  • location 객체 : 현재 URL 정보 및 새로운 URL 관련 설정 기능 제공
  • alert/confirm/prompt 역시 BOM의 일부로, 문서와 직접 연결되어 있지 않으나 사용자와 브라우저 간 인터랙션을 도와주는 순수 브라우저 메서드

BOM은 HTML 명세서의 일부에 해당한다. BOM에 관련된 자체 명세서는 따로 존재하지 않지만 해당 명세서에서는 HTML 명세뿐만이 아니라 다양한 객체와 메서드, 브라우저에서만 사용되는 DOM 확장을 같이 다루고 있다.

DOM 트리

HTML은 마크업 언어이고 태그(Tag) 기반으로 작성된다. 문서 객체 모델(DOM)에 따르면, 모든 HTML 태그는 객체이다. 만약 태그 내에 또 다른 태그가 있는 경우엔 중첩 태그라고 부르며, 태그 내에 문자 역시 객체로 인식한다.

예를 들어 document.body는 HTML의 <body> 태그를 객체로 나타낸 것이다. HTML의 모든 태그는 객체로 구성되고 이는 DOM 모델로 구축되기 때문에, 자바스크립트를 통해 모든 객체에 접근할 수 있고 조작 또한 가능하다.

DOM을 조작하는 방법을 살펴보기 전에 먼저 DOM의 구조에 대해 먼저 알아보자.

1) DOM 구조

다음은 HTML로 작성한 간단한 마크업 구조의 페이지이다.

<!DOCTYPE HTML>
<html>
  <head>
    <title>사슴에 관하여</title>
  </head>
  
  <body>
    사슴에 관한 진실.
  </body>
</html>

위 구조를 DOM으로 나타내면 다음과 같은 구조를 띄게 된다.

해당 트리 구조에서 나타나는 모든 항목은 객체이다.

태그는 요소 노드(Element Node)라고 불리는 노드이며, 트리 구조를 구성한다. 가령 <html>DOM에서 루트노드가 되고, <head><body>는 루트노드의 자식노드가 된다.

반면 요소 내의 문자는 텍스트 노드(Text Node)로 분류한다. 해당 노드는 오직 문자열만 담으며, 자식노드를 가질 수 없는 리프노드에 해당한다.

이때 <head> 요소 노드에 <title> 요소 노드 앞 뒤로 공백과 같은 값이 들어있는 텍스트 노드가 있는 것에 유의하자. 각각의 값은 새 줄(new line)과 공백(space)에 해당하는 문자열인데 이는 공백으로 보이는 무의미한 값으로 보일지라도 자바스크립트에서는 항상 유효한 문자로 취급된다.

따라서 이 두 특수문자는 당연히 텍스트 노드가 되며, 동시에 DOM의 일부로 자리잡는 것이다. 이는 위 HTML 문서에서 엔터와 공백을 입력한 값들 역시 모두 텍스트 노드로 인식되어 DOM 구조에 삽입된 것이다. 다만 텍스트 노드 생성엔 두 가지 예외가 존재한다.

  1. 역사적인 이유로 <head> 이전의 공백과 새 줄은 무시된다.
  2. HTML 명세에 따르면 모든 콘텐츠는 <body> 태그 안 쪽에 있어야 한다. 따라서 </body> 뒤에 무언가를 넣더라도 그 콘텐츠는 자동으로 body 태그 안쪽으로 옮겨진다. 따라서 </body> 뒤에는 공백이 존재할 수 없다.

이 경우를 제외하곤 문서 내 공백은 모두 텍스트 노드로 인식되어 DOM 트리가 만들어지게 될 것이다. 만약 이러한 텍스트 노드를 만들지 않으려면 다음과 같이 공백이 없도록 HTML 문서를 작성해야 할 것이다.

<!DOCTYPE HTML>
<html><head><title>사슴에 관하여</title><body>사슴에 관한 진실.</body></html>

그러나 보통 이런 구조로 마크업 구조를 작성하지 않을 뿐더러 공백과 같은 텍스트 노드는 크게 중요한 의미를 가지고 있지 않다. 심지어 개발자 도구에서는 문자열 양 끝 공백과 공백만 있는 텍스트 노드를 별도로 표기하지 않는다. 이들 모두 표시하게 되면 과도하게 화면을 차지할 우려가 있기 때문이다. 때문에 이후 작성할 DOM 트리 역시 의미 없는 공백의 텍스트 노드는 별도로 표기하지 않을 예정이다.

2) 자동 교정

HTML 문서는 오류에 대해 굉장히 관용적이다. 웬만한 오타와 요류는 HTML 파서를 통해 마크업 구조를 분석할 때 자체적으로 수정하게 된다. 이와 관련한 자세한 알고리즘과 세부사항은 네이버 기술 블로그에 상세하게 설명되어있으니 궁금하다면 참고하도록 하자.

중요한 것은 기형적인 구조의 HTML을 만날 때 브라우저가 DOM 생성과정에서 이를 자동으로 교정한다는 것이다. 예를 들어 가장 최상위 태그는 항상 <html>이어야 하는데 문서에 <html> 태그가 없는 경우 문서 최상위에 이를 자동으로 추가한다. 따라서 DOM은 항상 <html>에 대응하는 노드를 가지게 된다. 다른 태그 역시 동일하다.

또 다른 예로 만약 HTML 문서 내 "안녕하세요"와 같은 문장 하나만 저장된 상황이라고 하더라고, 브라우저는 이를 자동으로 <html><body> 태그로 감싸진 형태로 만들어준다.

그 밖에도 DOM 생성과정에서 브라우저는 문서에 있는 에러, 닫는 태그가 없는 에러 등을 자동으로 교정한다. 가령 HTML 명세서에 의하면 table 태그에는 항상 tbody 태그가 있어야 한다고 말하고 있지만 대부분 HTML을 작성할 때는 이를 생략하는 경우가 많다. 이 역시 브라우저가 자동으로 DOMtbody를 추가한 트리를 만들게 된다.

3) 기타 노드 타입

요소 노드와 텍스트 노드 외에도 다양한 노드 타입이 있다. 대표적으로 주석 역시 노드가 된다.

<!DOCTYPE HTML>
<html>
  <body>
    사슴에 관한 진실.
    <ol>
      <li>사슴은 똑똑합니다</li>
      <!-- COMMENT -->
      <li>그리고 잔꾀를 잘 부리죠!</li>
    </ol>
  </body>
</html>

해당 DOM 트리를 확인하면 초록빛깔로 새로운 노드 타입이 등장한 것을 볼 수 있다. 이를 주석 노드(Comment Node)라고 칭한다.

주석은 화면 출력에 영향을 주지 않는 요소인데 왜 DOM 트리를 같이 구성하는 요소가 되는지 의아할 수 있다. 이는 단순히 HTML 문서 내 모든 것은 심지어 그것이 주석이라 할 지라도 모두 DOM을 구성한다는 규칙이 있기 때문이다. 주석 뿐만 아니라 문서 최상단에 선언하는 <!DOCTYPE ...> 역시 DOM 트리를 구성하는 노드가 되지만, 해당 포스트에서는 이 노드에 대해 다루지 않을 것이기 때문에 별도로 표시하지는 않았다. 하지만 DOM 트리에 존재하고 있다는 점은 알아두도록 하자.

그 외에도 여러가지 노드 타입이 존재한다. 이러한 노드 타입은 총 12가지 인데, 실무에서는 주로 네 가지 노드를 다룬다고 한다.

  1. DOM의 진입접이 되는 document 노드
  2. HTML 태그에서 만들어지며 DOM 트리를 구성하는 블록인 요소 노드(Element Node)
  3. 텍스트를 포함하는 텍스트 노드(Text Node)
  4. 주석 노드(Comment Node)

DOM 탐색하기

DOM을 이용하면 요소와 요소의 콘텐츠에 적극적으로 개입할 수 있다. 이는 기존 요소의 내용을 변경하는 것 부터 새로운 요소의 삽입 또는 제거 등 다양한 조작이 가능하다. 이처럼 어떤 요소를 조작하기 위해서는 당연히 먼저 조작하고자 하는 DOM 객체에 대한 접근이 선행되어야 할 것 이다.

앞서 DOM 의 진입점이 되는 객체는 document 객체라고 했다. 따라서 해당 객체를 통과하면 어떤 노드에도 접근할 수 있다. 즉 DOM에 수행하는 모든 연산은 document 객체에서 시작한다.

1) 트리 상단의 documentElement와 body

DOM 트리 상단의 노드들은 document가 제공하는 프로퍼티를 통해 접근이 가능하다.

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

document.bodynull일 수도 있다. 모듈 챕터에서 우리는 HTML이 동기적으로 렌더링되는 것을 살펴보았다. 따라서 만약 <body> 태그보다 이전에 선언된 부분에서 아직 렌더링 되지 않은 태그에 접근하려는 경우가 되기 때문에 null을 반환하는 경우가 생길 수 있다. 즉 브라우저가 아직 document.body를 읽지 않은 시점에서 접근하려는 경우가 그러한 사례에 해당된다.

2) childNodes, firstChild, lastChild 자식노드 탐색

부모 노드 밑에 있는 노드를 흔히 자식 노드라고 한다. 이때 다음과 같이 용어를 정립하고 출발하자.

  • 자식노드(child node, children) : 바로 아래에 있는 자식 요소만을 의미한다.
  • 후손노드(descendants) : 중첩 관계에 있는 모든 요소를 말한다. 자식노드, 자식의 자식노드, ... , 리프노드 등 까지 이어질 수 있다.

childNodes 컬렉션은 텍스트 노드를 포함한 모든 자식노드를 담고 있다. 아래 예시에서 출력값은 document.body 다음과 같은 자식노드가 된다.

<html>
  <body>
    <div>시작</div>
    
    <ul>
      <li>항목</li>
    </ul>
    
    <div></div>
    
    <script>
      for (let i = 0; i < document.body.childNodes.length; i++) {
        console.log( document.body.childNodes[i] ); 
        // Text, DIV, Text, UL, ... , SCRIPT          
      }
    </script>
      
    ... 추가 태그 내용
      
  </body>
</html>

모든 자식노드에 접근하기 때문에 텍스트 노드, 그리고 <script> 태그까지 접근하고 있는 것을 볼 수 있다. 이때 <script> 태그 아래쪽에 있는 추가 태그 내용은 브라우저가 스크립트 실행 시점에서 아직 읽지 못하기 때문에 접근할 수 없다.

firstChildlastChild는 그 의미처럼 빠르게 첫 번째 자식노드와 마지막 노드에 접근할 수 있다. 이때도 마찬가지로 모든 자식노드를 포함한 상태에서의 첫 번째와 마지막 자식노드를 말한다. 따라서 자식 노드가 존재한다면 아래 비교문은 항상 참이 된다.

elem.childNodes[0] === elem.firstChild;
elme.childNodes[elem.childNodes.length - 1] === elem.lastChild;

3) DOM 콜렉션

위에서 살펴본 childNodes는 마치 배열 같아 보이지만 이는 배열이 아닌 반복 가능한(Iterable) 유사 배열 객체인 컬렉션(Collection)이다. 유사 배열 객체이기 때문에 다음과 같은 특징을 가진다.

  1. for...of를 사용할 수 있다.
for (let node of document.body.childNodes) {
  console.log(node);	// 컬렉션 내 모든 자식 노드 출력
}

for...in 을 통한 순회는 지양하도록 하자. 해당 반복문은 객체의 모든 열거 가능한 프로퍼티를 순회하게 되는데, 컬렉션엔 거의 사용되지 않는 추가 프로퍼티가 있기 때문에 이것까지 모두 순회의 대상이 되기 때문이다.

  1. 배열이 아니기 때문에 배열 메서드를 바로 사용할 순 없다.

    • 따라서 만약 배열 메서드를 적용해야 하는 경우에는 Array.from() 메서드를 이용하여 배열로 먼저 변환하는 과정이 필요하다.
const res = document.body.childNodes.filter;	// undefined

const res = Array.from( document.body.childNodes ).filter	// function

DOM 컬렉션은 읽는 것만 가능하는 것에 주의하자. DOM 컬렉션을 비롯해 이번 챕터에서 이야기하는 모든 탐색용 프로퍼티는 읽기 전용이다. 따라서 childNodes[i] = ... 과 같이 자식 노드를 교체하는 방법은 불가능하다. 만약 교체를 하고 싶다면 다른 메서드를 사용해야 한다.

또한 DOM 컬렉션은 몇몇 예외사항을 제외하면 live하다. 즉 DOM에 추가 수정사항 등이 발생했을 때 현재 상태를 바로 반영한다. 예를 들어 elem.childNodes를 참조하고 있는 도중에 다른 곳에서 DOM에 새로운 노드를 추가하는 경우엔, 해당 변경사항이 기존에 참조하고 있던 컬렉션에도 자동으로 반영된다.

4) 형제와 부모노드

같은 부모를 가진 노드는 서로 형제 관계에 있다 하여 형제(Sibling)노드라고 부른다. 예를 들어 <head><body>는 대표적인 형제 노드이다.

<html>
  <head>...</head><body>...</body>
</html>

이때 형제 노드끼리도 순서를 정할 수 있는데 위 사례에서 두 형제노드끼리는 다음의 관계가 성립한다.

  • <body><head>의 다음(next) 혹은 우측에 있는 형제 노드
  • <head><body>의 이전(previous) 혹은 좌측에 있는 형제 노드

따라서 다음 형제노드에 대한 정보는 nextSibling, 이전 형제노드에 대한 정보는 previousSibling 프로퍼티에서 접근할 수 있다.

부모노드에 대한 정보는 parentNode 프로퍼티를 이용하여 접근이 가능하다.

document.body.parentNode === document.documentElement;
document.head.parentNode === document.documentElement;

document.head.nextSibling === document.body;

document.body.previousSibling === document.head;

5) 요소 간 이동

위에서 살펴본 탐색 관련 프로퍼티는 모든 종류의 노드를 참조한다. childNodes를 이용하면 텍스트 노드, 요소 노드, 그리고 주석 노드까지 참조할 수 있다.

그러나 실무의 영역에서는 텍스트 또는 주석 노드에 대한 접근은 좀처럼 잘 다루지 않는다. 대부분 태그의 분신인 요소 노드에 접근하여 조작하는 경우가 많다.

위 그림을 보면 앞서 살펴봤던 이미지와 크게 다르지 않은 것을 알 수 있다. 유일한 차이점이라고 한다면 Element라는 단어가 추가되었다는 점이다. 각 요소들을 하나씩 살펴보자.

  • children 프로퍼티는 해당 요소의 자식 노드 중에 오직 요소 노드만 참조한다.
  • firstElementChildlastElementChild 프로퍼티는 각각 첫 번째 자식 요소 노드와 마지막 자식 요소 노드를 참조한다.
  • previousElementSiblingnextElementSibling 프로퍼티는 형제 요소 노드를 참조한다.
  • parentElement 프로퍼티는 부모 요소 노드를 참조한다.

이때 parentElement는 항상 요소 노드를 참조하기 때문에 parentNode와 달리 부모에 요소 노드가 없다면 null을 리턴하는 것에 주의해야 한다. 하지만 이러한 차이는 임의의 요소 elem에서 시작해 <html>까지 거슬러 올라가고 싶지만 document까지는 가고 싶지 않을 때 유용하게 활용할 수 있다.

// <html>까지 거슬러 올라간다
while (elem = elem.parentElement) {
  console.log(elem);
}

앞에서 보았던 예시에서 childNodeschildren으로 대체한다면 요소 노드만 출력되는 것을 확인할 수 있다.

<html>
  <body>
    <div>시작</div>
    
    <ul>
      <li>항목</li>
    </ul>
    
    <div></div>
    
    <script>
      for (let elem of document.body.children) {
        console.log( elem ); 
        // DIV, UL, DIV, SCRIPT          
      }
    </script>
      
    ... 추가 태그 내용
      
  </body>
</html>

6) 테이블 탐색하기

일부 DOM 요소 노드는 편의성을 위해 기본 프로퍼티 외에 추가적인 프로퍼티를 제공하는 경우가 있다. 가장 대표적인 테이블 <table>을 예시로 추가적인 프로퍼티들을 살펴보자.

  • table.rows : <tr> 요소를 담은 컬렉션 참조
  • table.caption/tHead/tFoot : <caption>, <thead>, <tfoot> 요소를 참조
  • table.tBodies : <tbody> 요소를 담은 컬렉션 참조
    • 표준에 따르면 테이블 내에 여러 개의 <tbody>가 존재할 수 있지만, 최소 하나는 반드시 존재해야 한다. 하지만 브라우저는 HTML 문서 테이블 내에 <tbody>가 없더라도 이를 자동으로 추가하는 자동 교정을 수행한다.

또한 <thead>, <tfoot>, <tbody> 요소는 rows 프로퍼티를 지원한다.

  • tbody.rows : tbody<tr> 요소 컬렉션 참조

<tr> 요소는 다음의 프로퍼티를 지원한다.

  • tr.cells : 주어진 <tr> 안의 모든 <td>, <th>를 담은 컬렉션 반환
  • tr.sectionRowIndex : 주어진 <tr><thead>/<tbody>/<tfoot> 안쪽에서 몇 번째 줄에 위치하는지를 나타내는 인덱스를 반환
  • tr.rowIndex : <table> 내에서 해당 <tr>이 몇 번째 줄인 지를 나타내는 숫자를 반환

마지막으로 <td><th> 요소는 다음 프로퍼티를 지원한다.

  • td.cellIndex : <td><th>가 속한 <tr>에서 해당 셀이 몇 번째인지 나타내는 숫자를 반환한다.

이처럼 몇몇 요소 노드는 테이블과 같이 편의성을 위해 추가적인 프로퍼티를 제공하는 경우가 많다.

References

  1. https://ko.javascript.info/document
  2. https://dom.spec.whatwg.org/
  3. https://d2.naver.com/helloworld/59361
profile
개발잘하고싶다

0개의 댓글