[2026] 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. 아래 코드는 html를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
<!DOCTYPE html>
<html>
<head>
<title>제목</title>
</head>
<body>
<h1 id="title">안녕하세요</h1>
<p class="content">내용</p>
</body>
</html>
DOM tree: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. | ||
| 아래 코드는 javascript를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요. |
// ✅ Safe: text only
el.textContent = userInput;
// ⚠️ Risky: userInput could contain <script>
el.innerHTML = userInput;
Attributes
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
아래 코드는 javascript를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 변수 선언 및 초기화
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
다음은 javascript를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
다음은 html를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<!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
다음은 html를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<!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
다음은 html를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<!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.
다음은 html를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<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. 다음은 html를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<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
다음은 html를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<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
아래 코드는 javascript를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
아래 코드는 javascript를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 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
다음은 javascript를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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. 다음은 html를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<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. 다음은 html를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<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