DOM 조작 — querySelector·이벤트 리스너
JS 가 웹페이지를 만지는 첫 단계. 중급 파트의 시작.
이제 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 | 값 변경 + 포커스 잃을 때 |
| submit | form 제출 |
| keydown / keyup | 키 누름/뗌 |
| mouseenter / mouseleave | 호버 진입/이탈 (버블 X) |
| scroll | 스크롤 (passive 권장) |
| DOMContentLoaded | HTML 파싱 끝 (이미지 기다리지 않음) |
| 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
이전: 13편 에러 처리 · 현재: 14편 (중급 시작) · 다음 → 15편 비동기 · 진행: 14/26