Skip to content
This page has been auto-translated and may contain errors.View in English

DOM

docs.scrimba.com

HTML 파일은 한 번 로드되고, 그 뒤로 페이지는 그대로 머물러 있습니다. 하지만 입력할 때마다 검색 결과를 제안해주는 검색창, 클릭하면 열리는 메뉴, 숫자가 올라가는 카운터 같은 것들은 HTML 파일 안에는 없습니다. 페이지가 로드된 뒤에 화면 위의 내용을 바꾸는 무언가가 있다면, 그것은 바로 JavaScript가 DOM을 통해 페이지에 손을 뻗은 것입니다. 이 장에서는 그 손길에 대해 다룹니다. JavaScript가 페이지의 부분들을 찾아서 바꾸고, 그 결과 독자의 눈앞에서 페이지가 업데이트되는 방식을 살펴봅니다.

DOM이란 무엇인가

브라우저가 페이지를 로드하면 HTML을 읽어서 그로부터 **객체들의 트리**를 만들어냅니다. 페이지 전체를 위한 객체 하나, <body>를 위한 객체 하나, 그리고 그 안의 제목, 문단, 버튼마다 각각 하나씩요. 이 트리가 바로 DOM(Document Object Model, 문서 객체 모델)입니다. JavaScript는 이 트리를 읽고 바꿀 수 있으며, 그렇게 하면 눈앞의 페이지가 곧바로 업데이트됩니다.

여기서 꼭 기억해야 할 점은, DOM이 곧 HTML 파일은 아니라는 것입니다. DOM은 브라우저의 메모리 안에 존재하는, 페이지의 살아있는 버전입니다. .html 파일은 처음 준비한 조리법이고, DOM은 브라우저가 실제로 내놓은 완성된 요리인데, JavaScript는 사람들이 그 요리를 먹고 있는 동안에도 접시 위 내용을 다시 배치할 수 있습니다.

DOM(Document Object Model)은 브라우저가 여러분의 HTML로부터 만들어내는 **메모리 상의 트리**입니다. 모든 요소는 태그가 중첩된 방식 그대로 중첩된 객체가 됩니다. <body> 안에 <main>이 있고, 그 안에 <section>이 있고, 그 안에 문단이 있는 식입니다. JavaScript는 전역 document를 통해 이 트리를 손에 넣고, 거기서부터 트리를 순회하며 어떤 요소든 읽고 바꿀 수 있습니다.

나중에 헷갈리지 않으려면 알아둘 구분점이 있습니다. DOM은 원본 HTML과는 별개라는 것입니다. HTML은 트리를 만들기 위해 한 번 파싱되고, 그 이후로는 트리가 실제로 살아있는 존재가 됩니다. DOM을 바꾸면 페이지가 업데이트되지만, 디스크에 있는 원본 HTML 파일 자체는 절대 바뀌지 않습니다. 그래서 JavaScript가 실행된 뒤에는 "페이지 소스 보기"로 본 내용이 화면에 보이는 것과 전혀 다를 수 있는 것입니다.

DOM은 문서에 대한 브라우저의 살아있는 객체 모델입니다. 요소, 텍스트, 주석, 그리고 문서 자체로 이루어진 **노드**들의 트리로서, 브라우저가 HTML을 파싱해서 만들고 이를 바탕으로 렌더링합니다. 이는 언어에 종속되지 않는 API로, document와 그에 매달린 객체들을 통해 JavaScript에 노출되지만 JavaScript 언어 자체의 일부는 아닙니다. 즉, 런타임이 제공하는 브라우저 기능입니다.

실무에서 중요한 관점은 이것입니다: HTML 소스는 입력값으로서 트리로 파싱되는 것은 한 번뿐이며, 그 이후로는 DOM이 진실의 원천이 됩니다. 스크립트가 실행된 뒤에 document.body.innerHTML을 읽으면 여러분이 배포한 바이트가 아니라 현재 시점에서 직렬화된 트리를 얻게 됩니다. 이것이 바로 DOM이 HTML에서 곧바로 나오는 이유이기도 합니다. 여러분이 작성하는 태그가 초기 트리를 정의하고, 그 이후 JavaScript가 하는 모든 일은 그 트리를 편집하는 것입니다. DOM을 조작하는 데는 소스 자체에는 없는 비용이 따르는데, 이는 변경 사항이 브라우저로 하여금 레이아웃을 재계산하고 다시 그리게 만들 수 있기 때문입니다. 이 흐름은 아래 심화 섹션에서 계속 따라가 보겠습니다.

DOM 안의 모든 요소는 객체에서 배운 것과 같은 종류의 객체이며, 읽고 바꿀 수 있는 속성을 가지고 있습니다. 요령은 바로 이것입니다. 페이지가 객체들로 이루어져 있고, 여러분은 이미 객체를 다루는 법을 알고 있으니, 페이지를 바꾸는 것은 곧 객체의 속성을 바꾸는 것입니다.

JunoDOM이란 무엇인가 브라우저는 HTML을 DOM이라는 객체 트리로 바꾸고, 그 트리가 바로 살아있는 페이지입니다. HTML 파일은 조리법이고, DOM은 식탁에 차려진 음식이며, JavaScript는 음식이 식탁 위에 있는 동안에도 그것을 바꿀 수 있습니다. 모든 요소는 객체이므로, 페이지를 바꾼다는 것은 곧 객체의 속성을 바꾼다는 뜻입니다.
JunoDOM이란 무엇인가 DOM은 브라우저가 HTML로부터 만들어내는, document를 통해 접근하는 메모리 상의 트리입니다. 원본 파일과는 별개입니다. 한 번 파싱된 뒤로는 트리가 실제로 살아있는 존재가 되기 때문에, "페이지 소스 보기"가 눈에 보이는 것과 다를 수 있습니다. 트리를 바꾸면 페이지가 업데이트됩니다.
JunoDOM이란 무엇인가 DOM은 브라우저가 HTML로부터 파싱해서 만들고 이를 바탕으로 렌더링하는 노드들의 트리이며, 언어의 일부가 아니라 브라우저 API입니다. 소스는 한 번 파싱되는 입력값이고, 그 이후로는 트리가 진실이 되므로 innerHTML은 배포한 바이트가 아니라 현재 트리를 반영합니다. 편집에는 레이아웃과 리페인트라는 비용이 따를 수 있으며, 이 때문에 나중에 쓰기 패턴이 중요해집니다.

요소 선택하기

요소를 바꾸기 전에, 먼저 트리 안에서 그것을 찾아야 합니다. CSS와 같은 방식으로 셀렉터를 사용해 요소를 가리키면, 브라우저가 그에 맞는 객체를 돌려줍니다.

가장 많이 쓰는 것은 document.querySelector입니다. CSS 셀렉터, 즉 CSS에서 쓰는 것과 같은 패턴을 넘겨주면 처음으로 일치하는 요소를 반환합니다.

js
const card = document.querySelector(".card"); // class="card"인 첫 번째 요소
const total = document.querySelector("#total"); // id="total"인 요소

.card는 클래스로 매칭하고, #total은 id로 매칭하며, "button"처럼 그냥 단어를 쓰면 태그 이름으로 매칭합니다. querySelector는 첫 번째로 일치하는 요소만 반환하고 거기서 멈춥니다. 일치하는 요소를 모두 원한다면 querySelectorAll을 쓰면 되는데, 이는 순회할 수 있는 목록을 반환합니다.

js
const cards = document.querySelectorAll(".card"); // class="card"인 모든 요소
console.log(cards.length); // 몇 개를 찾았는지

일치하는 것이 없으면 querySelectornull을 반환하는데, 이는 JavaScript가 "여기에는 아무것도 없다"고 말하는 방식입니다.

document.querySelector는 어떤 CSS 셀렉터든 받아서 처음 일치하는 요소를 반환하거나, 일치하는 것이 없으면 null을 반환합니다. document.querySelectorAll은 일치하는 모든 요소를 목록으로 반환합니다.

js
const toggle = document.querySelector(".menu-toggle"); // 첫 번째 일치, 없으면 null
const items = document.querySelectorAll(".card"); // 일치하는 모든 것, 목록으로

이 셀렉터는 클래스와 id뿐 아니라 CSS 전체를 지원합니다. querySelector("nav a")는 nav 안의 첫 번째 링크를 찾고, querySelector("input[type='email']")은 속성으로 매칭합니다. 스타일시트에서 요소를 선택할 수 있는 방법이라면 여기서도 그대로 통합니다.

미리 대비해둬야 할 것은 null 반환값입니다. 일치하는 게 없으면 null이 되기 때문에, document.querySelector(".missing").textContent처럼 작성한 코드는 오류를 던집니다. null의 속성을 읽을 수는 없기 때문입니다. 요소가 존재하지 않을 수도 있는 상황이라면 먼저 확인하세요: if (toggle) { ... }.

document.querySelector(selector)는 문서 순서상 CSS 셀렉터에 처음으로 일치하는 요소를 반환하거나, 없으면 null을 반환합니다. document.querySelectorAll(selector)는 일치하는 모든 요소를 NodeList로 반환합니다. 둘 다 CSS 셀렉터 문법을 전부 지원하므로, 결합자, 속성 셀렉터, 가상 클래스 모두 사용할 수 있습니다.

js
const firstError = document.querySelector(".form .field.is-invalid input");
const externalLinks = document.querySelectorAll("a[href^='http']");

결과값의 두 가지 특성을 알아둘 필요가 있습니다. 첫째, querySelectorAll이 반환하는 NodeList정적입니다. 호출 시점에 찍힌 스냅샷이므로, 그 뒤에 페이지에 추가된 요소는 여기에 나타나지 않습니다. 이는 더 오래된 API들(getElementsByClassName, getElementsByTagName)과 다른 점인데, 이들은 DOM이 바뀔 때마다 스스로 업데이트되는 라이브(live) HTMLCollection을 반환합니다. 이 차이는 마지막 섹션에서 더 자세히 다룹니다. 둘째, 범위 지정은 성능과 정확성 모두에 영향을 줍니다. document가 아니라 특정 요소에 대해 querySelector를 호출하면(card.querySelector(".title")) 그 요소의 하위 트리 안에서만 검색하므로, 큰 페이지에서 더 빠르고 다른 곳의 무언가와 잘못 매칭될 일도 없습니다. 이미 컨테이너를 가지고 있다면 범위가 지정된 형태를 쓰는 게 좋습니다.

Juno요소 선택하기 요소를 찾으려면 document.querySelector.card#total 같은 CSS 셀렉터를 넘기세요. 그러면 첫 번째로 일치하는 요소를 돌려줍니다. 전부를 목록으로 원한다면 querySelectorAll을 쓰세요. 일치하는 게 없으면 "여기에는 아무것도 없다"는 뜻의 null을 받게 됩니다.
Juno요소 선택하기querySelector는 첫 번째 일치 요소를, querySelectorAll은 일치하는 모든 요소를 목록으로 반환하며, 둘 다 스타일시트에서 쓸 법한 어떤 CSS 셀렉터든 받습니다. 일치하지 않으면 null이 반환되므로 그 값의 속성을 읽으면 오류가 발생합니다. 요소가 없을 수도 있다면 if (element)로 방어하세요.
Juno요소 선택하기 두 메서드 모두 CSS 셀렉터를 전부 지원하며, querySelectorAll은 이후에 추가된 요소를 반영하지 않는 정적인 NodeList 스냅샷을 돌려줍니다. document가 아니라 특정 요소에서 querySelector를 호출하면 그 하위 트리만 검색하므로 더 빠르고 페이지 다른 곳과 잘못 매칭되지 않습니다. 그리고 일치하지 않으면 null이라는 점을 잊지 마세요.

요소 읽고 바꾸기

일단 요소를 손에 넣었다면, 다른 객체를 읽고 바꿀 때와 마찬가지로 그 속성을 통해 읽고 바꿉니다. 여기서부터 페이지가 실제로 움직이기 시작합니다.

가장 많이 쓰게 될 속성은 요소 안의 텍스트를 나타내는 textContent입니다. 읽을 수도, 설정할 수도 있습니다.

js
const total = document.querySelector("#total");
total.textContent = "42 items"; // 이제 페이지에 "42 items"가 보입니다

무언가의 모양을 바꿀 때는 보통 클래스를 추가하거나 제거해서 CSS가 스타일을 처리하게 둡니다. classList에는 그럴 때 쓰는 메서드가 세 가지 있습니다.

js
const card = document.querySelector(".card");
card.classList.add("selected"); // 클래스 추가
card.classList.remove("selected"); // 클래스 제거
card.classList.toggle("selected"); // 없으면 추가하고, 있으면 제거

.style을 통해 단일 스타일을 직접 설정할 수도 있고, setAttributesrchref 같은 속성을 설정할 수도 있습니다.

js
card.style.color = "crimson"; // 인라인 스타일 하나
const avatar = document.querySelector(".avatar");
avatar.setAttribute("src", "jimin.jpg"); // 이미지 소스 설정

classList를 먼저 쓰고, .style은 일회성 조정에만 쓰세요. 스타일링을 CSS에 몰아두면 한곳에서 관리할 수 있습니다.

textContent는 요소 안의 텍스트를 읽고 씁니다. classList는 클래스를 토글해서 스타일링이 CSS 안에 남아있도록 해줍니다. .style은 개별 인라인 스타일을 설정하고, setAttribute는 어떤 속성이든 설정합니다.

js
const badge = document.querySelector(".badge");
badge.textContent = `${count} new`; // 텍스트 설정
badge.classList.toggle("is-hidden", count === 0); // 조건과 함께 토글
badge.setAttribute("aria-label", `${count} new messages`); // 속성 설정

classList.toggle은 두 번째 인자로 불리언을 선택적으로 받는데, true면 클래스를 강제로 켜고 false면 강제로 끕니다. addremoveif/else로 감싸는 것보다 깔끔합니다.

미리 짚어둘 주의사항이 있습니다. innerHTML이라는 것도 있는데, 이는 요소의 내용을 태그를 포함한 순수 HTML로 설정합니다. 유연해 보여서 손이 가기 쉽지만, 사용자로부터 온 텍스트라면 innerHTML이 아니라 textContent를 쓰세요. 사용자 입력으로 innerHTML을 설정하면 그 입력이 실제 HTML을 페이지에 주입할 수 있게 되는데, 이는 보안 취약점입니다. 어차피 순수 텍스트를 설정하는 상황이라면 textContent가 맞는 도구입니다.

읽고 쓸 수 있는 범위는 넓지만, 대부분의 작업은 네 가지 속성이 담당합니다: 텍스트는 textContent, 상태 클래스는 classList, 계산된 일회성 스타일은 .style, 속성은 setAttribute입니다. .style보다는 classList를 우선해서, 표현은 CSS에 맡기고 JavaScript는 상태 클래스(스타일시트가 스타일을 적용하는 is-open이나 is-loading 같은 클래스)만 전환하게 하세요.

js
const panel = document.querySelector(".panel");
panel.classList.toggle("is-open", shouldOpen);
panel.setAttribute("aria-expanded", String(shouldOpen)); // 속성은 문자열입니다

주의해야 할 지점은 innerHTML입니다. 이 값을 설정하면 문자열을 HTML로 파싱해서 실제 노드를 만들어내므로, 그 문자열 안의 마크업은 무엇이든 실제로 살아있는 코드가 됩니다. 사용자가 입력한 텍스트 안의 <script>onerror 핸들러가 페이지 안에서 실행되는 것입니다. 이것이 바로 크로스 사이트 스크립팅(XSS)입니다. 텍스트로 취급했던 데이터를 통해 실행 가능한 콘텐츠를 주입하는 공격입니다. textContent는 HTML을 절대 파싱하지 않고 텍스트만, 오직 텍스트만 설정하므로 더 안전한 기본 선택이며 더 빠르기도 합니다. 브라우저가 HTML 파싱 단계를 건너뛰기 때문입니다. innerHTML은 완전히 여러분이 통제하는 마크업으로만 제한하고, 사용자 입력을 이어붙여서 만들지 마세요. 구조화된 마크업에 사용자 데이터를 넣어야 한다면, 안전한 방법으로 구조를 만든 다음 신뢰할 수 없는 값은 textContent로 끼워 넣으세요. 이것이 바로 다음 섹션에서 다룰 패턴입니다.

속성에 대해 한 가지 더 짚어두겠습니다. setAttribute는 HTML 속성(문자열)을 씁니다. 하지만 많은 요소는 이와 다를 수 있는 라이브 **속성(property)**도 함께 노출합니다. input.value는 사용자가 지금 이 순간 입력한 값을 반영하고, input.getAttribute("value")는 마크업에 있던 원래 기본값을 반환합니다. 폼 필드라면 속성(attribute)이 아니라 프로퍼티(property)를 읽으세요.

Juno요소 읽고 바꾸기textContent로 요소 안의 텍스트를 설정하고, classList.add, remove, toggle로 클래스를 추가하거나 제거해서 모양을 바꾸세요. 일회성 조정에는 .style을, 이미지 소스 같은 것에는 setAttribute를 쓰세요. 클래스에 의존하면 스타일링이 CSS 안에 그대로 남습니다.
Juno요소 읽고 바꾸기 텍스트는 textContent, 상태 클래스는 classList, 일회성 스타일은 .style, 속성은 setAttribute를 쓰세요. classList.toggle은 불리언을 받아 강제로 켜거나 끌 수도 있습니다. 사용자가 입력한 것에는 innerHTML을 피하세요. 실제 HTML을 페이지에 주입하게 되니까요. 순수 텍스트라면 어차피 textContent가 맞는 도구입니다.
Juno요소 읽고 바꾸기classList로 상태 클래스를 전환하고 스타일링은 CSS에 맡기세요. 속성에는 setAttribute를 쓰되 이는 문자열이라는 점을 기억하세요. innerHTML은 문자열을 살아있는 HTML로 파싱하므로 그 안의 사용자 데이터는 XSS 취약점이 됩니다. textContent는 파싱하지 않으므로 안전하고 더 빠릅니다. 그리고 폼 필드는 속성이 아니라 라이브 value 프로퍼티를 읽으세요.

요소 만들고 지우기

이미 존재하는 요소를 바꾸는 것만으로도 많은 것을 할 수 있지만, 때로는 아직 아무것도 없는 상황도 있습니다. 새로 추가하는 할 일 항목이나 도착하는 검색 결과처럼요. 이럴 때는 새 요소를 만들어 트리에 넣고, 오래된 요소는 빼내야 합니다.

document.createElement로 새 요소를 만들고, 내용을 채운 다음, append로 페이지에 추가합니다.

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li"); // 새 <li>, 아직 페이지에는 없음
item.textContent = "오트밀크 사기"; // 텍스트 채우기
list.append(item); // 이제 목록 맨 끝에 나타남

createElement로 만든 새 요소는 어딘가에 append하기 전까지는 메모리 안에만 존재합니다. 페이지에 나타나게 하는 것은 바로 그 단계입니다. 요소를 페이지에서 없애려면 그 요소에 remove를 호출하면 됩니다.

js
item.remove(); // 페이지에서 사라짐

document.createElement(tag)는 요소를 만들고, append는 그것을 부모의 마지막 자식으로 추가하며, remove는 요소를 트리에서 빼냅니다.

js
const list = document.querySelector(".todo-list");
const item = document.createElement("li");
item.textContent = "오트밀크 사기"; // 안전함: HTML로 파싱되지 않는 텍스트
item.classList.add("todo-item");
list.append(item); // 페이지에 붙이기

append하기 전에 textContent로 텍스트를 설정하세요. HTML 문자열을 직접 만드는 방식이 아니라요. 그러면 사용자가 입력한 텍스트가 마크업을 주입할 수 없게 됩니다. append는 순수 문자열이나 여러 노드를 한 번에 받을 수도 있어서(list.append(item, "or", another)), 혼합된 콘텐츠를 다룰 때 편리합니다.

사람들이 흔히 놓치는 점이 있습니다. 요소를 만드는 것만으로는 화면에 아무런 변화도 일어나지 않습니다. 이미 트리에 있는 무언가에 붙이기 전까지는 메모리 안에 그대로 머물러 있습니다. 그전까지 이 요소는 실재하지만 화면 밖에 있으며, 심화 섹션에서는 바로 이 특성을 이용해 효율적으로 작업하는 법을 다룹니다.

createElement는 트리에서 떨어진 노드를 만들고, append는 노드(또는 문자열)를 부모의 마지막 자식으로 삽입하며, remove는 노드를 트리에서 분리합니다. 흥미로운 지점은 효율성 문제인데, 살아있는 트리에 삽입하는 작업에는 비용이 따를 수 있기 때문입니다.

기하학적 특성에 영향을 주는 방식으로 DOM을 바꿀 때마다, 브라우저는 위치와 크기를 다시 계산해야 할 수 있고(리플로우(reflow), 레이아웃이라고도 부릅니다) 그다음 다시 그려야 합니다. 이런 작업을 반복문 안에서, 각 반복마다 살아있는 페이지에 노드 하나씩 추가하는 식으로 하면 그 작업이 반복적으로 발생할 수 있습니다. 해법은 DOM 밖에서 만들어두고 한 번에 삽입하는 것입니다. DocumentFragment는 정확히 이런 용도를 위한 가볍고 화면 밖에 있는 컨테이너입니다. 노드를 이 프래그먼트에 추가하고(프래그먼트는 살아있는 트리 안에 없으므로 리플로우가 없습니다), 그다음 프래그먼트를 한 번에 추가하면, 그 자식들이 단 한 번의 작업으로 옮겨갑니다.

js
const list = document.querySelector(".results");
const fragment = document.createDocumentFragment(); // 화면 밖 컨테이너

for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name; // 사용자 데이터의 안전한 삽입
  fragment.append(li); // 리플로우 없음: 프래그먼트는 DOM 밖에 있음
}

list.append(fragment); // 살아있는 트리에 단 한 번 삽입

프래그먼트를 추가하면 그 내용이 부모 안으로 비워지므로, 프래그먼트 자체는 DOM에 나타나지 않고 그 자식들만 나타납니다. 같은 원리가 더 넓은 습관으로 이어집니다: DOM 쓰기 작업을 묶어서 처리하는 것입니다. offsetHeight 같은 레이아웃 속성을 읽으면 브라우저가 대기 중인 레이아웃을 강제로 처리(flush)하게 되므로, 반복문 안에서 읽기와 쓰기를 섞어서 하면 레이아웃 스래싱(layout thrash), 즉 반복적인 강제 리플로우가 발생합니다. 읽기는 한데 모으고, 쓰기도 한데 모으세요. 노드가 몇 개뿐이라면 이런 것이 별 의미가 없지만, 수백 개라면 부드러움과 끊김의 차이를 만듭니다.

Juno요소 만들고 지우기document.createElement로 새 요소를 만들고, textContent로 텍스트를 채우고, append로 페이지에 추가하세요. 새로 만든 요소는 append하기 전까지 메모리 안에만 있으므로, 그 단계가 바로 화면에 나타나게 만드는 단계입니다. 페이지에서 없애려면 그 요소에 remove를 호출하세요.
Juno요소 만들고 지우기createElement가 요소를 만들고, append가 마지막 자식으로 붙이고, remove가 빼냅니다. HTML 문자열을 만드는 대신 append하기 전에 textContent로 텍스트를 설정하세요. 그러면 사용자가 입력한 것이 마크업을 주입할 수 없습니다. 만든 요소는 이미 페이지에 있는 무언가에 붙이기 전까지 보이지 않는다는 점을 기억하세요.
Juno요소 만들고 지우기 살아있는 트리에 반복문으로 추가하면 매번 리플로우가 발생할 수 있습니다. DocumentFragment 안에서 DOM 밖으로 만들어두고 한 번에 추가해서 자식들이 함께 옮겨가게 하세요. 반복문 중간에 레이아웃 속성을 읽으면 강제로 레이아웃이 처리되어 스래싱이 일어나므로, 읽기를 모으고 그다음 쓰기를 모으세요. 노드가 몇 개뿐이라면 의미 없지만, 수백 개라면 부드러움과 끊김의 차이입니다.

DOM, 여기서부터는 어디로 가야 할까요

선택하고, 읽고, 바꾸고, 만들고, 지우는 것이 JavaScript가 페이지에 하는 일의 대부분을 차지하며, 그 모든 것은 밑바탕에서 보면 동일한 동작입니다: 트리에서 객체를 찾아 그 속성을 바꾸는 것입니다. DOM이 곧 객체들이기 때문에, 객체에서 배운 모든 것이 여기에도 그대로 적용되며, 이 장에서 다룬 작은 메서드 모음(querySelector, textContent, classList, createElement, append)이 도구상자의 대부분을 차지합니다.

지금까지 빠져 있는 것은 타이밍입니다. 여기 나온 예제는 모두 위에서 아래로 한 번만 실행되지만, 실제 페이지는 독자가 하는 행동, 클릭, 키 입력, 제출된 폼에 반응해서 바뀝니다. DOM 변경을 이런 순간들에 연결하는 것이 다음 장인 이벤트이며, 여기서 addEventListener는 "사용자가 이것을 했다"와 "그래서 페이지가 저것을 한다"를 이어줍니다. 선택하고 바꾸는 것에 이벤트에 반응하는 능력까지 더해지면, 이 안내서가 목표로 하는 인터랙티브한 페이지를 만들 수 있게 됩니다.