JavaScript DOM Manipulation | Control Web Pages Dynamically
이 글의 핵심
JavaScript DOM tutorial: select and update elements, events, delegation, forms, and common pitfalls—querySelector, addEventListener, and production-ready patterns.
Introduction
What is the DOM?
The DOM (Document Object Model) represents an HTML document as a tree. JavaScript can manipulate the DOM to change the page dynamically.
// 실행 예제
<!DOCTYPE html>
<html>
<head>
<title>제목</title>
</head>
<body>
<h1 id="title">안녕하세요</h1>
<p class="content">내용</p>
</body>
</html>
DOM tree:
document
└─ html
├─ head
│ └─ title
└─ body
├─ h1#title
└─ p.content
A closer look at the tree
- document: Entry to the tree.
document.documentElementis<html>;document.bodyis<body>. - Node: Not only elements—text and comments are nodes too. In
<p>안녕</p>,"안녕"is a text node. - Parent, child, sibling: Navigate with
parentElement,children,nextElementSibling, etc. (see «Other selection helpers» below). - Rendering: The browser parses HTML into a DOM and paints with CSS. Changing the DOM (text, attributes, children) updates what you see. In practice, use the Elements panel in DevTools to inspect structure often.
1. Selecting elements
getElementById
// By id (often fastest)
const title = document.getElementById("title");
console.log(title.textContent); // 안녕하세요
querySelector / querySelectorAll
// CSS selector (first match)
const title = document.querySelector("#title");
const content = document.querySelector(".content");
const firstP = document.querySelector("p");
// All matches
const allPs = document.querySelectorAll("p");
console.log(allPs.length); // number of p elements
// Complex selectors
const link = document.querySelector("div.container > a.link");
const items = document.querySelectorAll("ul li:nth-child(odd)");
// Iterate NodeList
allPs.forEach(p => {
console.log(p.textContent);
});
Other selection helpers
// By class
const elements = document.getElementsByClassName("content");
// By tag
const paragraphs = document.getElementsByTagName("p");
// Children
const parent = document.getElementById("parent");
const children = parent.children; // HTMLCollection
const firstChild = parent.firstElementChild;
const lastChild = parent.lastElementChild;
// Siblings
const element = document.getElementById("myElement");
const next = element.nextElementSibling;
const prev = element.previousElementSibling;
// Parent
const parent = element.parentElement;
2. Changing elements
Text
const title = document.getElementById("title");
// textContent: plain text
title.textContent = "새로운 제목";
// innerHTML: parses HTML (watch XSS!)
title.innerHTML = "새로운 <strong>제목</strong>";
// innerText: visible text (layout-aware)
title.innerText = "제목";
innerHTML vs textContent in production
textContent | innerHTML | |
|---|---|---|
| Content | Plain text only | Parses HTML strings into the DOM |
| XSS | User input won’t execute as markup | Untrusted strings can inject scripts |
| Cost | Simple and safe | HTML parse cost + security concerns |
Rule: For user input or API text, default to textContent. If you truly need HTML, sanitize first (e.g. DOMPurify). For dynamic lists, createElement + appendChild is often easier to audit than big innerHTML strings. |
// ✅ Safe: text only
el.textContent = userInput;
// ⚠️ Risky: userInput could contain <script>
el.innerHTML = userInput;
Attributes
const link = document.querySelector("a");
// Read
console.log(link.getAttribute("href"));
// Write
link.setAttribute("href", "https://google.com");
link.setAttribute("target", "_blank");
// Remove
link.removeAttribute("target");
// Direct properties
link.href = "https://google.com";
link.id = "myLink";
link.className = "link active";
// classList
link.classList.add("highlight");
link.classList.remove("active");
link.classList.toggle("selected"); // remove if present, else add
console.log(link.classList.contains("highlight")); // true
Styles
const box = document.getElementById("box");
// Inline styles
box.style.color = "red";
box.style.backgroundColor = "yellow";
box.style.fontSize = "20px";
// Multiple at once
Object.assign(box.style, {
color: "blue",
backgroundColor: "lightgray",
padding: "10px",
borderRadius: "5px"
});
// Computed styles
const styles = window.getComputedStyle(box);
console.log(styles.color); // rgb(0, 0, 255)
3. Creating and removing elements
Creating
// Create
const div = document.createElement("div");
div.textContent = "새 요소";
div.className = "box";
div.id = "newBox";
div.setAttribute("data-id", "123");
// Append
document.body.appendChild(div); // at end of body
// Insert at a position
const container = document.getElementById("container");
const firstChild = container.firstElementChild;
container.insertBefore(div, firstChild); // before first child
// insertAdjacentHTML
container.insertAdjacentHTML("beforeend", "<p>새 단락</p>");
// beforebegin: before element
// afterbegin: before first child
// beforeend: after last child
// afterend: after element
Removing
const element = document.getElementById("myElement");
// Option 1: remove()
element.remove();
// Option 2: removeChild()
const parent = element.parentElement;
parent.removeChild(element);
// Clear all children
const container = document.getElementById("container");
container.innerHTML = ""; // simple but does not remove listeners cleanly
// Or
while (container.firstChild) {
container.removeChild(container.firstChild);
}
Cloning
const original = document.getElementById("original");
// Shallow (no descendants)
const shallowClone = original.cloneNode(false);
// Deep (with descendants)
const deepClone = original.cloneNode(true);
document.body.appendChild(deepClone);
4. Events
Listeners
const button = document.getElementById("myButton");
button.addEventListener("click", function(event) {
console.log("클릭됨!");
console.log("이벤트 타입:", event.type);
console.log("타겟:", event.target);
});
// Arrow function
button.addEventListener("click", (e) => {
console.log("클릭됨!");
});
// Remove listener (same reference)
function handleClick(e) {
console.log("클릭!");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
// Run once
button.addEventListener("click", () => {
console.log("한 번만 실행");
}, { once: true });
Propagation: capturing and bubbling
Events travel the DOM in two phases:
- Capturing:
window→ target (top down). - Target: the element that received the event.
- Bubbling: target →
window(bottom up). Most events bubble. The third argument toaddEventListenercontrols the capture phase.
// 변수 선언 및 초기화
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
outer.addEventListener(
"click",
() => console.log("outer 캡처"),
{ capture: true }
);
inner.addEventListener("click", () => console.log("inner 타깃"));
outer.addEventListener("click", () => console.log("outer 버블"));
// Click inner: typical order — outer capture → inner target → outer bubble
- event.target: the innermost element that originated the event.
- event.currentTarget: the element the handler is bound to (may be a parent when delegating).
Use
event.stopPropagation()only when needed—it blocks parent handlers too.
Common events
// Mouse
element.addEventListener("click", e => {}); // click
element.addEventListener("dblclick", e => {}); // double-click
element.addEventListener("mouseenter", e => {}); // enter
element.addEventListener("mouseleave", e => {}); // leave
element.addEventListener("mousemove", e => {}); // move
// Keyboard
input.addEventListener("keydown", e => {
console.log("키:", e.key);
console.log("코드:", e.code);
if (e.key === "Enter") {
console.log("엔터 입력!");
}
});
input.addEventListener("keyup", e => {});
input.addEventListener("keypress", e => {}); // deprecated
// Forms
form.addEventListener("submit", e => {
e.preventDefault(); // block default submit
console.log("폼 제출");
});
input.addEventListener("input", e => {
console.log("입력값:", e.target.value);
});
input.addEventListener("change", e => {
console.log("변경됨:", e.target.value);
});
// Window
window.addEventListener("load", () => {
console.log("페이지 로드 완료");
});
window.addEventListener("resize", () => {
console.log("창 크기:", window.innerWidth, window.innerHeight);
});
window.addEventListener("scroll", () => {
console.log("스크롤 위치:", window.scrollY);
});
Event object
button.addEventListener("click", (event) => {
console.log(event.type); // click
console.log(event.target); // clicked element
console.log(event.currentTarget); // element with the listener
console.log(event.clientX, event.clientY); // viewport
console.log(event.pageX, event.pageY); // document
event.preventDefault();
event.stopPropagation();
});
Event delegation
// ❌ Listener per row (wasteful for many items)
const items = document.querySelectorAll(".item");
items.forEach(item => {
item.addEventListener("click", () => {
console.log("클릭:", item.textContent);
});
});
// ✅ One listener on parent
const list = document.getElementById("list");
list.addEventListener("click", (e) => {
if (e.target.classList.contains("item")) {
console.log("클릭:", e.target.textContent);
}
});
// Works for new nodes too
const newItem = document.createElement("li");
newItem.className = "item";
newItem.textContent = "새 항목";
list.appendChild(newItem);
5. Practical examples
Example 1: To-do list
<!DOCTYPE html>
<html>
<head>
<title>To-Do 리스트</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
}
.todo-item {
padding: 10px;
margin: 5px 0;
border: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.completed {
text-decoration: line-through;
opacity: 0.6;
}
button {
padding: 5px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>To-Do 리스트</h1>
<input type="text" id="todoInput" placeholder="할 일 입력">
<button id="addBtn">추가</button>
<div id="todoList"></div>
<script>
const input = document.getElementById("todoInput");
const addBtn = document.getElementById("addBtn");
const todoList = document.getElementById("todoList");
// Add todo
function addTodo() {
const text = input.value.trim();
if (!text) {
alert("할 일을 입력하세요!");
return;
}
const todoItem = document.createElement("div");
todoItem.className = "todo-item";
const span = document.createElement("span");
span.textContent = text;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "삭제";
todoItem.appendChild(span);
todoItem.appendChild(deleteBtn);
todoList.appendChild(todoItem);
input.value = "";
input.focus();
}
addBtn.addEventListener("click", addTodo);
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
addTodo();
}
});
// Delegation: complete / delete
todoList.addEventListener("click", (e) => {
const todoItem = e.target.closest(".todo-item");
if (e.target.tagName === "SPAN") {
todoItem.classList.toggle("completed");
} else if (e.target.tagName === "BUTTON") {
todoItem.remove();
}
});
</script>
</body>
</html>
Example 2: Tabs
<!DOCTYPE html>
<html>
<head>
<title>탭 UI</title>
<style>
.tabs {
display: flex;
border-bottom: 2px solid #ddd;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: none;
}
.tab.active {
border-bottom: 3px solid #007bff;
color: #007bff;
}
.tab-content {
padding: 20px;
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="tabs">
<button class="tab active" data-tab="tab1">탭 1</button>
<button class="tab" data-tab="tab2">탭 2</button>
<button class="tab" data-tab="tab3">탭 3</button>
</div>
<div id="tab1" class="tab-content active">
<h2>탭 1 내용</h2>
<p>첫 번째 탭입니다.</p>
</div>
<div id="tab2" class="tab-content">
<h2>탭 2 내용</h2>
<p>두 번째 탭입니다.</p>
</div>
<div id="tab3" class="tab-content">
<h2>탭 3 내용</h2>
<p>세 번째 탭입니다.</p>
</div>
<script>
const tabs = document.querySelectorAll(".tab");
const contents = document.querySelectorAll(".tab-content");
tabs.forEach(tab => {
tab.addEventListener("click", () => {
tabs.forEach(t => t.classList.remove("active"));
contents.forEach(c => c.classList.remove("active"));
tab.classList.add("active");
const targetId = tab.getAttribute("data-tab");
document.getElementById(targetId).classList.add("active");
});
});
</script>
</body>
</html>
Example 3: Modal
<!DOCTYPE html>
<html>
<head>
<title>모달</title>
<style>
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 10px;
max-width: 500px;
}
.close {
float: right;
cursor: pointer;
font-size: 24px;
}
</style>
</head>
<body>
<button id="openModal">모달 열기</button>
<div id="modal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h2>모달 제목</h2>
<p>모달 내용입니다.</p>
</div>
</div>
<script>
const openBtn = document.getElementById("openModal");
const modal = document.getElementById("modal");
const closeBtn = document.querySelector(".close");
openBtn.addEventListener("click", () => {
modal.classList.add("active");
});
closeBtn.addEventListener("click", () => {
modal.classList.remove("active");
});
modal.addEventListener("click", (e) => {
if (e.target === modal) {
modal.classList.remove("active");
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
modal.classList.remove("active");
}
});
</script>
</body>
</html>
Example 4: Dynamic list (add/delete)
Build <li> nodes from input; attach delete with createElement or use delegation. Below uses delegation only.
<ul id="itemList"></ul>
<input type="text" id="itemInput" placeholder="항목">
<button type="button" id="addBtn">추가</button>
<script>
const list = document.getElementById("itemList");
const input = document.getElementById("itemInput");
const addBtn = document.getElementById("addBtn");
function addItem() {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text; // text only (avoid HTML injection)
const del = document.createElement("button");
del.type = "button";
del.textContent = "삭제";
del.dataset.action = "delete";
li.appendChild(del);
list.appendChild(li);
input.value = "";
}
addBtn.addEventListener("click", addItem);
list.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-action='delete']");
if (!btn) return;
btn.closest("li")?.remove();
});
</script>
Example 5: Client-side form validation
Use HTML5 required, pattern, type=“email” for first-line checks; JavaScript can show messages and manage focus. Always validate again on the server.
<form id="signup" novalidate>
<label>
이메일
<input type="email" id="email" name="email" required>
</label>
<span id="emailError" class="error" aria-live="polite"></span>
<label>
비밀번호 (8자 이상)
<input type="password" id="password" name="password" minlength="8" required>
</label>
<span id="pwError" class="error" aria-live="polite"></span>
<button type="submit">가입</button>
</form>
<script>
const form = document.getElementById("signup");
const email = document.getElementById("email");
const password = document.getElementById("password");
const emailError = document.getElementById("emailError");
const pwError = document.getElementById("pwError");
function validateEmailField() {
emailError.textContent = "";
if (!email.validity.valid) {
emailError.textContent = email.validationMessage || "이메일 형식을 확인하세요.";
return false;
}
return true;
}
function validatePasswordField() {
pwError.textContent = "";
if (password.value.length < 8) {
pwError.textContent = "비밀번호는 8자 이상이어야 합니다.";
return false;
}
return true;
}
email.addEventListener("blur", validateEmailField);
password.addEventListener("blur", validatePasswordField);
form.addEventListener("submit", (e) => {
const okEmail = validateEmailField();
const okPw = validatePasswordField();
if (!okEmail || !okPw) {
e.preventDefault();
return;
}
console.log({ email: email.value });
});
</script>
6. Forms
Form events
<form id="myForm">
<input type="text" id="username" name="username" required>
<input type="email" id="email" name="email" required>
<input type="password" id="password" name="password" required>
<button type="submit">제출</button>
</form>
<script>
const form = document.getElementById("myForm");
form.addEventListener("submit", (e) => {
e.preventDefault(); // avoid full page reload
const formData = new FormData(form);
const data = {
username: formData.get("username"),
email: formData.get("email"),
password: formData.get("password")
};
console.log(data);
const username = document.getElementById("username").value;
const email = document.getElementById("email").value;
if (username.length < 3) {
alert("사용자명은 3자 이상이어야 합니다");
return;
}
fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => console.log(result))
.catch(error => console.error(error));
});
const username = document.getElementById("username");
username.addEventListener("input", (e) => {
const value = e.target.value;
if (value.length < 3) {
username.style.borderColor = "red";
} else {
username.style.borderColor = "green";
}
});
</script>
7. Common mistakes
Mistake 1: touching the DOM before it exists
// ❌ Runs before HTML is parsed
const button = document.getElementById("myButton"); // null!
button.addEventListener("click", () => {}); // TypeError
// ✅ Wait for DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById("myButton");
button.addEventListener("click", () => {
console.log("클릭!");
});
});
// ✅ Or put <script> at the end of <body>
Mistake 2: clearing innerHTML drops listeners
// ❌ innerHTML rebuilds subtree — listeners are gone
const container = document.getElementById("container");
const button = document.createElement("button");
button.textContent = "클릭";
button.addEventListener("click", () => console.log("클릭!"));
container.appendChild(button);
container.innerHTML = ""; // listeners removed
// ✅ removeChild loop
while (container.firstChild) {
container.removeChild(container.firstChild);
}
Mistake 3: event bubbling surprises
// Bubbling example
<div id="parent">
<button id="child">버튼</button>
</div>
document.getElementById("parent").addEventListener("click", () => {
console.log("부모 클릭");
});
document.getElementById("child").addEventListener("click", (e) => {
console.log("자식 클릭");
// e.stopPropagation();
});
// Click button:
// 자식 클릭
// 부모 클릭 (bubble)
8. Practice problems
Problem 1: Counter
Build increment/decrement buttons around a number.
<div id="counter">
<button id="decrease">-</button>
<span id="count">0</span>
<button id="increase">+</button>
</div>
<script>
let count = 0;
const countSpan = document.getElementById("count");
document.getElementById("increase").addEventListener("click", () => {
count++;
countSpan.textContent = count;
});
document.getElementById("decrease").addEventListener("click", () => {
count--;
countSpan.textContent = count;
});
</script>
Problem 2: Dynamic list
Add items from input and delete them.
<input type="text" id="itemInput">
<button id="addItem">추가</button>
<ul id="itemList"></ul>
<script>
const input = document.getElementById("itemInput");
const addBtn = document.getElementById("addItem");
const list = document.getElementById("itemList");
function addItem() {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.innerHTML = `
${text}
<button class="delete">삭제</button>
`;
list.appendChild(li);
input.value = "";
}
addBtn.addEventListener("click", addItem);
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") addItem();
});
list.addEventListener("click", (e) => {
if (e.target.classList.contains("delete")) {
e.target.parentElement.remove();
}
});
</script>
Summary
Key takeaways
- Selection:
getElementById(): by idquerySelector(): CSS selectorquerySelectorAll(): all matches
- Updates:
- Text:
textContent(default),innerHTML(trusted input only) - Attributes:
getAttribute(),setAttribute() - Style:
style,classList
- Text:
- Create/remove:
- Create:
createElement() - Insert:
appendChild(),insertBefore() - Remove:
remove(),removeChild()
- Create:
- Events:
- Add:
addEventListener() - Remove:
removeEventListener() - Delegation: listen on an ancestor
- Phases: capturing (
capture: true) and bubbling
- Add:
- Event object:
event.target: origin elementevent.preventDefault(): cancel default actionevent.stopPropagation(): stop propagation
Best practices
- ✅ Prefer
querySelectorfor flexibility - ✅ Use delegation for dynamic lists
- ✅ Wait for
DOMContentLoaded(or place scripts at the bottom) - ✅ Prefer
createElementover untrustedinnerHTML - ✅ Remove or avoid duplicate listeners when components unmount
Next steps
Related posts
- JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
- C++ Kafka 고급 활용 | 스트림 처리·트랜잭션·정확히 한 번 전달 완벽 가이드 [#52-6]
- C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]
- C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
- HTML/CSS 시작하기 | 웹 개발 첫걸음
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. JavaScript DOM tutorial: select and update elements, events, delegation, forms, and common pitfalls—querySelector, addEv… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. JavaScript 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [JavaScript Async Programming](/en/blog/javascript-series-05-async/
- [JavaScript Classes | ES6 Class Syntax Explained](/en/blog/javascript-series-07-classes/
- JavaScript DOM 조작 | 웹 페이지 동적으로 제어하기
이 글에서 다루는 키워드 (관련 검색어)
JavaScript, DOM, Events, querySelector, addEventListener, Web 등으로 검색하시면 이 글이 도움이 됩니다.