46. DOM 여행

소개

어떤 방법으로든 HTML 문서와 상호작용하지 않고도, 자바스크립트를 유용하게 사용한 웹 사이트를 찾기란 지극히 힘들 것이다. 일반적으로 말해서 당신의 스크립트 코드는 페이지에서 그 값을 읽어와야 하고 그 값을 어떤 방법으로 처리해야 하며, 그리고는 그 결과를 눈에 보이는 – 혹은 어떤 정보의 형태를 가지는 메시지의 – 형태로 만들어내야 한다. 당신의 웹 페이지와 웹 프로그램이 쓸만한 상호작용을 갖추게끔 하고자 하는 목표로 한걸음 더 다가가기 위해, 이 글과 다음 글은 Document Object Model 에 대해서 다룰 것이다. DOM은 당신이 만들어내는 의미적 계층과 표현적 계층을 탐험하고, 조종하는 방법을 제공할 것이다.

이 글을 읽고 나면, 당신은 DOM 이 무엇인지 잘 이해할 수 있을 것이고, HTML 페이지에서 당신이 원하는 정보를 찾거나 어떤 변화를 가하고자 하는 정확한 지점을 찾아내는 방법 역시 알게 될 것이다. 시리즈의 다음 글( HTML을 만들고 변경하기)에서는 실제로 페이지의 데이터를 조작하고, 값을 변경하며, 또는 완전히 새로운 요소와 속성을 추가하는 방법들에 대해 소개할 것이다.

이 글의 구조는 다음과 같다:

  • 씨 뿌리기
  • 나무 키우기
  • 노드
  • 가지에서 가지로
  • 직접적 접근
  • 요약
  • 연습문제

씨 뿌리기

DOM, 그 이름에서 짐작할 수 있듯 Document Object Model은 당신이 작성한 웹 페이지를 브라우저가 로딩하면서 만들어내는 HTML 문서의 모델이다. 자바스크립트는 이 모델의 모든 정보들에 접근할 수 있다. 잠시만 뒤로 돌아가서 정확히 어떤 것들이 모델화되는지 살펴보도록 하자.

페이지를 작성할 때 나의 목적은 원래 내용에 의미를 추가해서 내가 사용할 수 있는 HTML 태그들을 삽입하는 것이다: 내용의 일부가 문단일 때 P 태그에 넣고, 그 다음은 링크 이므로 A 태그를 사용할 것이고… 이런 식이다. 또한, 요소들 사이에 연관관계를 설정할 수도 있다: INPUT 요소 각각은 LABEL을 가질 것이며, FIELDSET 내부에 포함될 것이다. 이에 더해서 나는 이러한 기본적인 HTML 문서에 ID와 CLASS 속성을 적절히 추가함으로서 스타일을 부여하거나 조종하기 위한 구조들을 페이지에 부여할 것이다. 이러한 HTML의 기초작업이 완성되면 CSS를 사용해서 순수하게 의미론적인 문서에 스타일리시 한 표현을 추가할 것이다. 자 어떤가! 사용자들을 즐겁게 할 수 있는 페이지를 만들어 내었다.

하지만 이게 전부는 아니다. 내가 만들어 낸 문서는, 자바스크립트로 조종할 수 있는 메타 정보가 풍부하게 들어있는 문서이다. 문서의 특정한 요소를 찾거나, 또는 요소의 그룹 을 찾아내서 사용자가 정의하는 변수에 따라 그것을 지울수도 추가할수도 수정할수도 있다. 표현적인 정보들(CSS)을 찾아내서는 즉석에서 스타일을 바꿀 수도 있다. 사용자들이 입력양식에 입력하는 정보의 유효성을 점검할수도 있고 그밖에 아주 여러가지 일들을 할 수 있다. 자바스크립트가 이러한 일들을 하기 위해서는 정보에 접근할 수 있어야 하고, DOM이 자바스크립트에게 모든 정보를 제공한다.

한가지 더 중요한 것은 잘 구조화된 HTML과 CSS가 자바스크립트를 위한 좋은 씨앗이 된다는 것이다. 빈약하게 구조화 된 문서는 당신이 생각했던 것에서 벗어난 방향으로 움직일 것이며, 브라우저들마다 다른 행동을 보일 것이다. 따라서 자바스크립트가 당신이 생각한대로 정확하게 움직이게끔 보장하려면 당신의 HTML과 CSS 모두가 올바르고 정확하게 구조화 되어 있는 것이 아주 중요하다.

나무 키우기

문서를 만들고 스타일을 입혔다면 다음 단계는 그것을 브라우저에 넘겨서 사용자들에게 보여주는 것이다. 그리고 여기에서 DOM이 개입하게 되는데 브라우저 내부에서 당신의 문서 전체를 읽고 즉석에서 DOM 을 만들어낸다. 구체적으로 말한다면 DOM은 HTML 문서를 나무로 표현하는데 당신의 선조들과 당신의 관계를 표현하는 “가계도”와 매우 흡사한 방식을 사용한다. 각각의 요소들은 DOM 내부에 노드NODE로서 포함되고, 요소를 잇는 각각의 가지들은 요소들이 직접적으로 포함하는 요소(즉, 자식children) 과, 그것을 직접적으로 포함하는 요소(즉, 부모parent) 들을 서로 연결한다. 자, 간단한 HTML 문서를 들여다보고 이 관계를 명확히 파악해보자:

<html>
  <head>
    <title>This is a Document!</title>
  </head>
  <body>
    <h1>This is a header!</h1>
    <p id="excitingText">
      This is a paragraph! <em>Excitement</em>!
    </p>
    <p>
      This is also a paragraph, but it`s not nearly as exciting as the last one.
    </p>
  </body>
</html>

위에서 보듯 전체 문서는 HTML 요소에 포함되어 있다. HTML 요소는 두개의 요소를 직접적으로 포함하는데 HEAD와 BODY 요소다. HEAD와 BODY는 HTML의 자손으로 표현되고, 또한 HTML을 부모로 갖는다. 그리고 이러한 관계가 문서의 계층구조를 따라 내려가면서 반복된다. 각각의 요소는 가장 가까운 자손 요소를 자식으로 가리키고, 가장 가까운 선조 요소를 부모로서 가리킨다:

  • TITLE 은 HEAD 의 자식이다.
  • BODY 는 세 개의 자식 요소들을 갖는다. 두개의 P 요소와 하나의 H1 요소이다.
  • id=“excitingText” 를 갖고 있는 P 요소는 하나의 EM 요소를 자식 요소로 가진다.
  • 요소의 평범한 텍스트 내용(This is a Document! 같은) 역시 DOM에서 Text Nodes 라고 표현된다. 이런것들은 자식 요소를 갖지 않으며 자신을 포함하는 요소를 부모로 가리킨다.

즉, 우리가 위의 예제에서 바라본 DOM 계층구조는 그림 1 처럼 표현되어질 수 있다:

그림1. HTML 문서를 시각적으로 표현한 DOM 트리

이것은 HTML 문서에서 이러한 트리 구조로 직선적으로 옮긴 것이다. 트리 구조는 페이지의 요소들 사이의 관계를 간결하게 표시하여 계층구조를 깔끔하게 보여준다. HTML 노드 위에 document 라는 이름의 노드를 추가한 것을 보았을 것이다. 이것은 문서의 루트 이며 자바스크립트가 사용할 수 있는 가장 분명한 길잡이이다.

노드

나무를 뒤흔들고 가지에서 가지로 헤엄쳐 다니기를 시작하기 전에 내가 정확히 무엇을 하려고 하는지 점검해 볼 시간을 잠시 갖는 것이 좋을것 같다.

DOM 트리의 모든 노드들은 객체이며 문서의 요소 각각을 나타낸다. 노드들은 인접해 있는 다른 노드들과의 상호관계에 대해 이해하고 있으며, 자신에 대해서도 충분한 정보를 포함하고 있다. 마을 뒤 언덕 참나무에 올라가서 가지와 가지 사이를 오르내리는 어린아이가 하는 방식과 비슷하게 하나의 노드에서 그 부모 혹은 자식 노드로 접근하는 방법을 충분히 알 수 있다.

자바스크립트 객체의 기본 속성을 통해 짐작하겠지만 이 경우 내가 찾고 있는 정보는 노드의 속성을 통해 드러난다. 구체적으로 말해서 parentNode 와 childNodes 속성이다. 페이지에 있는 각각의 객체들은 항상 하나의 부모를 가지므로 parentNode 속성은 다른 선택의 여지가 없고 항상 부모 요소를 가리킨다. 그와 반대로, 노드들이 가질 수 있는 자식 노드의 숫자에는 제한이 없으므로 childNodes 속성은 배열을 구성하게 된다. 배열의 각 요소들은 하나의 자식을 가리키는데 배열에 나타난 요소의 순서는 문서에 나타난 순서와 동일하다. 위에서 보여준 예제의 BODY 요소가 가지는 childNodes 배열은 H1, 첫번째 P 요소, 두번째 P 요소를 순서대로 포함하게 된다.

물론, 노드에서 흥미로운 속성들이 이것뿐인 것은 아니다. 하지만 시작점으로서는 꽤 괜찮다. 자 그럼, 이 노드들 중 하나에 접근하려면 어떤 코드를 써야 할까? 어디에서부터 탐색을 시작해야 할까?

가지에서 가지로

시작 지점으로 가장 좋은것은 문서의 루트인데 실로 창의적으로 이름지어진 document 객체를 통해서 접근할 수 있다. document는 루트에 있으므로 parentNode를 갖지 않고 단 하나의 자식요소 HTML 요소를 가지며 다음과 같이 접근할 수 있다:

var theHtmlNode = document.childNodes[0];

이 코드는 theHtmlNode라는 이름의 새로운 변수를 만들어내고, 그 값으로 document 객체의 첫번째(자바스크립트 배열은 항상 0으로 시작하지, 1로 시작하지 않는다는 점을 기억하기 바란다) 자식을 할당한다. 우리가 HTML 노드를 다루고 있다는 사실은 theHtmlNode 의 nodeName 속성을 통해 확인할 수 있다. nodeName 속성은 선택한 노드의 정확한 종류에 대해 중요한 정보를 알려준다:

alert( "theHtmlNode is a " + theHtmlNode.nodeName + " node!" );

이 코드는 “theHtmlNode is a HTML node!” 라고 써 있는 경고상자를 만들어낸다. 대단하다! nodeName 속성은 노드의 타입에 대해 알려준다. 요소 노드들에 대해서는 이 속성은 대문자로 씌어진 태그 이름을 반환한다. 여기서는 HTML 이고, 링크에 대해서는 A 일 것이며 문단에 대해서는 P 일 것이다. 텍스트 노드의 nodeName 속성은 #text 이고, document의 nodeName은 #document 이다.

theHtmlNode 는 부모 요소에 대한 정보도 포함하고 있을 것이다. 이것의 동작은 다음과 같이 확인해볼 수 있다:

if ( theHtmlNode.parentNode == document ) {
  alert( "Hooray!  The HTML node's parent is the document object!" );
}

정확히 우리가 기대한 대로 움직이고 있다. 이러한 정보를 이용해서 예제 문서의 첫번째 문단을 참조하는 코드를 만들어 보기로 하자. 이것은 BODY 요소의 두번째 자식이며, BODY 요소는 HTML 요소의 두번째 자식이고, HTML 요소은 또한 document 객체의 첫번째 자식이다. 휴우.

var theHtmlNode = document.childNodes[0];
var theBodyNode = theHtmlNode.childNodes[1];
var theParagraphNode = theBodyNode.childNodes[1];
alert( "theParagraphNode is a " + theParagraphNode.nodeName + " node!" );

원더푸울! 우리가 원하는 그대로 동작하고 있다. 하지만 이것은 실로 번거로우며 이것을 쓸 수 있는 훨씬 좋은 방법이 있다. 객체에 관한 글에서 객체 참조를 연결하는 방법을 배웠는데 같은 것을 여기에서도 할 수 있다. 중간의 변수들을 없애버리고 다음과 같이 쓰는 것이다.

var theParagraphNode = document.childNodes[0].childNodes[1].childNodes[1];
alert( "theParagraphNode is a " + theParagraphNode.nodeName + " node!" );

훨씬 덜 번거로우며, 코드의 양도 줄여준다.

노드의 첫번째 자식은 항상 node.chileNodes[0]이며, 노드의 마지막 자식은 항상 node.childNodes[node.childNodes.length – 1] 이다. 이것들을 굉장히 자주 사용하는데, 계속해서 타이핑하기엔 좀 부담스럽다. 이것들이 아주 자주 사용되므로, DOM 에서는 두가지에 대해 명시적인 바로가기를 제공한다: 각각 .firstChild와 .lastChild이다. html이 document의 첫번째 자식이고 body는 html의 마지막 자식이므로, 위의 코드를 아래와같이 더욱 간결하게 쓸 수 있다:

var theParagraphNode = document.firstChild.lastChild.childNodes[1];
alert( "theParagraphNode is a " + theParagraphNode.nodeName + " node!" );

이렇게, 근접한 노드들 간의 탐색방법은 유용하고, 이것만을 사용해서도 문서의 어디로든 갈 수 있지만, 매우 성가신 것이 사실이다. 위와 같은 아주 간단한 문서에서도 루트 노드에서 출발하여 마크업 속으로 깊숙히 탐색해 나가는 것이 얼마나 고된 노동인지 깨닫고 있을 것이다. 즉, 더 좋은 방법이 있을 것이다!

직접 접근

페이지에서 당신이 원하는 각각의 요소까지 탐색하는 명시적인 경로들을 설정하는 것은 대단히 어렵다. 또한, 만약 당신이 작업하는 페이지가 동적으로 생성(예를 들어, PHP나 ASP.NET과 같은 서버사이드 언어가 만들어내는)되는 상황이라면 이것은 완전히 불가능해진다. 당신이 찾고자 하는 문단이 항상 BODY 노드의 두번째 자식임을 보장할 수 없기 때문이다. 따라서, 원하는 요소를 둘러싼 환경에 대해 명시적인 정보를 갖지 않고도 요소를 탐색할 수 있는 더 좋은 방법이 필요하다.

위의 예에서 들었던 HTML 문서를 돌이켜 보면, ID 속성을 가진 문단이 있다. 이 ID 속성은 유일하고, 문서의 특정 위치를 명시함으로서 당신이 루트에서부터 출발하는 기나긴 길을 명시하지 않아도 되게끔 한다. document 객체의 getElementById 메서드를 사용하는 것이다. 이 메서드는 당신이 예상하는 바로 그대로 동작하는데, 요청하는 ID가 문서에 존재하지 않는다면 null을 반환하며, 존재한다면 그 요소 노드를 반환한다. 이것을 테스트하기 위해 다음의 새로운 메서드를 앞의 것과 비교해보도록 하자:

var theParagraphNode = document.getElementById('excitingText');
if ( document.firstChild.lastChild.childNodes[1] == theParagraphNode ) {
  alert( "theParagraphNode is exactly what we expect!" );
}

이 코드 역시 확인 메세지를 띄울 것이고, 메세지를 통해서 두가지 방법이 예제 문서에서는 동일한 결과를 가져온다는 것을 보여줄 것이다. getElementById는 페이지의 특정 부분에 접근하는 가장 효율적인 방법이다. 페이지의 어딘가에 접근하기 위해서 무엇인가 해야 한다면(특히, 거기가 어디인지 확신할 수 없다면) ID 속성을 추가하는 것은 많은 시간을 절약해 줄 것이다.

이와 비슷하게 유용한 것이 DOM의 getElementsByTagName 메서드인데, 이것은 페이지에서 특정 타입의 모든 요소들에 대한 묶음을 반환한다. 예를 들어 자바스크립트를 이용해 페이지에 있는 모든 P 요소들을 볼 수도 있다. 다음의 예제는 흥미진진한 문단과 그보다는 조금 덜 재미있는 그의 형제를 우리에게 데려다준다:

var allParagraphs = document.getElementsByTagName('p');

allParagraphs에 저장되어 있는 결과 묶음collection을 활용하는 가장 좋은 방법은 for 반목문이다. 배열에 대해 사용하는 방법과 거의 동일하게 할 수 있다:

for (var i=0; i < allParagraphs.length; i++ ) {
  //  do your processing here, using
  //  "allParagraphs[i]" to reference
  //  the current element of the
  //  collection

  alert( "This is paragraph " + i + "!" );
}

더욱 복잡한 문서에 대해서는 특정 타입의 모든 요소를 가져오는 것 역시 끝없는 일이 될 것이다. 방대한 페이지에 포함된 200개의 DIV 요소 속에서 허우적대는 것 보다는 당신이 원하는 것은 아마도 특정 섹션에 포함된 DIV 요소들을 조종하는 것일 것이다. 이러한 경우 결과를 제한하기 위해서 두가지 방법을 병행할 수 있다. ID를 이용해서 하나의 요소를 골라내고, 거기에 포함된 특정 타입의 모든 요소들을 질의하는 것이다. 예를 들어, 흥미진진한 문단에 포함된 모든 EM 요소들을 가져오기 위해 다음과 같이 할 수 있다:

document.getElementById('excitingText').getElementsByTagName('em')

요약

DOM은 자바스크립트가 우리를 위해 웹에서 할 수 있는 거의 모든 것에 대한 기반이다. 이것은 페이지의 내용과 상호작용하기 위한 인터페이스이고, 그 모델 위에서 어떻게 움직일 것인지 이해하는 것은 아주 중요하다.

이 글에서는 그러한 작업을 위한 기본적인 도구들을 제공하였다. 이제 당신은 document를 통해 DOM의 루트에 접근할 수 있고, childNodes와 parentNode를 통해 트리의 노드들 사이의 직접적 연관관계를 오르락 내리락 할 수 있다. 기초적인 것들을 생략하고 길고 지루한 경로를 일일히 작성하는 것을 피하기 위해 getElementById와 getElementsByTagName을 사용해서 당신만의 바로가기를 만들어 낼 수 있다. 하지만 당신의 트리 안에서 움직이는 것은 시작에 불과하다.

논리적인 다음 단계는 자바스크립트가 제공하는 재미있는 것들을 실제로 해보기 시작하는 것이다. 데이터를 획득하고 스크립트에 힘을 싣는 방법을 배워야 할 것이고, 흥미진진한 상호작용들을 통해 문서의 데이터를 조작할 수 있어야 할 것이다. 문서의 노드들과 그 속성들 사이에서 상호작용하기 위해 DOM 이 제공하는 방법들을 알려주는, 그리고 언젠가 당신이 직접 만들어 낼 인터페이스와 스크립트들 속으로 누비어 나갈 길을 제시하는 다음 글에서 이러한 주제들을 연구해 볼 것이다.

연습문제

  • 글에서 사용한 예제 문서를 통해 HEAD 요소에 접근할 수 있는 세가지의 서로 다른 경로들을 작성해 보라. childNodes, parentNode 메서드들을 원하는대로 연결해서 사용할 수 있음을 기억하기 바란다.
  • 임의의 노드에서 그 타입을 어떻게 구별할 수 있는가?
  • 임의의 노드에서 document 객체로 어떻게 돌아갈 수 있는가? 힌트: document 객체의 parentNode 속성은 null을 반환한다는 것을 상기하라.

저자에 관해

Mike West 는 경험많고 성공한 웹 개발자의 탈을 쓴 철학과 학생이다. 그는 웹 분야에서 10년 넘게 일해오고 있으며, 야후! 의 유럽 뉴스 사이트 개발의 책임을 맡은 팀에서 오랫동안 일했다.

2005년, 텍사스 교외의 광활한 평야를 떠나, Mike 는 독일의 뮌헨에 정착했고 독일어를 익히기 위한 그의 고군분투는 다행히 성공적이다. mikewest.org 가 웹에서의 그의 집이며, 후대를 위해 자신의 글과 링크들을 (천천히)모아 가고 있다. 그가 작성한 코드들은 GitHub 에서 찾아볼 수 있다.

이 글을 다 읽어주셨다면, 댓글을 남겨주세요. 좋았다라는 격려도 좋고, 잘못된 부분을 지적해 주시는 것도 좋습니다. 마음에 드셨다면 아래 Like 버튼을 눌러서 페이스북과 트위터로 소개해 주시면 더욱 좋겠습니다.