JavaScript DOM Manipulation | Control Web Pages Dynamically

JavaScript DOM Manipulation | Control Web Pages Dynamically

이 글의 핵심

Hands-on DOM guide: querying nodes, changing text and attributes, creating elements, handling events, and building to-do, tabs, and modal UIs.

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.documentElement is <html>; document.body is <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

textContentinnerHTML
ContentPlain text onlyParses HTML strings into the DOM
XSSUser input won’t execute as markupUntrusted strings can inject scripts
CostSimple and safeHTML 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:

  1. Capturing: window → target (top down).
  2. Target: the element that received the event.
  3. Bubbling: target → window (bottom up). Most events bubble.

The third argument to addEventListener controls 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">&times;</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

  1. Selection:

    • getElementById(): by id
    • querySelector(): CSS selector
    • querySelectorAll(): all matches
  2. Updates:

    • Text: textContent (default), innerHTML (trusted input only)
    • Attributes: getAttribute(), setAttribute()
    • Style: style, classList
  3. Create/remove:

    • Create: createElement()
    • Insert: appendChild(), insertBefore()
    • Remove: remove(), removeChild()
  4. Events:

    • Add: addEventListener()
    • Remove: removeEventListener()
    • Delegation: listen on an ancestor
    • Phases: capturing (capture: true) and bubbling
  5. Event object:

    • event.target: origin element
    • event.preventDefault(): cancel default action
    • event.stopPropagation(): stop propagation

Best practices

  1. ✅ Prefer querySelector for flexibility
  2. ✅ Use delegation for dynamic lists
  3. ✅ Wait for DOMContentLoaded (or place scripts at the bottom)
  4. ✅ Prefer createElement over untrusted innerHTML
  5. ✅ Remove or avoid duplicate listeners when components unmount

Next steps

  • JavaScript 클래스
  • JavaScript 모듈
  • JavaScript 에러 처리

  • JavaScript 시작하기 | 웹 개발의 필수 언어 완벽 입문
  • C++ Kafka 고급 활용 | 스트림 처리·트랜잭션·정확히 한 번 전달 완벽 가이드 [#52-6]
  • C++ 스크립팅 엔진 통합 | Lua·Python·JavaScript 바인딩 완벽 가이드 [#55-3]
  • C++ JavaScript 스크립팅 완벽 가이드 | V8 임베딩·컨텍스트·C++↔JS 바인딩 [실전]
  • HTML/CSS 시작하기 | 웹 개발 첫걸음