Ayden's journal

DOM과 인터랙티브 자바스크립트

DOM은 문서 객체 모델(The Document Object Model)로서 웹페이지 내부의 모든 요소를 객체로 나타낸 것이다. 문서 객체의 상위에는 웹브라우저의 요소를 객체로 나타낸 브라우저 객체 모델(window 객체)이 존재한다. 브라우저 객체 모델에는 document 객체 뿐만 아니라 alert, console 등의 객체도 포함되어있다.

JS를 사용하여 웹 페이지를 '인터랙티브'하게 구현하는 첫걸음은 결국 마개조해줄 요소 노드(element node)를 변수나 상수에 가져다 넣는 것부터 시작할 것이다. 여기에는 이미 존재하는 노드를 불러오는 것 뿐만 아니라 당장은 문서 객체 내에 존재하지 않는 노드를 생성하는 것 역시 포함된다.

 

 

조작할 노드 불러오기

존재하지 않는 노드 생성하기

createElement() 매소드를 활용해 새로운 요소 노드를 생성할 수 있다.

const newTag = document.createElement('li');
// <li></li> 태그를 생성

이렇게 생성한 태그의 내부는 textContent 등의 프로퍼티나 createTextNode(), appendChild() 메소드를 활용하여 꾸며줄 수 있다.

newTag.textContent = "방 청소하기";
// textContent 외에도 innerHTML 등을 사용할 수도 있음.

const newText = document.createTextNode("방 청소하기");
newTag.appendChild(newText);
// node.appendChild()는 아규먼트로 들어온 노드를 특정 부모 노드의 자식 리스트 중 마지막에 붙여준다
// newTag의 자식 리스트는 비어있는 텍스트 노드

 

존재하는 노드 불러오기

getElement~() 메소드

const x = document.getElementById('id 선택자');
// 주어진 문자열과 일치하는 id 속성을 가진 문서 내 첫 번째 요소를 찾고, 해당 요소를 객체로 반환
// 존재하지 않는 id를 찾으면 null 값을 반환

const y = document.getElementsByClassName('클래스 선택자');
// 주어진 문자열을 클래스로 가진 모든 자식 엘리먼트의 실시간 HTMLCollection을 반환
// 존재하지 않는 class를 찾으면 빈 유사배열을 반환

const z = document.getElementsByTagName('태그 이름');
// 주어진 문자열과 일치하는 요소의 HTMLCollection을 주어진 태그명을 반환

 

querySelector~() 메소드

CSS 선택자를 활용하여 원하는 노드를 찾을 수 있다. 때문에 getElement와는 다르게 속성 선택자(attribute selectors)를 활용할 수 있다.

const isildur = document.querySelector('#isildur');
// 주어진 선택자와 일치하는 문서 내 첫 번째 요소를 반환함
// 존재하지 않는 선택자를 찾으면 null 값을 반환

const anaarion = document.querySelectorAll('.anaarion');
// 주어진 선택자와 일치하는 모든 요소를 NodeList로 반환함.

 

찾은 노드의 가족 노드 불러오기

자식 노드 불러오기

let doingList = document.querySelector('#doing-list')

doingList.children
// 찾은 노드의 자식 요소를 HTMLCollection이라는 유사 배열로 반환
// children[0] 등과 같이 index를 사용할 수 있지만, 유사 배열이라 그런가 [-1]은 안됨

doingList.firstElementChild
// 찾은 노드의 맏이 요소를 반환

doingList.lastElementChild
// 찾은 노드의 막내 요소를 반환

 

부모, 형제 노드 불러오기

doingList.parentElement
// 찾은 노드의 부모 요소를 반환

doingList.previousElementSibling
// 찾은 노드의 바로 앞 형제 요소를 반환

doingList.nextElementSibling
// 찾은 노드의 바로 다음 형제 요소를 반환

 

HTMLCollection과 NodeList

어떤 메소드를 사용하는가에 따라 HTMLCollection로 반환될 수도, NodeList로 반환될 수도 있다. 이 두 가지는 모두 유사 배열(array_like)로서 숫자 형태의 인덱스가 존재하고, length 프로퍼티를 사용할 수도 있다. 그러나 push()나 pop()등과 같이 배열에 직접적인 수정을 가하는 메소드는 사용할 수 없다. 두 유사 배열의 근본적인 차이는 <여기>에서 살펴볼 수 있다.

 

 

불러온 노드 조작하기

없는 노드를 새로 만들거나, 있는 노드를 그대로 가져오거나, 가져온 노드의 주변을 찾아가는 방식을 통해 우리는 앞으로 조작하게 될 노드를 특정해주었다. 이제부터는 조작을 가하는 방식들에 대해 알아보려고 한다.

 

노드의 삭제와 이동(어쩌면 추가)

node.remove() 메소드를 사용하면 선택한 노드를 삭제할 수 있다.

특정한 노드를 원하는 곳으로 이동시키기 위해서는 우선 이동시킬 노드와 위치시킬 노드를 둘 다 선택해주어야 한다. 그리고는 다양한 메소드를 이용해 배치하고 싶은 정확한 위치를 특정해주면 된다.

let doneList = document.querySelector('#done-list')
let doingList = document.querySelector('#doing-list')

doingList.append(doneList.children[1]);
// 위치할_노드.prepend(이동시킬_노드); 의 형태로 사용한다
// prepend()는 맏이 노드로 이동시키고, append()는 막내 노드로 이동시킨다

앞서 살펴본 prepend()와 append() 메소드는 우리가 선택한 노드를 위치시킬 노드의 자식 노드로 옮겨준다. 그러나 자식 노드 중에서도 원하는 위치가 아니라 맏이 혹은 막내 노드로만 배치시킬 수 있다. 만약 그 위치를 세밀하게 정해주고 싶다면 after()와 before() 메소드를 사용할 수 있다. 이 메소드는 특정한 노드의 앞 혹은 뒤로 우리가 선택한 노드를 옮겨준다.

doneList.children[2].before(doingList.children[1]);
// 만약 doneList.before()라고 하면, <li> 태그들 사이가 아니라 <ul> 태그 앞으로 간다

DOM 상에 존재하지 않는 태그를 새로 만들어서 prepend()나 before()를 사용하면 이는 노드의 추가라고 볼 수 있다.

 

노드 프로퍼티

아래의 프로퍼티를 사용하면 노드 내부의 형태를 문자열로 반환 받을 수 있다. 이러한 문자열과 연산자를 활용하여 특정한 코드를 추가하거나 대체, 혹은 삭제할 수도 있다.

node.outerHTML
// 요소 노드를 포함한 전체적인 HTML 코드를 문자열로 반환

node.innerHTML
// 요소 노드 내부의 HTML 코드를 문자열로 반환

node.textContent
// 요소 안의 내용들 중에서 HTML 태그는 제외하고 텍스트만 반환

등호를 사용하면 변수에 새로운 값을 넣어주는 것처럼 기존의 코드를 지우고 새로운 코드로 내용을 대체하게 된다. +=와 같은 복합 연산자를 사용하면 기존 코드 뒤에 새로운 코드를 추가할 수도 있다.

node.innerHTML = '<li>낮잠자기</li>'; // 코드를 대체

node.innerHTML += '<li>낮잠자기</li>'; // 코드를 추가

textContent 프로퍼티는 HTML 태그를 제외한 텍스트만 받아온다. 때문에 node.textContent += '<li>낮잠자기</li>';와 같은 명령을 실행하면 <li> 태그가 추가되는 것이 아닌 &lt;li&gt;낮잠자기&lt;/li&gt;가 추가된다.

 

 

요소 노드의 속성(Attributes) 다루기

어떤 노드를 삭제하고 이동시키는 게 그냥 커피라면, 속성 조작은 T.O.P에 가깝다. CSS와 직접적으로 연결된 class나 id와 관련된 부분을 수정할 수도 있고, a[href]나 img[src]와 같은 속성을 바꿔줄 수도 있다.

 

HTML 표준 속성 다루기

node.hrefnode.className처럼 특정 노드에 속성 이름의 프로퍼티를 사용하면 해당 값이 문자열로 반환된다. 만약 사용하지 않은 속성(가령 title 속성을 쓰지 않았는데 node.title)을 사용하면 undefined가 출력된다.

노드 프로퍼티를 수정하는 것과 마찬가지로, 속성 이름 프로퍼티로 반환된 문자열에 연산자를 사용해 간단하게 수정해줄 수도 있다.

<p style="color: red;"></p>

const getRedTag = document.querySelector("[style*=red]");
getRedTag.style += " text-decoration: underline;"
// 결과 : <p style="color: red; text-decoration: underline;"></p>

 

style 프로퍼티

이 프로퍼티는 특정 노드의 CSSStyleDeclaration라고 하는 객체를 반환한다. 이 객체를 통해 노드에 적용되어있거나 적용하지 않은 CSS 속성의 값을 모두 조회할 수 있다. 우리는 이 CSSSD 객체의 프로퍼티가 문자열을 뱉어낸다는 것을 스바라시하게 활용해볼 수 있다.

const newObj = document.querySelectorAll(".text--large")

for (let tag of newObj)
    if (tag.style.fontFamily.includes("pretendard") == true) {
        tag.style.fontFamily = "NotoSans"
    } // pretendard 글꼴이 사용된 노드를 찾아서, NotoSans 글꼴로 바꿔준다
}

혹은 [href*="youtube"] 속성을 가진 태그를 유사 배열로 받아서, 반복문을 통해 유튜브 링크라는 의미로 style.color = "var(--youtube)"를 먹여버릴 수도 있겠다.

const newObj = document.querySelectorAll("[href*=youtube]")

for (let tag of newObj) {
    if (Boolean(tag.style.color) == true) { // 이미 color가 있으면
        let getColor = tag.style.color;	// 여기서 속성값을 따로 저장해주고
        tag.style.color = `var(--youtube, ${getColor})`; // 이렇게 속성값을 가져가는
        } else {
        tag.style.color = `var(--youtube)`
    }
}

갑자기 삘받아서 폭주한 것 치고는 아쉬운 일이지만, style 프로퍼티로 받아오는 CSSSD 객체는 노드에 인라인 스타일로 적용된 내용만 긁어온다. 그리고 적용하는 것 역시 인라인 스타일로 적용한다.

 

className 프로퍼티

앞서 살펴본 style 프로퍼티와 마찬가지로, className 프로퍼티 역시 해당 값이 문자열로 반환된다.

<p class="text"></p>

const exTag = document.querySelector("[class]");

exTag.className = 'doing';
// 덮어쓰기가 되어버린다
// 결과 : <p class="doing"></p>

exTag.className += 'done';
// 결과 : <p class="doingdone"></p>
// 문자열에 띄여쓰기를 안 넣으면 텍스트의 바로 뒤에 붙어버린다
// .doing과 .done이 각각 적용되는 것이 아니라 .doingdone가 적용되어버린다

className 프로퍼티는 간단하지만, 새로운 값을 추가해주거나 덮어쓸 수만 있을 뿐이다.

 

classList 프로퍼티

이 프로퍼티는 DOMTokenList라고 하는 유사 배열을 뱉어낸다. 이 유사 배열에 다양한 프로퍼티와 메소드를 이용하면 특정 노드의 클래스에 대한 다양한 정보를 파악할 수 있고, className 보다 더 나은 수준의 조작도 가능해진다.

node.classList
// 해당 노드에 적용된 클래스를 유사 배열로 반환

node.classList.value
// 해당 노드에 적용된 클래스를 전부 문자열로 반환

node.classList.length
// 해당 노드에 적용된 클래스의 갯수를 반환
node.classList.contains('nav__btn');
// 지정한 클래스 값이 요소의 class 속성에 존재하는지 확인하여 불린값으로 반환

node.classList.add('nav__btn', 'btn');
node.classList.remove('nav__btn');
// 원하는 클래스를 추가할 수도, 제거할 수도 있다
// 아규먼트로 두 개 이상의 클래스를 입력해줄 수 있다

node.classList.toggle('input__eyes--open', true/false);
// 이벤트에 따라 done 클래스를 추가했다 삭제했다 할 수 있다.
// 한 가지 클래스만 토글할 수 있다

 

HTML 비표준 속성 다루기

대부분의 HTML 속성은 DOM 객체의 프로퍼티로 변환된다. 표준 속성이 아닌 경우 아래 메서드를 활용하면 표준이 아닌 HTML 속성들도 다룰 수 있다.

node.getAttribute('href');
// 해당 속성에 적용된 값을 반환한다

node.setAttribute('class', 'fireWire');
// 해당 속성에 특정 속성값을 덮어쓴다
// 존재하지 않는 속성이라면 우선 해당 속성을 만들고, 그 다음 추가한다

node.removeAttribute('class');
// 해당 속성과 그 속성값을 모두 삭제한다

 

dataset 프로퍼티

표준이 아닌 커스텀 속성을 좀 더 안전하게 사용할 수 있는 방법이다. "data-*"의 형태로 표기하며, DOM으로 변환될 때 "data-" 부분은 dataset 객체(DOMStringMap)로 통합되고, "*" 부분이 프로퍼티 네임으로 사용된다.

<div data-max="5" data-min="1" data-time_limit="30"></div>

node.dataset // DOMStringMap {max: '5', min: '1', time_limit: '30'}
node.dataset["time_limit"] // "30"

"*"부분이 프로퍼티 네임으로 사용되기 때문에, 식별자 네이밍 규칙에 따라야 한다.

node.getAttribute("data-max"); // "5"

node.removeAttribute("data-time_limit");

 

 

이벤트 핸들링

지금까지 살펴본 내용은 변수 혹은 상수에 노드를 집어넣고, 그 노드를 어떻게 바꿀 것인지에 대한 것이었다. 이러한 맥락에 따른다면 이벤트 핸들링은 노드를 '언제' 바꿀 것인지에 대한 내용이라고 할 수 있겠다. JS에서는 마우스 클릭이나 키보드 스트로크 등을 이벤트라고 부르는데, 브라우저는 특정한 이벤트가 발생할 때마다 지속적으로 이를 감지한다. 그리고 미리 정해둔 이벤트가 발생했을 때 일련의 과정을 따라 이벤트를 처리할 수 있도록 JS 코드를 실행하는 과정을 이벤트 핸들링이라 부르는 것이다.

 

DOM 객체의 onclick 프로퍼티

const btn = document.querySelector('#myBtn');

btn.onclick = function() {
  console.log('Hello World!');
};

onclick 프로퍼티에 두 개의 이벤트 핸들러를 붙여준다고 해서, 둘 다 동작하지는 않는다. = 연산자가 늘 그렇듯 좀 더 나중에 등록한 이벤트 핸들러가 이전의 것을 덮어쓴다. 혹시 +=를 쓰면 두 개 붙나 싶어 해봤지만 아예 예상치못한 결과(문자열로 저장)가 나왔다.

 

add/removeEventListener() 메서드

function consoleEvent1() {
	console.log('안녕하세요');
}

function consoleEvent2() {
	console.log('반갑습니다');
}

btn.addEventListener('click', consoleEvent1);
btn.addEventListener('click', consoleEvent2);

btn.removeEventListener('click', consoleEvent2);

addEventListener() 메소드는 onclick 프로퍼티와 다르게 두 개 등록하면 둘 다 동작한다. removeEventListener() 메소드는 파라미터로 전달하는 타입과 이벤트 핸들러가 addEventListener() 메서드로 등록할 때와 동일 할 때만 이벤트 핸들러를 삭제할 수 있다. 

 

이벤트 객체

일단 이벤트가 발생하면, DOM은 그 이벤트에 대한 정보를 담은 이벤트 객체라는 것을 생성한다. 추가적인 기능과 정보를 제공하기 위해서 이벤트 핸들러의 첫 아규먼트로는 (일단 파라미터가 있다는 전제가 깔려있기는 한데 아무튼) 이벤트 객체가 자동으로 전달된다.

const myBtn = document.querySelector('#myBtn');

function printEvent(e) {
	console.log(e) ;
	e.target.style.color = 'red';
}

myBtn.addEventListener('click', printEvent);

이벤트 객체에서 자주 쓰는 프로퍼티에는 이벤트 타입을 확인하는 e.type이랑 이벤트가 발생한 노드를 집어오는 e.target 등이 있는 듯하다. addEventListener()와 같은 메소드도 결국은 if (e.type == 'click') {이벤트핸들러();}와 같은 방식으로 발생한 이벤트의 모시깽이를 체크하는 듯하다.

 

캡쳐링과 버블링

부모 요소를 가지고 있는 요소에서 이벤트가 발생되었을 때, 현대의 브라우저들은 두 가지 다른 단계(phase) — 즉, 캡처링(capturing) 단계와 버블링(bubbling) 단계  를 실행한다. 현대의 브라우저들은 기본적으로 이벤트 핸들러를 버블링 단계에 작동하도록 되어있다. 

 

캡처링 단계 : 브라우저는 요소의 가장 바깥쪽의 조상(<html>)이 캡처링 단계에 대해 그것에 등록된 onclick 이벤트 핸들러가 있는지를 확인하기 위해 검사하고, 만약 그렇다면 실행한다. 그리고서 <html>내부에 있는 다음 요소로 이동하고 같은 것을 하고, 그리고서 그 다음 요소로 이동하고, 실제로 선택된 요소에 닿을 때까지 반복한다.

 

버블링 단계 : 캡쳐링 단계와 정확히 반대의 과정이 진행된다. 브라우저는 선택된 요소가 버블링 단계에 대해 그것에 등록된 onclick 이벤트 핸들러를 가지고 있는지 확인하기 위해 검사하고, 만약 그렇다면 실행한다.

그리고서 그것은 바로 다음의 조상 요소로 이동하고 같은 일을 하고, 그리고서 그 다음 요소로 이동하고, <html>요소에 닿을 때까지 반복한다.

 

 

이벤트 위임

어떤 요소의 다수의 자식 요소 중 하나를 선택했을 때 코드가 실행되기를 원한다면, 모든 자식에게 개별적으로 이벤트 리스너를 설정하는 것 대신 이벤트 리스너를 부모 요소에 설정하고, 버블링을 통해 모든 자식 요소에게 코드가 실행될 수 있도록 하는 것이다. 이는 이벤트 핸들러와 연결된 노드를 반환하는 e.currentTarget과 달리, 항상 이벤트가 시작된 곳을 반환하는 e.target의 특성 덕분에 가능한 것이다.

<ul id="to-do-list">
	<li class="item">자바스크립트 공부하기</li>
	<li class="item">체스 공부하기</li>
	<li class="item">독후감 쓰기</li>
</ul>
const toDoList = document.querySelector('#to-do-list');

function updateToDo(e) {
  if (e.target.classList.contains('item')) {
    e.target.classList.toggle('done');
  }
};

toDoList.addEventListener('click', updateToDo);

만약 if문으로 이벤트의 범위를 제약하지 않으면 <ul> 노드에도 done 클래스가 토글될 것이다. dataset 프로퍼티를 활용하면 이런 이벤트 범위 제한을 좀 더 효과적으로 작성할 수 있지 않을까 싶다.

 

 

결론

결국 자바스크립트를 인터랙티브하게 사용하기 위해서는 아래의 세 가지 단계를 거치게 된다. 각 단계를 어떤 프로퍼티와 메소드를 이용해 구현할 것인지는 자유롭게 (혹은 필요에 의해) 결정할 수 있지만, 단계 중 하나를 생략하거나 할 수는 없을 것 같다.

  1. 조작할 노드 불러오기
    • 없는 노드 만들기 : document.createElement("");
    • 있는 노드 찾아오기 : document.getElementById(""); document.querySelerctor(""); 등
    • 찾은 노드 주변 찾기 : node.children; node.parentElement; 등
  2. 불러온 노드 조작하기
    • 지우기 : node.remove();
    • 위치 옮기기 : node.append(); node.before(); 등
    • 노드 내부 마개조 : node.textContent; node.innerHTML; 등
    • 노드 속성 마개조 : node.classList; node.dataset; 등
  3. 조작 시점 결정하기
    • 프로퍼티 : node.onclick;
    • 메소드 : node.addEventListener();

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기