자바스크립트 교재 · 14편 / 26편

DOM 조작 — querySelector·이벤트 리스너

JS 가 웹페이지를 만지는 첫 단계. 중급 파트의 시작.

중급읽는 시간 7분2026-05-17
JS 가 HTML 요소를 selector 로 잡고 이벤트로 반응하는 도식

이제 JS 가 진짜 빛나는 영역입니다 — HTML 페이지를 동적으로 바꾸기. 버튼 누르면 색 바뀌고, 입력하면 미리보기가 보이고, 스크롤하면 추가 로드. 14편은 그 기본인 DOM API 의 핵심을 정리합니다 — 중급 파트의 시작.

DOM 이란 — Document Object Model

// HTML 한 줄: <h1 id="title">Hello</h1>
// JS 에서는 객체로 표현됨

const title = document.getElementById("title");
title.textContent;        // "Hello"
title.style.color;        // ""
title.classList;          // DOMTokenList

브라우저가 HTML 을 읽어서 만든 객체 트리가 DOM. JS 는 이 트리를 읽고·수정해서 화면을 바꿉니다.

요소 선택 — querySelector 가 정답

// 옛 방식
document.getElementById("title");          // id 로
document.getElementsByClassName("btn");    // class 로 (HTMLCollection — live)
document.getElementsByTagName("p");        // 태그로

// 현대 표준 — CSS selector 그대로
document.querySelector("#title");          // 첫 매치 1개
document.querySelector(".btn.primary");    // 복합 selector
document.querySelector("ul > li:first-child");
document.querySelectorAll(".item");        // 모두 — NodeList (정적)

// NodeList 는 forEach 가능 (HTMLCollection 은 안 됨)
document.querySelectorAll(".item").forEach(el => {
  el.style.color = "red";
});

한 줄 권장. 거의 모든 경우 querySelector/querySelectorAll. CSS 선택자 그대로 쓸 수 있어 일관성·유연성이 모두 좋습니다. 옛 API 는 안 쓰는 게 정답.

내용 바꾸기 — textContent vs innerHTML

const el = document.querySelector("#msg");

// 텍스트 (안전)
el.textContent = "안녕";
el.textContent;            // 자식 텍스트 다 합쳐 반환

// HTML (XSS 위험)
el.innerHTML = "<b>안녕</b>";   // 태그가 실제로 파싱됨

// 사용자 입력은 절대 innerHTML 직접 X
el.innerHTML = userInput;   // ❌ XSS!
el.textContent = userInput; // ✅ 텍스트로만

XSS 1번지. 외부 데이터(사용자 입력·URL 파라미터·API 응답) 를 innerHTML 에 그대로 넣으면 <script> 태그가 실행될 수 있습니다. 모르면 항상 textContent, HTML 이 꼭 필요하면 sanitize 라이브러리(DOMPurify) 사용.

속성 — getAttribute / dataset / classList

// HTML 속성
img.src;                          // .src 프로퍼티 직접
img.getAttribute("src");          // 같은 결과
img.setAttribute("alt", "텍스트");

// data-* 속성 (커스텀 데이터)
// <div data-user-id="42" data-role="admin">
el.dataset.userId;                // "42" (camelCase 변환)
el.dataset.role;                  // "admin"
el.dataset.newKey = "x";          // 추가도 OK

// classList — 클래스 조작 표준
el.classList.add("active");
el.classList.remove("disabled");
el.classList.toggle("open");      // 있으면 빼고 없으면 더하기
el.classList.contains("active");  // true/false
el.classList.replace("a", "b");

이벤트 — addEventListener

const btn = document.querySelector("#save");

btn.addEventListener("click", (event) => {
  console.log("클릭됨", event.target);
  event.preventDefault();   // 기본 동작 차단 (form submit 등)
});

// 한 번만
btn.addEventListener("click", handler, { once: true });

// 캡처링
btn.addEventListener("click", handler, { capture: true });

// passive — 스크롤·터치 성능
window.addEventListener("scroll", handler, { passive: true });

// 제거 (같은 함수 참조)
btn.removeEventListener("click", handler);
// 화살표 함수를 인라인으로 넣었으면 못 제거 — 항상 이름 있는 함수로

자주 쓰는 이벤트

이벤트발생 시점
click마우스/터치 클릭
input입력값 변경 (every keystroke)
change값 변경 + 포커스 잃을 때
submitform 제출
keydown / keyup키 누름/뗌
mouseenter / mouseleave호버 진입/이탈 (버블 X)
scroll스크롤 (passive 권장)
DOMContentLoadedHTML 파싱 끝 (이미지 기다리지 않음)
load이미지·CSS 까지 다 로드

이벤트 위임 — 동적 요소에 한 번에

// ❌ 안 됨 — 100 개에 각각 리스너
document.querySelectorAll(".todo").forEach(el => {
  el.addEventListener("click", handler);
});
// 새로 추가된 .todo 에는 안 걸림

// ✅ 부모 한 곳에 위임
const list = document.querySelector("#todos");
list.addEventListener("click", (event) => {
  const todo = event.target.closest(".todo");
  if (!todo) return;
  // 어떤 .todo 든 여기로 모임 — 추가/삭제와 무관
  console.log(todo.dataset.id);
});

이벤트 위임의 가치. ① 리스너 1개로 N 개 처리(메모리 절약) ② 동적으로 추가된 요소도 자동으로 잡힘 ③ closest() 가 부모 거슬러 올라가며 selector 매치. React 의 SyntheticEvent 도 이 원리로 동작.

요소 만들기 / 추가 / 제거

// 만들기
const li = document.createElement("li");
li.textContent = "새 할일";
li.classList.add("todo");
li.dataset.id = "42";

// 추가 (뒤에)
list.appendChild(li);

// 위치 지정 (현대)
list.append(li);                  // 여러 개 가능
list.prepend(li);                 // 앞에
existingEl.before(li);            // 형제 앞
existingEl.after(li);             // 형제 뒤

// 제거
li.remove();                      // 자기 자신 제거 (현대)

// 한 번에 HTML — insertAdjacentHTML (innerHTML 보다 안전)
list.insertAdjacentHTML("beforeend", "<li>...</li>");

실전 — 토글 한 화면

const btn = document.querySelector("#toggle");
const menu = document.querySelector("#menu");

btn.addEventListener("click", () => {
  const isOpen = menu.classList.toggle("open");
  btn.setAttribute("aria-expanded", isOpen);
});

// 바깥 클릭하면 닫기
document.addEventListener("click", (event) => {
  if (!btn.contains(event.target) && !menu.contains(event.target)) {
    menu.classList.remove("open");
    btn.setAttribute("aria-expanded", "false");
  }
});

15편 — 비동기 입문 (setTimeout·이벤트 루프)

JS 가 한 줄씩 도는데 어떻게 동시에 보이나 — 이벤트 루프와 콜백.

📚 쉽게 배우는 자바스크립트 교재
이전: 13편 에러 처리 · 현재: 14편 (중급 시작) · 다음 → 15편 비동기 · 진행: 14/26

© 2026 주나이테크(주) @JUNAITECH