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입니다. DOM은 Document Object Model의 약자입니다. JavaScript는 이 트리를 읽고 변경할 수 있으며, 그렇게 하면 보는 페이지가 바로 업데이트됩니다.

중요한 점: DOM은 HTML 파일이 아닙니다. 페이지의 라이브 버전으로 브라우저 메모리에 있습니다. .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"); // 모든 일치, 리스트로

선택자는 전체 CSS이며, 클래스와 id만 아닙니다: 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']");

결과의 두 가지 속성이 알아두기에 가치가 있습니다. 먼저, querySelectorAllNodeList정적입니다: 호출 시점에 찍은 스냅샷이므로, 나중에 페이지에 추가된 요소는 나타나지 않습니다. 이것은 이전 API(getElementsByClassName, getElementsByTagName)와 다르며, 이들은 DOM 변경에 따라 자신을 업데이트하는 라이브 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을 통해 직접 설정할 수도 있으며, setAttribute를 사용하여 src 또는 href 같은 속성을 설정합니다:

js
card.style.color = "crimson"; // 하나의 인라인 스타일
const avatar = document.querySelector(".avatar");
avatar.setAttribute("src", "min-ji.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일 때 끕니다. 이것은 addremove 주변의 if/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 속성(문자열)을 쓰지만, 많은 요소는 또한 다를 수 있는 라이브 속성을 노출합니다. input.value는 사용자가 지금 입력한 것을 반영합니다. input.getAttribute("value")는 마크업의 원본 기본값을 반환합니다. 폼 필드의 경우, 속성이 아닌 속성을 읽으세요.

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); // 페이지에 첨부

어떤 사용자 입력도 마크업을 삽입할 수 없도록 HTML 문자열을 구축하기 전에 textContent로 텍스트를 설정하세요. append는 또한 평문 문자열과 여러 노드를 한 번에 수용합니다(list.append(item, "또는", another)). 이것은 혼합 콘텐츠에 편합니다.

사람들을 잡는 세부 사항: 요소 생성은 자체적으로 눈에 띄는 것을 하지 않습니다. 이미 트리에 있는 것에 첨부될 때까지 메모리에 있습니다. 그때까지는 실제이지만 화면 밖입니다. 이것이 정확히 고급 섹션이 효율적으로 구축하는 데 사용하는 속성입니다.

createElement는 분리된 노드를 구축하고, append는 노드(또는 문자열)를 부모의 마지막 자식으로 삽입하며, remove는 노드를 트리에서 분리합니다. 효율성 질문이 흥미로운 것입니다. 기하학에 영향을 주는 방식으로 DOM을 변경할 때마다, 브라우저가 위치와 크기를 다시 계산해야 할 수 있기 때문입니다(리플로우, 또한 레이아웃이라고도 함). 그것을 루프 내부에서 수행하고 반복당 하나의 노드를 라이브 페이지에 추가하면, 그 작업을 반복해서 유발할 수 있습니다. 해결책은 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)을 읽으면, 브라우저가 대기 중인 레이아웃을 플러시하도록 강제하므로, 루프의 읽기와 쓰기를 인터리브하면 레이아웃 스래시, 반복된 강제 리플로우가 발생합니다. 읽기를 그룹하고, 그 다음 쓰기를 그룹하세요. 몇 개의 노드의 경우 이것은 문제가 아닙니다. 수백 개의 경우 부드러운 것과 버벅거리는 것의 차이입니다.

Juno요소 생성 및 제거하기document.createElement로 새 요소를 구축하고, textContent로 텍스트를 제공하며, append로 페이지에 배치합니다. 신선한 요소는 추가될 때까지 메모리에만 있으므로, 그 단계가 나타나게 합니다. 페이지에서 제거하려면, 그것에 대해 remove를 호출하세요.
Juno요소 생성 및 제거하기createElement가 구축하고, append가 마지막 자식으로 첨부하며, remove가 제거합니다. HTML 문자열을 구축하기 전에 textContent로 텍스트를 설정하여 사용자가 입력한 것이 마크업을 삽입할 수 없도록 합니다. 생성된 요소는 페이지에 이미 있는 것에 첨부될 때까지 보이지 않음을 기억하세요.
Juno요소 생성 및 제거하기 루프에서 라이브 트리에 추가하면 매번 리플로우를 유발할 수 있습니다. DOM 밖의 DocumentFragment에 구축하고 한 번 추가하여 자식이 함께 이동하도록 합니다. 루프 중간에 레이아웃 속성을 읽으면 레이아웃을 강제하고 스래시를 유발하므로, 읽기를 일괄 처리한 다음 쓰기를 일괄 처리하세요. 몇 개의 노드에서는 문제가 없습니다. 수백 개에서는 부드러운 대 버벅거리는 것입니다.

DOM이 여기서 나아가는 곳

선택, 읽기, 변경, 생성, 제거는 JavaScript가 페이지에 하는 대부분의 것을 다루며, 모두 동일한 움직임입니다: 트리의 객체를 찾아 속성을 변경합니다. DOM이 객체이기 때문에, 객체에서 배운 모든 것이 직접 여기에 적용되며, 이 장의 작은 메서드 집합(querySelector, textContent, classList, createElement, append)이 대부분의 도구 키트입니다.

지금까지 빠진 것은 타이밍입니다. 여기의 모든 예제는 한 번 위에서 아래로 실행되지만, 실제 페이지는 독자가 하는 것에 응하여 변경됩니다: 클릭, 키 입력, 제출된 폼. DOM 변경을 그 순간에 연결하는 것은 이벤트이며, 다음 장이고, addEventListener는 "사용자가 이것을 했다"를 "따라서 페이지가 그것을 한다"에 연결합니다. 선택과 변경이 이벤트 응답과 함께 앉으면, 이 핸드북의 나머지가 목표로 하는 대화형 페이지를 구축할 수 있습니다.