문서 읽는 데 47분 · D1

D-1: DOM 조작

전체 23강 중 19강 · HTML·CSS·JS
난이도 · 입문

안녕하세요, 홍순구 튜터입니다. 지난 시간까지 우리는 자바스크립트 문법을 차곡차곡 쌓았어요. 변수·함수·배열·객체를 지나 비동기와 Promise까지 왔죠. 그런데 그 긴 여정 내내 한 가지 답답한 점이 있었어요. 결과를 전부 콘솔(console.log)로만 봤다는 거예요. 좋아요 1240개를 받아내고, 댓글 목록을 걸러내도, 정작 화면엔 아무것도 안 나타났어요.

오늘 드디어 그 벽을 넘어요. 자바스크립트로 HTML 요소를 직접 찾아서, 내용을 바꾸고, 새 요소를 만들어 붙이고, 지우는 거예요. 멈춰 있던 인스타 클론 화면에 처음으로 생명을 불어넣는 시간이에요.

   지난 시간 (C-1 ~ C-7)                  오늘 (D-1)
   ┌──────────────────────────┐        ┌──────────────────────────┐
   │ 변수·함수·배열·객체·비동기 │  ──▶   │ 화면(HTML)을 직접 조작     │
   │ 결과는 전부 콘솔로만 확인   │        │ 찾고 → 바꾸고 → 만들고 → 지움 │
   │ 화면은 멈춰 있었음          │        │ 좋아요 토글 · 댓글 추가/삭제 │
   └──────────────────────────┘        └──────────────────────────┘

오늘부터 우리 무기에 DOM이라는 새 도구가 들어와요. 이름이 낯설어도 걱정 마세요. 한 단어로 풀면 "브라우저가 HTML로 만든 가계도"예요. 이 가계도에서 사람(요소)을 찾고, 옷을 갈아입히고(내용·속성 변경), 새 식구를 들이고(생성), 떠나보내는(삭제) 게 오늘 할 일의 전부예요.

💡 오늘 수업의 핵심 — "DOM은 브라우저가 HTML로 만든 가계도다. querySelector로 찾고, textContent·classList로 바꾸고, createElement·append로 만들고, remove로 지운다." 🎯

🎯 학습 목표

  • DOM이 무엇인지, 왜 "가계도(트리)"에 비유하는지 이해합니다.
  • querySelector / querySelectorAll로 화면의 요소를 찾습니다(B에서 쓴 CSS 선택자 그대로!).
  • textContentinnerHTML의 차이를 알고, 안전하게 내용을 바꿉니다.
  • getAttribute / classList로 속성과 클래스를 조작해 CSS 스타일을 켜고 끕니다.
  • createElementappend로 새 요소를 만들어 화면에 붙입니다.
  • remove로 요소를 화면에서 지웁니다.
  • 좋아요 버튼 토글과 댓글 추가/삭제를 직접 만들어, 콘솔에서 호출해 확인합니다.

Step 1: "화면은 사실 가계도다" — DOM이란?

브라우저는 우리가 쓴 HTML 파일을 그대로 화면에 그리는 게 아니에요. 먼저 HTML을 한 줄씩 읽어서, 머릿속에 가계도 같은 구조를 하나 만들어요. 할아버지 아래 아버지, 아버지 아래 자식이 있는 족보처럼요. 이 족보의 정식 이름이 DOM(Document Object Model), 우리말로 "문서 객체 모델"이에요.

말이 어렵죠? 그냥 "HTML을 자바스크립트가 만질 수 있도록 가계도 모양으로 정리해 둔 것"이라고 생각하면 돼요. 우리 feed.html의 구조를 가계도로 그려보면 이래요.

 document  (문서 전체 — 가계도의 시조)
    │
    └─ <html>
         ├─ <head>  (제목·스타일 연결 등 — 화면엔 안 보임)
         └─ <body class="app">
              ├─ <header class="topbar">
              ├─ <nav class="sidebar">
              └─ <main>
                   └─ <article>          ← 게시물 하나
                        ├─ <header>       (작성자·아바타)
                        ├─ <figure>       (사진)
                        ├─ <div class="post-actions">  (♥ 💬 ↗ 🔖 버튼)
                        ├─ <p class="post-likes">       (좋아요 1,240개)
                        └─ <ul class="comment-list">    (댓글이 들어올 자리)

가계도니까 가족 용어를 그대로 써요. <body><header>·<nav>·<main>부모(parent), 거꾸로 <main><body>자식(child), <header><main>은 서로 형제(sibling)예요. 가장 꼭대기에 document라는 시조가 있고요. 자바스크립트는 바로 이 document에서 출발해 가계도를 타고 내려가며 원하는 요소를 찾아요.

오늘 우리가 할 일은 딱 네 가지예요. 가계도에서 ① 사람을 찾고, ② 옷을 갈아입히고(내용·속성), ③ 새 식구를 들이고(생성), ④ 식구를 떠나보내요(삭제). 이 네 동작이 DOM 조작의 전부예요.

💡 DOM은 "브라우저가 HTML을 읽어 만든 가계도"예요. 자바스크립트는 document에서 시작해 이 가계도를 타고 다니며 요소를 찾고 바꿔요. 화면이 멈춰 보여도, 사실 그 뒤엔 만질 수 있는 족보가 있어요.


Step 2: "원하는 요소를 콕 집어내기" — querySelector

가계도에서 원하는 사람을 어떻게 찾을까요? 다행히 새로 외울 게 없어요. B 카테고리에서 CSS로 쓰던 그 선택자 문법 그대로 쓰면 돼요. .post-likes라고 적으면 그 클래스를 가진 요소를, article이라고 적으면 그 태그를 찾아요.

도구는 두 개예요. document.querySelector(...)는 조건에 맞는 첫 번째 하나만, document.querySelectorAll(...)맞는 것 전부를 가져와요.

// instagram-clone-frontend/js/feed.js
const firstLikes = document.querySelector(".post-likes");   // 맞는 첫 하나
console.log(firstLikes);                 // <p class="post-likes">좋아요 1,240개</p>

const allArticles = document.querySelectorAll("article");   // 맞는 것 전부 (NodeList)
console.log("게시물 개수:", allArticles.length);            // 게시물 개수: 10

querySelector(".post-likes")는 화면에 .post-likes가 여러 개 있어도 맨 위 첫 번째 하나만 집어와요. 콘솔에 찍어보면 그 <p> 요소가 통째로 나와요. 반면 querySelectorAll("article")은 게시물 <article>전부 담은 묶음을 줘요. 이 묶음을 NodeList(노드 목록)라고 부르는데, 배열과 비슷해서 .length로 개수를 셀 수 있어요. 우리 피드엔 게시물이 10개라서 10이 찍혀요.

  querySelector(".post-likes")          querySelectorAll("article")
  ──────────────────────────           ──────────────────────────
  맞는 것 중 "첫 번째 하나"              맞는 것 "전부" (묶음 = NodeList)
  요소 1개 반환                         .length 로 개수 확인 (여기선 10)

찾기는 document에서만 시작하는 게 아니에요. 이미 찾은 요소 안에서 또 찾을 수도 있어요. 예를 들어 someArticle.querySelector(".icon-btn-like")라고 하면, 그 게시물 안에서만 좋아요 버튼을 찾아요. 가계도에서 "이 집안 식구 중에서만 찾아줘"라고 범위를 좁히는 거죠. 이 방식은 잠시 뒤 좋아요 토글에서 요긴하게 써요.

💡 querySelector는 CSS 선택자로 요소를 찾아요. B에서 배운 선택자 지식이 그대로 살아 있어요. 하나만 필요하면 querySelector, 전부 필요하면 querySelectorAll이에요.


Step 3: "내용을 읽고 바꾸기" — textContent vs innerHTML

요소를 찾았으면, 이제 그 안의 내용을 읽거나 바꿀 차례예요. 방법이 두 가지 있는데, 비슷해 보여도 성격이 꽤 달라요. 바로 textContentinnerHTML이에요.

// instagram-clone-frontend/js/feed.js
const caption = document.querySelector(".post-caption");
console.log(caption.textContent);   // 글자만: "jaehoon오늘의 일상 — ..."
console.log(caption.innerHTML);     // 태그째: "<strong>jaehoon</strong>오늘의..."

textContent는 요소 안의 글자만 꺼내요. 안에 <strong> 같은 태그가 있어도 무시하고 순수한 텍스트만 줘요. 반면 innerHTML은 안에 든 HTML 태그째 문자열로 줘요. 같은 캡션을 읽었는데 textContentjaehoon오늘의 일상..., innerHTML<strong>jaehoon</strong>오늘의...로 다르게 나오죠.

  같은 <p class="post-caption"> 를 읽으면

  textContent  →  "jaehoon오늘의 일상 — ..."        (글자만, 안전)
  innerHTML    →  "<strong>jaehoon</strong>오늘의..." (태그째, 강력하지만 위험)

읽기뿐 아니라 바꿀 때도 둘 다 쓸 수 있어요. caption.textContent = "새 내용"이라고 하면 글자가 통째로 바뀌고, caption.innerHTML = "<strong>굵게</strong>"라고 하면 진짜 굵은 글씨가 돼요. innerHTML은 태그를 해석하니 더 강력해 보이지만, 여기엔 함정이 있어요.

⚠️ innerHTML남이 입력한 내용(댓글, 검색어 등)을 그대로 넣으면 위험해요. 누군가 댓글에 악성 스크립트 태그를 적어 넣으면, innerHTML이 그걸 진짜 코드로 해석해 실행해버릴 수 있거든요. 이런 공격을 XSS(크로스 사이트 스크립팅)라고 해요. 그래서 **사용자가 적은 글자를 넣을 땐 항상 textContent**를 쓰는 게 안전해요. 오늘 댓글을 추가할 때도 textContent만 써요.

🙋 학생 질문 — "튜터님, 그럼 innerHTML은 위험하니까 안 쓰는 게 좋은 거예요?"

위험한 게 아니라 "쓸 자리를 가려야" 해요. innerHTML은 내가 직접 통제하는 안전한 HTML 조각을 한 번에 그릴 때는 아주 편리해요. 문제는 딱 하나, 남이 입력한 값을 그대로 넣을 때예요.

기준은 간단해요. "이 문자열에 사용자가 적은 내용이 섞여 있나?"를 물어보세요. 섞여 있다면 textContent로 글자만 안전하게 넣고, 내가 100% 만든 고정 HTML이라면 innerHTML도 괜찮아요. 오늘은 댓글(사용자 입력)을 다루니까, 우리는 처음부터 끝까지 textContent만 써서 안전한 습관을 들일 거예요.


Step 4: "속성과 클래스 갈아입히기" — getAttribute / classList

내용 말고 속성도 바꿀 수 있어요. 요소의 aria-label, src, href 같은 값들이요. 읽을 땐 getAttribute("속성이름"), 바꿀 땐 setAttribute("속성이름", "새 값")을 써요.

그런데 그중에서도 오늘 가장 많이 쓸 건 클래스 조작이에요. 클래스를 켜고 끄는 것만으로 B에서 만든 CSS 스타일을 화면에서 입혔다 벗겼다 할 수 있거든요. 이걸 위한 전용 도구가 classList예요.

// instagram-clone-frontend/js/feed.js
const firstArticle = document.querySelector("article");
const likeBtn = firstArticle.querySelector(".icon-btn-like");
console.log(likeBtn.getAttribute("aria-label"));      // "좋아요"
console.log(likeBtn.classList.contains("is-active")); // false (아직 안 눌렀어요)

firstArticle.querySelector(".icon-btn-like")로 첫 게시물의 좋아요 버튼을 찾았어요(Step 2에서 말한 "요소 안에서 다시 찾기"죠). getAttribute("aria-label")은 그 버튼의 aria-label 속성값 "좋아요"를 읽어와요. 그리고 classList.contains("is-active")는 "이 요소에 is-active 클래스가 붙어 있니?"를 묻고 true/false로 답해요. 아직 안 눌렀으니 false죠.

classList로 할 수 있는 일은 네 가지예요.

  likeBtn.classList.add("is-active")      → is-active 클래스를 붙임
  likeBtn.classList.remove("is-active")   → is-active 클래스를 뗌
  likeBtn.classList.toggle("is-active")   → 있으면 떼고, 없으면 붙임 (켜기/끄기)
  likeBtn.classList.contains("is-active") → 붙어 있는지 물어봄 (true/false)

여기서 진짜 마법은 toggle이에요. 우리 components.css엔 이미 .icon-btn-like.is-active { color: 빨강 } 규칙이 들어 있어요. 그러니 자바스크립트로 is-active 클래스를 붙이기만 하면, CSS가 알아서 하트를 빨갛게 칠해줘요. 자바스크립트는 클래스만 켜고 끌 뿐, 실제 색은 CSS가 담당하는 거죠. 역할이 깔끔하게 나뉘어요. 이 패턴을 Step 7에서 좋아요 토글로 완성해요.

💡 classList.toggle("클래스")는 클래스를 켰다 껐다 해요. 스타일 자체는 CSS가 들고 있고, 자바스크립트는 스위치만 누르는 셈이에요. "JS는 클래스, CSS는 디자인" — 이 분담이 프론트엔드의 기본기예요.


Step 5: "새 요소를 만들어 붙이기" — createElement / append

지금까지는 이미 화면에 있는 요소를 찾아 바꿨어요. 이번엔 아예 없던 요소를 새로 만들어 화면에 붙여볼게요. 댓글 한 줄을 자바스크립트로 만들어 다는 거예요.

두 단계로 나뉘어요. 먼저 document.createElement("태그이름")으로 빈 요소를 만들어요. 이때는 아직 화면에 안 보여요. 메모리 안에만 떠 있는 상태죠. 그다음 부모.append(새요소)로 가계도에 붙이면, 그제서야 화면에 등장해요.

// instagram-clone-frontend/js/feed.js
const list = firstArticle.querySelector(".comment-list");

const c1 = document.createElement("li");      // 빈 <li> 를 메모리에 만들고
c1.className = "comment";
c1.textContent = "minji_ 사진 너무 예뻐요!";  // 글자를 넣고 (textContent — 안전)
list.append(c1);                              // 목록 맨 뒤에 붙이면 화면에 등장

차근차근 보세요. createElement("li")로 빈 <li>를 만들고, className으로 comment 클래스를 붙이고(CSS 스타일 적용용), textContent로 댓글 글자를 넣어요(Step 3에서 배운 안전한 방법!). 여기까지는 화면에 아무 변화가 없어요. 마지막 list.append(c1)이 실행되는 순간, 이 <li>가 댓글 목록 맨 뒤에 붙으면서 드디어 화면에 나타나요.

  createElement("li")          append(c1)
  ─────────────────           ─────────────────
  빈 <li> 가 메모리에 떠 있음    가계도(트리)에 붙임
  → 화면엔 아직 안 보임          → 비로소 화면에 등장

  <ul class="comment-list">          <ul class="comment-list">
     (비어 있음)            ──▶          └─ <li class="comment">minji_ 사진 너무 예뻐요!</li>

"만들기"와 "붙이기"가 별개라는 게 핵심이에요. 요소를 만든다고 화면에 나오는 게 아니라, 가계도의 어느 부모에 append로 매달아야 비로소 보여요. 새 식구를 데려와서 족보에 올리는 것과 같죠.

💡 새 요소는 createElement로 만들고 append로 붙여요. 만들기만 하면 화면엔 안 보이고, 부모에 append해야 등장해요. "제작 → 부착" 두 박자를 기억하세요.


Step 6: "요소를 지우기" — remove

붙였으면 지울 줄도 알아야죠. 방금 만든 댓글을 화면에서 떼어내는 건 정말 간단해요. 지울 요소에 .remove()를 붙이면 끝이에요.

// instagram-clone-frontend/js/feed.js
const c2 = document.createElement("li");
c2.className = "comment";
c2.textContent = "yuna 다음 여행 같이 가요";
list.append(c2);   // 일단 붙였다가

c2.remove();        // 다시 떼어내기 — 화면에서 사라져요

두 번째 댓글 c2를 만들어 append로 붙였어요. 그리고 바로 c2.remove()를 부르면, 그 <li>가 가계도에서 떨어져 나가 화면에서 사라져요. 족보에서 한 식구를 빼는 것과 같아요. 그 자리는 자연스럽게 메워지고요.

  지우기 전                              c2.remove() 후
  <ul class="comment-list">             <ul class="comment-list">
     ├─ <li>minji_ 사진 너무 예뻐요!</li>    └─ <li>minji_ 사진 너무 예뻐요!</li>
     └─ <li>yuna 다음 여행 같이 가요</li>   (yuna 댓글은 사라짐)

그래서 feed.js를 저장하고 feed.html을 Live Server로 열어보면, 첫 게시물 댓글 자리에 minji_ 사진 너무 예뻐요! 한 줄만 남아 있어요. yuna 댓글은 만들어 붙였다가 remove로 지웠으니까요. 우리가 자바스크립트로 만든 첫 요소가 진짜 화면에 떠 있는 모습을, 이 시점에 눈으로 확인할 수 있어요.

💡 요소를 지울 땐 요소.remove() 한 줄이면 돼요. 가계도에서 그 식구를 떼어내면 화면에서도 사라져요. 만들기(createElement)·붙이기(append)·지우기(remove)가 DOM 조작의 3대 동작이에요.


Step 7: "좋아요를 누르면 하트가 빨개진다" — 좋아요 토글

이제 배운 도구를 모아 진짜 기능을 만들어요. 첫 실습은 좋아요 토글이에요. 좋아요를 누르면 하트가 빨개지고 숫자가 1 올라가고, 다시 누르면 원래대로 돌아오는 거죠. Step 4의 classList.toggle과 Step 3의 textContent를 합치면 돼요.

이 기능은 따로 like.js라는 파일에 담았어요.

// instagram-clone-frontend/js/like.js
export function toggleLike(index) {
  const article = document.querySelectorAll("article")[index];
  if (!article) {
    console.log("그런 게시물이 없어요:", index);
    return;
  }

  // 1) 하트 버튼의 'is-active' 클래스를 켜고 끄기 (CSS 가 빨간 하트로 보여줘요)
  //    classList.toggle 은 켜졌으면 true, 꺼졌으면 false 를 돌려줘요.
  const likeBtn = article.querySelector(".icon-btn-like");
  const liked = likeBtn.classList.toggle("is-active");

  // 2) 좋아요 숫자 갱신 — <strong> 안의 글자만 textContent 로 바꿔요
  const strong = article.querySelector(".post-likes strong");
  let count = Number(strong.textContent.replaceAll(",", "")); // "1,240" → 1240
  count = liked ? count + 1 : count - 1;
  strong.textContent = count.toLocaleString();                // 1241 → "1,241"

  console.log(liked ? "좋아요 ❤️" : "좋아요 취소 🤍", "→", strong.textContent);
  return liked;
}

흐름을 따라가 볼게요. toggleLike(index)는 "몇 번째 게시물의 좋아요를 누를지"를 번호로 받아요. querySelectorAll("article")[index]로 그 게시물을 집어내고, 혹시 없는 번호면 안내만 하고 멈춰요(if (!article)).

핵심은 두 줄이에요. likeBtn.classList.toggle("is-active")로 하트 버튼의 클래스를 켜고 끄면, CSS가 알아서 빨갛게/원래대로 칠해줘요. 그리고 toggle이 돌려준 값(liked)을 보고, 켜졌으면 숫자 +1, 꺼졌으면 1을 해요. 이때 좋아요 숫자가 "1,240"처럼 쉼표가 있어서, replaceAll(",", "")로 쉼표를 떼어 숫자로 바꾼 뒤 계산하고, 다시 toLocaleString()으로 "1,241"처럼 쉼표를 붙여 textContent에 넣어요.

이 함수는 모듈 안에 들어 있어서 콘솔에서 바로 부를 수가 없어요. 그래서 feed.js에서 window에 붙여 콘솔에서 부를 수 있게 해뒀어요.

// instagram-clone-frontend/js/feed.js
window.toggleLike = toggleLike;
window.addComment = addComment;
window.removeComment = removeComment;

window는 브라우저의 가장 바깥 전역 공간이에요. 여기에 함수를 얹어두면 콘솔에서 이름만으로 부를 수 있어요. 자, feed.html을 Live Server로 열고 콘솔(F12)에서 toggleLike(0)을 쳐보세요. 첫 게시물 하트가 빨개지고 숫자가 1,240 → 1,241로 올라가요. 한 번 더 치면 다시 원래대로 돌아오고요. 이 순간이, 멈춰 있던 화면이 처음으로 우리 손가락에 반응하는 장면이에요.

💡 좋아요 토글은 classList.toggle(하트 색)과 textContent(숫자)를 합친 거예요. toggle이 돌려주는 true/false로 +1 할지 1 할지를 정해요. 색은 CSS, 숫자는 JS — 역할이 또 나뉘죠.


Step 8: "댓글을 달고 지우자" — 댓글 추가/삭제

두 번째 실습은 댓글 추가/삭제예요. Step 5·6에서 배운 createElement·append·remove를 그대로 함수로 묶어요. 이건 comment.js에 담았어요.

먼저 댓글을 추가하는 addComment예요.

// instagram-clone-frontend/js/comment.js
export function addComment(index, text) {
  const article = document.querySelectorAll("article")[index];
  if (!article) {
    console.log("그런 게시물이 없어요:", index);
    return;
  }
  const list = article.querySelector(".comment-list");

  // 1) 빈 <li> 를 만들고 클래스를 붙여요
  const li = document.createElement("li");
  li.className = "comment";

  // 2) 댓글 글자는 textContent 로 (innerHTML 아님 — 남의 입력은 안전하게)
  const span = document.createElement("span");
  span.textContent = text;

  // 3) 삭제 버튼도 만들어 둬요 (실제 클릭 연결은 다음 시간 이벤트에서)
  const delBtn = document.createElement("button");
  delBtn.type = "button";
  delBtn.className = "comment-del";
  delBtn.textContent = "삭제";

  li.append(span, delBtn);  // append 는 여러 개를 한 번에 붙일 수 있어요
  list.append(li);          // 목록 맨 뒤에 추가 → 화면에 등장

  console.log("댓글 추가:", text);
  return li;                // 지울 때 쓰라고 만든 li 를 돌려줘요
}

순서대로 보면, 댓글을 담을 <li>를 만들고, 그 안에 글자를 담을 <span>과 "삭제" <button>을 각각 만들어요. 글자는 Step 3에서 약속한 대로 textContent로 안전하게 넣고요(사용자가 적은 댓글이니까요). 그다음 li.append(span, delBtn)<li> 안에 글자와 버튼을 한꺼번에 넣어요. append는 이렇게 여러 개를 한 번에 붙일 수 있어요. 마지막으로 list.append(li)로 댓글 목록에 매달면 화면에 떠요.

함수 끝에서 만든 lireturn하는 게 보이죠? 나중에 이 댓글을 지우려면 "어떤 <li>를 지울지" 알려줘야 하거든요. 그래서 만든 요소를 돌려줘요. 지우는 함수는 이걸 받아서 remove만 해요.

// instagram-clone-frontend/js/comment.js
export function removeComment(li) {
  if (!li) {
    console.log("지울 댓글이 없어요");
    return;
  }
  li.remove();   // 자기 자신을 트리에서 떼어내요
  console.log("댓글 삭제 완료");
}

콘솔에서 직접 써볼까요? addComment(0, "댓글 정말 멋져요!")를 치면 첫 게시물에 댓글이 한 줄 생겨요. 그리고 지우고 싶으면, 추가할 때 돌려받은 li를 변수에 담아뒀다가 removeComment에 넘기면 돼요.

  콘솔에서 이렇게 써보세요

  > addComment(0, "댓글 정말 멋져요!")     ← 댓글이 화면에 추가됨
  > const li = addComment(0, "이건 지울 거예요")
  > removeComment(li)                      ← 방금 단 댓글이 사라짐

추가할 때 만든 "삭제" 버튼은 아직 눌러도 아무 일이 안 일어나요. 버튼이라는 요소를 만들어 붙이기만 했지, "누르면 지워라"라는 동작은 아직 연결하지 않았거든요. 그 연결은 다음 시간에 배우는 이벤트의 몫이에요. 오늘은 요소를 만들고 지우는 것까지만 익혀요.

💡 댓글 추가는 createElement<li>·<span>·<button>을 만들어 append로 붙이는 거예요. 삭제는 그때 돌려받은 liremove하면 끝이고요. Step 5·6에서 배운 3대 동작이 실전 기능으로 합쳐졌어요.


마무리

오늘 우리는 멈춰 있던 화면에 처음으로 손을 댔어요. 콘솔에만 머물던 결과를 진짜 화면 위로 끌어올린 거죠. 되짚어볼게요.

  • DOM — 브라우저가 HTML로 만든 가계도(트리). document에서 출발해 부모·자식·형제를 타고 다녀요.
  • querySelector / querySelectorAll — CSS 선택자로 요소 찾기. 하나면 querySelector, 전부면 querySelectorAll.
  • textContent / innerHTML — 글자만(안전) vs 태그째(강력하지만 사용자 입력엔 위험). 사용자 글자엔 textContent.
  • getAttribute / classList — 속성 읽고 쓰기, 클래스 켜고 끄기. classList.toggle로 CSS 스타일을 스위치처럼.
  • createElement / append — 새 요소를 만들어(메모리) 부모에 붙이기(화면 등장).
  • remove — 요소를 가계도에서 떼어내 화면에서 지우기.
  • 좋아요 토글 · 댓글 추가/삭제 — 위 도구를 합친 첫 실전 기능. 콘솔에서 호출해 확인.

한 줄로 외워두세요. 찾고(querySelector) → 바꾸고(textContent·classList) → 만들고(createElement·append) → 지운다(remove). 이 네 박자가 앞으로 만들 모든 화면 조작의 뼈대예요.

다음 시간 예고

그런데 오늘은 좀 어색했죠? 좋아요를 누르려고 진짜 하트를 클릭한 게 아니라, 콘솔에서 toggleLike(0)을 손으로 쳤잖아요. 댓글의 "삭제" 버튼도 만들기만 했지, 눌러도 아무 반응이 없었고요. 진짜 인스타라면 하트를 클릭하는 순간 좋아요가 켜져야 하는데 말이죠.

다음 시간엔 이 마지막 한 칸을 채워요. "사용자가 하트를 클릭하면 toggleLike가 저절로 불리게" 연결하는 거예요. 이걸 이벤트(event)라고 하고, 연결하는 도구가 addEventListener예요. 게시물이 100개여도 버튼마다 일일이 연결하지 않고 한 번에 처리하는 이벤트 위임이라는 영리한 기법도 함께 배워요. 오늘 만든 함수들이 드디어 진짜 클릭에 반응해 살아나는 순간이에요. 다음 시간에 만나요!


과제

오늘 배운 DOM 조작(querySelector·textContent·classList·createElement·append·remove)을 직접 익혀볼 차례예요. 모든 과제는 feed.html을 Live Server로 열고, 브라우저 콘솔(F12)에서 함수를 호출해 화면이 바뀌는지 눈으로 확인하세요. 아직 클릭 이벤트(addEventListener)나 서버 통신(fetch)은 쓰지 않아요 — 오늘 배운 DOM 도구만으로 충분히 풀 수 있어요.

[구현] 좋아요 숫자를 화면에서 직접 바꿔보기

querySelectortextContent만으로 화면의 숫자를 바꿔보세요.

  • 콘솔에서 첫 게시물의 .post-likes strong 요소를 querySelector로 찾으세요.
  • 그 요소의 textContent를 읽어 콘솔에 찍어보세요(쉼표가 든 문자열로 나오는지 확인).
  • textContent에 새 숫자 문자열을 직접 넣어, 화면의 좋아요 수가 즉시 바뀌는지 확인하세요.
  • classList.add("is-active")로 그 게시물 하트를 빨갛게 켜보고, remove로 다시 꺼보세요. 색이 바뀌는 주체가 JS인지 CSS인지 한 문장으로 설명해보세요.

[구현] 게시물 두 곳에 댓글을 달고, 하나만 지우기

addComment·removeComment로 댓글을 추가하고 골라 지워보세요.

  • 콘솔에서 addComment(0, ...)로 첫 게시물에 댓글 두 개를 다세요.
  • 두 번째 댓글은 추가할 때 돌려받은 li를 변수에 담아두세요.
  • 그 변수를 removeComment에 넘겨, 두 번째 댓글만 사라지고 첫 댓글은 남는지 확인하세요.
  • addComment가 왜 만든 lireturn하는지, 그게 삭제에 어떻게 쓰이는지 한 문장으로 정리해보세요.

[탐구] textContent와 innerHTML의 차이를 직접 눈으로 확인하기

같은 값을 두 방식으로 넣어, 결과가 어떻게 다른지 추적해보세요.

  • 콘솔에서 빈 요소를 하나 찾거나 만들어, textContent<strong>굵게</strong>라는 문자열을 넣어보세요. 화면에 무엇이 보이나요?
  • 같은 문자열을 innerHTML에 넣어보세요. 이번엔 무엇이 보이나요?
  • 두 결과가 왜 다른지 설명하고, "사용자가 입력한 댓글"을 넣을 땐 둘 중 무엇을 써야 안전한지 이유와 함께 정리해보세요.

생각해볼 주제

정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.

1. 색을 바꾸는 일은 누가 해야 할까 — JS일까, CSS일까?

오늘 좋아요 하트를 빨갛게 만들 때, 자바스크립트가 직접 색(color: red)을 칠하지 않았어요. 대신 classList.toggle("is-active")로 클래스만 켜고, 실제 빨간색은 components.css.icon-btn-like.is-active 규칙이 칠했죠. 자바스크립트로도 likeBtn.style.color = "red"처럼 색을 직접 줄 수 있는데도, 우리는 일부러 클래스만 건드렸어요.

만약 좋아요·저장·팔로우처럼 "켜짐/꺼짐" 상태가 여러 개로 늘어난다면, 색을 JS가 직접 칠하는 방식과 클래스만 토글하는 방식 중 어느 쪽이 고치기 쉬울까요? 디자인이 바뀌어 "빨강을 분홍으로" 한다면 어디를 고쳐야 하는지를 떠올리며, 역할을 나누는 게 왜 유리한지 곱씹어보세요.

2. 요소를 "만드는 것"과 "화면에 보이는 것"은 왜 다를까?

Step 5에서 createElement<li>를 만들어도 화면엔 안 보였고, append로 부모에 붙여야 비로소 나타났어요. "만들면 곧 화면에 나오는" 게 아니라, 만드는 단계와 붙이는 단계가 따로 있었죠.

이 두 단계가 분리되어 있는 게 불편해 보일 수도 있지만, 사실 큰 장점이 돼요. 예를 들어 댓글 100개를 만들어 화면에 다는 상황을 떠올려보세요. 100개를 하나씩 만들어 그때그때 붙이는 것과, 100개를 다 만들어 모아뒀다가 마지막에 한 번에 붙이는 것 중 어느 쪽이 화면을 덜 흔들고 빠를까요? "만들기"와 "붙이기"가 나뉘어 있어서 가능한 일이 무엇일지 생각해보세요.

✅ 예시 답안정답 보기

🎯 [과제 1 예시답안] 좋아요 숫자를 화면에서 직접 바꿔보기

수업에서 toggleLike가 한꺼번에 해준 일을, 이번엔 querySelector·textContent·classList로 한 조각씩 직접 해보는 과제예요. 화면의 숫자와 하트 색을 콘솔 명령만으로 바꿔보면서, "글자는 누가 바꾸고 색은 누가 칠하는지"를 손으로 확인하는 거예요.

핵심 접근

querySelector(".post-likes strong")로 좋아요 숫자가 든 <strong>을 콕 집어요. 그 textContent를 읽으면 쉼표가 든 문자열이 나오고, 거기에 새 문자열을 넣으면 화면이 즉시 바뀌어요. 하트 색은 classList.add/remove("is-active")로 켜고 끄는데, 실제 빨간색은 CSS가 칠해줘요.

예시 구현

// feed.html 을 Live Server 로 열고 콘솔(F12)에서 한 줄씩 쳐보세요.

const strong = document.querySelector(".post-likes strong");
console.log(strong.textContent);        // "1,240"  (쉼표가 든 문자열)

strong.textContent = "9,999";           // 화면의 좋아요 수가 즉시 바뀜

const likeBtn = document.querySelector(".icon-btn-like");
likeBtn.classList.add("is-active");     // 하트가 빨개짐 (색은 CSS 가 칠함)
likeBtn.classList.remove("is-active");  // 다시 원래 색으로

strong.textContent를 읽으면 "1,240"처럼 우리가 화면에서 보던 숫자가 문자열로 나와요. 거기에 "9,999"를 넣자마자 화면이 바뀌죠. 그리고 likeBtn.classList.add("is-active")를 하면 하트가 빨개지는데, 자바스크립트는 is-active 클래스만 붙였을 뿐이에요. 빨간색을 칠한 건 components.css.icon-btn-like.is-active { color: ... } 규칙이에요. 글자는 JS가, 색은 CSS가 맡은 거죠.

채점 포인트

포인트 무엇을 보는가 배점 가중
정확한 요소 선택 .post-likes 통째가 아니라 안의 strong을 잡았는가
textContent 읽기 읽은 값이 쉼표가 든 문자열임을 확인했는가
textContent 쓰기 새 문자열을 넣어 화면이 즉시 바뀌는 걸 확인했는가
classList 토글 add/remove로 하트 색이 켜지고 꺼지는 걸 봤는가
역할 설명 색을 바꾸는 주체가 JS가 아니라 CSS임을 설명했는가

흔한 실수

  • .post-likes만 잡아 textContent를 바꿈 → "좋아요 1,240개"가 통째로 사라지고 내가 넣은 글자만 남아요. 숫자만 바꾸려면 안쪽 strong을 잡아야 해요.
  • likeBtn.style.color = "red"로 색을 직접 칠함 → 동작은 하지만, 디자인 정보를 자바스크립트 안에 섞어 넣는 셈이에요. 색은 CSS에 맡기고 JS는 클래스만 켜는 게 좋아요(생각해볼 주제 1에서 더 다뤄요).
  • strong.textContent에 숫자 9999를 그대로 넣음 → 자동으로 문자열이 되긴 하지만, 화면 표시용 값은 처음부터 문자열("9,999")로 다루는 습관이 안전해요.

실무 한 걸음 더

색을 classList로 다루면, 나중에 디자이너가 "좋아요 색을 빨강에서 분홍으로 바꿔주세요"라고 할 때 CSS의 색 한 줄만 고치면 끝나요. 자바스크립트 코드는 손댈 필요가 없죠. 지금은 콘솔에서 classList.add를 손으로 쳤지만, 다음 시간엔 하트를 클릭하는 순간 이게 저절로 불리도록 연결해요. 오늘 만든 조각들이 그때 진짜 버튼으로 살아나요.


🎯 [과제 2 예시답안] 게시물 두 곳에 댓글을 달고, 하나만 지우기

수업에서 만든 addComment·removeComment를 콘솔에서 직접 불러, 댓글을 여러 개 달고 그중 하나만 골라 지우는 과제예요. 핵심은 "지울 댓글을 어떻게 가리키느냐"예요.

핵심 접근

addComment(0, ...)로 첫 게시물에 댓글을 두 개 달아요. 이때 addComment는 자기가 만든 <li>return해주니까, 지우고 싶은 댓글은 그 반환값을 변수에 담아둬요. 나중에 그 변수를 removeComment에 넘기면, 정확히 그 한 줄만 사라져요.

예시 구현

// feed.html 콘솔에서

addComment(0, "첫 번째 댓글이에요");          // 첫 게시물에 댓글 추가
const second = addComment(0, "이건 지울 댓글"); // 만들어진 li 를 변수에 담아둠

removeComment(second);   // 두 번째 댓글만 사라지고, 첫 댓글은 그대로 남음

addComment가 함수 끝에서 return li로 만든 요소를 돌려준다는 게 열쇠예요. 첫 번째 호출의 반환값은 안 받아도 되지만, 지우려는 두 번째 댓글은 const second = addComment(...)로 꼭 붙잡아둬요. 그래야 removeComment(second)로 "바로 이 <li>를 지워라"라고 정확히 가리킬 수 있어요. 결과적으로 화면엔 첫 번째 댓글이에요 한 줄만 남아요.

채점 포인트

포인트 무엇을 보는가 배점 가중
댓글 두 개 추가 addComment(0, ...)로 같은 게시물에 두 개를 달았는가
반환값 저장 두 번째 addComment의 반환 li를 변수에 담았는가
정확한 삭제 그 변수를 removeComment에 넘겨 두 번째만 지웠는가
결과 확인 첫 댓글은 남고 둘째만 사라진 걸 화면으로 확인했는가
return 이유 설명 addCommentli를 돌려주는 이유와 쓰임을 설명했는가

흔한 실수

  • removeComment(1)이나 removeComment(0)처럼 번호를 넘김 → removeComment는 번호가 아니라 <li> 요소를 받아요. if (!li)에 걸려 "지울 댓글이 없어요"가 떠요.
  • 반환값을 안 받고 지우려 함 → 어떤 <li>를 지울지 가리킬 방법이 없어요. 추가할 때 돌려받은 값을 꼭 변수에 담아두세요.
  • 첫 번째 댓글을 지우려고 second를 그대로 넘김 → 변수에 담긴 건 두 번째 <li>예요. 지우려는 댓글의 반환값을 정확히 담았는지 확인하세요.

실무 한 걸음 더

지금은 "추가할 때 돌려받은 li"로 댓글을 가리켰는데, 실제 서비스에선 댓글마다 고유한 id(예: data-comment-id="42")를 붙여 서버의 데이터와 짝지어요. 그래야 새로고침해도, 다른 사람이 단 댓글이라도 정확히 찾아 지울 수 있거든요. 그리고 댓글에 붙인 "삭제" 버튼은 오늘은 눌러도 반응이 없죠. 다음 시간에 배우는 이벤트로 "삭제 버튼을 누르면 그 댓글을 지워라"를 연결하면, 비로소 진짜 삭제 버튼이 돼요.


🎯 [과제 3 예시답안] textContent와 innerHTML의 차이를 직접 눈으로 확인하기

같은 문자열 <strong>굵게</strong>textContentinnerHTML에 각각 넣어, 결과가 어떻게 갈리는지 눈으로 확인하는 탐구예요. 이 차이를 한 번 보고 나면, 사용자 입력을 어디에 넣어야 안전한지 감이 잡혀요.

핵심 접근

빈 요소를 하나 골라(예: 첫 게시물의 .comment-list), 똑같은 태그 문자열을 두 방식으로 넣어봐요. textContent는 태그를 글자 그대로 보여주고, innerHTML은 태그를 해석해 진짜 스타일을 입혀요. 같은 입력, 다른 결과를 비교하는 게 목적이에요.

예시 구현

// feed.html 콘솔에서

const box = document.querySelector(".comment-list");

box.textContent = "<strong>굵게</strong>";
// 화면에 보이는 것:  <strong>굵게</strong>   ← 태그가 글자 그대로 보임

box.innerHTML = "<strong>굵게</strong>";
// 화면에 보이는 것:  굵게                    ← 진짜 굵은 글씨가 됨

같은 문자열인데 결과가 완전히 달라요. textContent는 안에 든 게 태그든 뭐든 전부 글자로 취급해서, <strong>까지 화면에 그대로 찍어요. 반면 innerHTML은 그 문자열을 HTML로 해석해서, <strong>을 진짜 굵게 만드는 태그로 처리해요. 그래서 한쪽은 꺾쇠가 보이고, 한쪽은 굵은 글씨만 보이죠.

여기서 보안 감각이 나와요. 만약 box에 들어갈 게 사용자가 입력한 댓글이라면 어떨까요? 누군가 댓글에 악성 스크립트 태그를 적어 넣었을 때, innerHTML은 그걸 진짜 코드로 해석해 실행해버릴 수 있어요(XSS). textContent는 전부 글자로만 보여주니 안전하고요. 그래서 **사용자 입력은 textContent**가 기본이에요.

채점 포인트

포인트 무엇을 보는가 배점 가중
같은 입력 두 방식 동일한 태그 문자열을 textContent·innerHTML에 각각 넣었는가
textContent 결과 태그가 글자 그대로 보이는 걸 확인했는가
innerHTML 결과 태그가 해석돼 굵은 글씨가 되는 걸 확인했는가
차이 설명 두 결과가 다른 이유(글자 vs 해석)를 설명했는가
안전 판단 사용자 입력엔 textContent가 안전한 이유를 짚었는가

흔한 실수

  • 둘이 똑같이 동작한다고 적음 → 평범한 글자만 넣으면 결과가 같아 보여요. 차이는 태그가 섞였을 때 드러나니, 꼭 <strong> 같은 태그를 넣어 비교하세요.
  • innerHTML이 더 강력하니 항상 낫다고 결론 냄 → 강력한 만큼 사용자 입력에선 위험해요. "강력함 = 안전함"이 아니에요.
  • textContent로 진짜 굵은 글씨를 만들려고 함 → textContent는 태그를 절대 해석하지 않아요. 글자만 다뤄요.

실무 한 걸음 더

규칙은 간단해요. 사용자가 적은 값은 textContent, 내가 100% 만든 고정 HTML만 innerHTML. 그래도 댓글에 굵게·링크 같은 서식을 꼭 넣어야 한다면, 입력값을 그대로 innerHTML에 넣지 않고 위험한 태그를 걸러내는 "살균(sanitize)" 과정을 거쳐요. 실무에선 이걸 검증된 라이브러리에 맡기고, 직접 만들지 않는 게 정석이에요. 오늘 단계에선 "사용자 입력엔 무조건 textContent"만 몸에 익혀도 충분해요.


💭 [생각해볼 주제 예시답안]


1. 색을 바꾸는 일은 누가 해야 할까 — JS일까, CSS일까?

문제 상황 요약

좋아요 하트를 빨갛게 만들 때, 자바스크립트가 직접 color: red를 칠하지 않고 classList.toggle("is-active")로 클래스만 켰어요. 실제 빨간색은 components.css.icon-btn-like.is-active 규칙이 칠했죠. JS로도 likeBtn.style.color = "red"처럼 색을 직접 줄 수 있는데, 왜 일부러 클래스만 건드렸을까요?

튜터의 가이드 및 해설

핵심은 "누가 무엇을 책임지는가"를 나누는 거예요. 화면을 만들 때 역할은 보통 이렇게 갈려요. HTML은 구조, CSS는 디자인, 자바스크립트는 동작. 색은 디자인이니 CSS의 몫이고, "지금 켜진 상태인지 꺼진 상태인지"라는 동작·상태만 JS가 맡는 게 자연스러워요.

두 방식을 비교해볼게요. JS가 색을 직접 칠하면(style.color = "red"), 당장은 동작해요. 하지만 "좋아요 색을 분홍으로 바꿔주세요", "눌렀을 때 살짝 커지게 해주세요" 같은 요청이 오면, 디자인을 고치는데 자바스크립트 코드를 뒤져야 해요. 색·크기·그림자 같은 디자인 정보가 JS 곳곳에 흩어지면 고치기 점점 어려워지죠.

반대로 클래스만 토글하면, 디자인은 전부 CSS의 .is-active 규칙 한 곳에 모여요. 색을 바꾸든 애니메이션을 더하든 CSS만 고치면 되고, 자바스크립트는 "클래스를 켠다/끈다"는 동작만 그대로 두면 돼요. 상태가 좋아요·저장·팔로우처럼 여러 개로 늘어날수록 이 분리의 이점이 커져요.

  • Option A — JS가 색을 직접(style.color): 빠르게 한 곳을 바꿀 땐 간단해요. 하지만 디자인이 JS 안에 섞여 들어가, 색·효과를 바꿀 때마다 코드를 고쳐야 하고 여러 상태가 겹치면 지저분해져요.
  • Option B — 클래스 토글 + CSS: 디자인은 CSS, 동작은 JS로 깔끔하게 나뉘어요. 디자인 변경이 CSS 한 곳으로 모이는 게 장점. 클래스 이름을 미리 약속해둬야 하는 게 작은 부담이에요.
  • 현업에서는 보통: 상태에 따른 스타일은 Option B예요. JS는 is-active·is-open 같은 상태 클래스만 켜고 끄고, 실제 모양은 CSS가 책임져요. "JS는 상태, CSS는 모양"이 화면 코드를 오래 깨끗하게 유지하는 기본기예요.

🎯 면접관을 홀리는 핵심 멘트

"색 같은 디자인은 CSS, '켜짐/꺼짐' 같은 상태는 JS가 맡는 게 기본이에요. 그래서 좋아요 하트도 자바스크립트로 색을 직접 칠하지 않고 classList.toggle로 상태 클래스만 켜고, 빨간색은 CSS의 .is-active 규칙에 맡겼어요. 이렇게 나눠두면 디자인이 바뀔 때 CSS 한 곳만 고치면 되고, 상태가 여러 개로 늘어도 자바스크립트가 지저분해지지 않아요."


2. 요소를 "만드는 것"과 "화면에 보이는 것"은 왜 다를까?

문제 상황 요약

createElement<li>를 만들어도 화면엔 안 보였고, append로 부모에 붙여야 비로소 나타났어요. "만들면 곧 보이는" 게 아니라 만드는 단계와 붙이는 단계가 따로였죠. 이 둘이 분리된 게 불편해 보일 수도 있는데, 사실 큰 장점이 돼요. 그 장점은 무엇일까요?

튜터의 가이드 및 해설

브라우저는 화면에 붙어 있는 요소가 바뀔 때마다, 위치와 크기를 다시 계산하고 다시 그려요. 이 다시 계산·다시 그리기가 공짜가 아니에요. 자주 일어날수록 화면이 버벅이죠. 그래서 "언제 화면에 붙이느냐"가 성능에 꽤 영향을 줘요.

댓글 100개를 다는 상황을 떠올려보세요. 만약 만들기와 붙이기가 한 몸이라 만들자마자 화면에 붙는다면, 100번 붙는 동안 브라우저가 100번 다시 계산하고 그릴 수 있어요. 반면 만들기와 붙이기가 나뉘어 있으면, 100개를 메모리에서 다 조립해둔 뒤 마지막에 한 번만 화면에 붙일 수 있어요. 그러면 다시 그리기가 한 번으로 줄죠.

바로 이게 두 단계가 분리된 덕분에 얻는 자유예요. "메모리에서 마음껏 조립하고, 완성되면 그때 붙인다." 화면에 안 붙은 요소를 만지는 동안엔 브라우저가 다시 그릴 일이 없으니, 복잡한 구조도 부담 없이 미리 만들어둘 수 있어요.

  • 하나씩 만들어 그때그때 붙이기: 코드가 단순하고, 댓글 한두 개면 차이가 안 느껴져요. 하지만 수십·수백 개를 이렇게 붙이면 다시 그리기가 그만큼 반복돼 느려질 수 있어요.
  • 모아서 한 번에 붙이기: 메모리에서 다 조립한 뒤 마지막에 한 번 붙여요. 다시 그리기를 최소로 줄여 빠르고 화면도 덜 흔들려요. 자바스크립트엔 이렇게 "여러 요소를 모았다가 한 번에 붙이는" 임시 바구니(DocumentFragment)도 준비돼 있어요.
  • 현업에서는 보통: 목록을 한꺼번에 그릴 땐 모아서 한 번에 붙여요. "만들기 ≠ 붙이기"가 나뉜 덕분에 가능한 최적화고, 목록이 길수록 효과가 커져요.

🎯 면접관을 홀리는 핵심 멘트

"createElement는 메모리에 요소를 만들 뿐이고, append로 트리에 붙는 순간 화면에 나타나요. 이 둘이 분리돼 있어서, 댓글 100개 같은 목록을 그릴 때 메모리에서 다 조립한 뒤 마지막에 한 번만 붙일 수 있어요. 화면에 붙을 때마다 브라우저가 다시 계산하고 그리는데, 붙이는 횟수를 한 번으로 줄이면 그만큼 빠르고 화면도 덜 흔들리죠. '만들기'와 '붙이기'가 따로인 게 불편이 아니라 최적화의 여지예요."

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.