자바스크립트 실전 #1 DOM 조작 기본

자바스크립트 트랙의 마지막 — 실전 시리즈입니다. 기초/중급/고급에서 다진 도구를 묶어 바닐라 자바스크립트로 동적 웹 페이지를 만듭니다.

총 6편으로 구성됩니다.

  • #1 DOM 조작 기본 ← 이번 글
  • #2 이벤트 핸들링과 위임
  • #3 fetch와 비동기 UI
  • #4 폼 다루기 — validation, FormData
  • #5 로컬 스토리지와 가벼운 상태 관리
  • #6 작은 앱 빌드 — Todo 또는 라이브 검색

이번 글에서는 DOM을 만지는 기본을 다룹니다. 페이지의 엘리먼트를 찾고, 내용/속성/클래스를 바꾸고, 새 엘리먼트를 만들어 끼워넣는 단계입니다.

DOM이 무엇인가? #

**DOM (Document Object Model)**은 브라우저가 HTML을 자바스크립트가 다룰 수 있는 객체 트리로 변환한 것입니다. <h1>, <p> 같은 태그가 모두 자바스크립트 객체가 됩니다.

간단한 HTML
<body>
  <h1 id="title">안녕</h1>
  <p class="message">본문</p>
</body>

이걸 자바스크립트가 보면:

DOM 트리
document
  └─ html
      └─ body
          ├─ h1#title  "안녕"
          └─ p.message  "본문"

document가 진입점이고, 트리 구조로 모든 엘리먼트에 접근할 수 있습니다.

엘리먼트 찾기 — querySelector 계열 #

가장 자주 쓰는 메서드.

요소 찾기
// CSS 선택자 — 한 개
const title = document.querySelector('#title');
const firstP = document.querySelector('p');
const greenBtn = document.querySelector('.btn.green');

// CSS 선택자 — 여러 개 (NodeList)
const allP = document.querySelectorAll('p');
const items = document.querySelectorAll('.item');

CSS 선택자 그대로 쓸 수 있어 직관적입니다. 옛날에는 getElementById, getElementsByClassName, getElementsByTagName이 따로 있었는데, 모던 코드에서는 거의 querySelector만 씁니다.

querySelectorAll의 NodeList — 배열이 아님 #

NodeList 는 배열이 아님
const items = document.querySelectorAll('.item');

items.length;       // OK — 길이 있음
items.forEach(...);  // OK — forEach 가능

items.map(...);      // ✗ map 없음
items.filter(...);   // ✗ filter 없음

// 진짜 배열로 만들려면
const arr = [...items];           // OK — spread
const arr2 = Array.from(items);   // OK

forEach는 NodeList 자체에 있지만, map/filter/reduce는 없습니다. 배열 메서드가 필요하면 spread 또는 Array.from으로 변환합니다.

부모/자식/형제 — 트리 탐색 #

트리 탐색
const el = document.querySelector('.item');

// 부모
el.parentElement;
el.parentNode;       // 거의 동일 (drift 케이스 있음)

// 자식
el.children;          // HTMLCollection — 엘리먼트만
el.childNodes;        // NodeList — 텍스트/주석 포함
el.firstElementChild;
el.lastElementChild;

// 형제
el.nextElementSibling;
el.previousElementSibling;

// 가장 가까운 조상 (자기 포함)
el.closest('.container');

closest는 자주 씁니다 — 이벤트 위임(#2) 패턴의 핵심입니다.

내용 바꾸기 — textContent vs innerHTML #

텍스트 vs HTML
const el = document.querySelector('#title');

// 텍스트만 — 안전
el.textContent = '새 제목';

// HTML 파싱 — 위험할 수 있음
el.innerHTML = '<strong>새 제목</strong>';

textContent는 텍스트로만 들어갑니다. 사용자 입력이나 외부 데이터를 넣어도 안전합니다.

innerHTML은 HTML로 파싱됩니다. <strong> 태그가 진짜 강조로 렌더되지만, 신뢰할 수 없는 데이터를 넣으면 XSS 공격에 취약해집니다.

XSS 위험
const userInput = '<img src=x onerror="alert(1)">';

el.textContent = userInput;   // 안전 — 그냥 문자열
el.innerHTML = userInput;     // ✗ 스크립트 실행됨

기본은 textContent. HTML이 진짜 필요하면 그 부분에서만 innerHTML을 신중하게 쓰세요.

속성 다루기 #

일반 속성 #

속성
const link = document.querySelector('a');

link.href;                          // 직접 속성으로 접근
link.href = 'https://...';
link.getAttribute('href');           // 동일
link.setAttribute('href', 'https://...');
link.removeAttribute('href');

link.hasAttribute('target');         // true/false

href, src, id 같은 흔한 속성은 직접 점 접근이 더 짧습니다. 잘 알려지지 않은 속성이나 동적 키는 getAttribute/setAttribute.

데이터 속성 — data-* #

HTML에 자바스크립트용 데이터를 박을 때.

HTML
<button data-id="42" data-action="delete">삭제</button>
dataset 으로 접근
const btn = document.querySelector('button');

btn.dataset.id;        // '42' (항상 string)
btn.dataset.action;    // 'delete'

btn.dataset.id = '100';   // 속성 갱신 → HTML 도 바뀜

camelCase 변환에 주의 — HTML의 data-user-name은 JS에서 dataset.userName으로 접근합니다.

클래스 다루기 — classList #

classList
const el = document.querySelector('.box');

el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');                 // 있으면 빼고, 없으면 추가
el.classList.toggle('disabled', isDisabled); // 두 번째 인자로 강제

el.classList.contains('active');   // boolean
el.classList.replace('a', 'b');

classList는 모던 자바스크립트의 표준 도구. 옛날의 el.className = 'a b c' (문자열로 직접 다루기) 보다 안전하고 가독성도 좋습니다.

스타일 다루기 #

인라인 스타일 #

style 속성
const el = document.querySelector('.box');

el.style.color = 'red';
el.style.backgroundColor = '#eee';   // CSS 의 background-color 는 JS 에서 camelCase
el.style.transform = 'translateX(10px)';

하지만 인라인 스타일은 남용하지 마세요. CSS 클래스를 토글하는 게 거의 항상 깔끔합니다.

권장 — 클래스 토글
el.classList.add('error');
// CSS 에서 .error { color: red; ... } 정의

CSS 변수 다루기 #

CSS 커스텀 속성(--var) 은 setProperty로 동적으로 바꿀 수 있습니다.

CSS 변수
el.style.setProperty('--main-color', '#ff5733');
const value = getComputedStyle(el).getPropertyValue('--main-color');

테마 변경, 동적 스타일링에 적합합니다.

엘리먼트 만들고 끼워넣기 #

새 엘리먼트 만들기 #

createElement
const div = document.createElement('div');
div.className = 'item';
div.textContent = '새 항목';

끼워넣기 — append / prepend / before / after #

끼워넣기 메서드
const parent = document.querySelector('.list');
const item = document.createElement('li');

parent.append(item);                 // 끝에 추가
parent.prepend(item);                // 앞에 추가
existingChild.before(item);          // 기존 자식 앞에
existingChild.after(item);           // 기존 자식 뒤에
existingChild.replaceWith(item);     // 자식 교체

옛 메서드(appendChild, insertBefore)도 동작하지만 — 새 메서드들이 더 짧고 직관적이라 모던 코드에서는 이쪽을 사용합니다.

한 번에 여러 엘리먼트 #

여러 자식 한 번에
parent.append(li1, li2, li3);
parent.append('텍스트도 가능', divEl, '\n');

문자열도 함께 받아 자동으로 텍스트 노드로 변환해 줍니다.

제거하기 — remove #

자기 자신 제거
el.remove();

옛 코드의 el.parentNode.removeChild(el)보다 훨씬 짧습니다.

효율 — DocumentFragment #

여러 엘리먼트를 한꺼번에 추가할 때, 트리에 한 번 끼울 때마다 reflow가 일어나면 비싸요. **DocumentFragment**가 임시 컨테이너 역할을 해 줍니다.

여러 자식 일괄 추가
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.append(li);   // fragment 에 일단 모음
}

list.append(fragment);   // 한 번만 진짜 트리에 끼움 — reflow 한 번

큰 차이는 100개 정도부터 보입니다. 대부분의 경우에는 의식할 필요가 없지만, 큰 리스트를 그릴 때 알아두면 좋습니다.

innerHTML + 템플릿 리터럴 — 짧지만 신중하게 #

innerHTML로 한 번에
list.innerHTML = items
  .map((item) => `<li class="item">${item.name}</li>`)
  .join('');

이건 매우 짧지만 — ${item.name}이 사용자 입력이라면 XSS 위험이 있습니다. 신뢰 가능한 데이터에만, 또는 escape 처리를 거친 뒤에 쓰세요.

엘리먼트 정보 #

자주 쓰는 정보
el.tagName;              // 'DIV' (대문자)
el.id;
el.className;            // 문자열 — 권장은 classList

el.getBoundingClientRect();   // 화면상 위치/크기
el.offsetWidth;
el.offsetHeight;
el.offsetTop;
el.offsetLeft;

el.scrollTop;
el.scrollLeft;

레이아웃이나 스크롤을 다룰 때 자주 만나요.

closest의 활용 #

위 트리 탐색에서 잠깐 본 메서드. 이게 이벤트 핸들링에서 매우 자주 쓰여서 한 번 더 정리.

closest 패턴
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (!item) return;

  const id = item.dataset.id;
  // ...
});

리스트 안의 어디를 클릭하든, 가장 가까운 .item을 찾아 id를 끌어옵니다. 다음 글에서 본격적으로 다룰 이벤트 위임 패턴의 기반입니다.

마무리 #

이번 글에서 정리한 내용:

  • DOM은 HTML을 객체 트리로 만든 자바스크립트의 인터페이스
  • querySelector / querySelectorAll가 모던 표준
  • NodeList는 배열이 아님 — 배열 메서드 필요하면 spread/Array.from
  • textContent가 안전, innerHTML은 신뢰 가능한 데이터에만
  • dataset으로 data-* 속성 다루기
  • classList.add/remove/toggle이 클래스 표준
  • append/prepend/before/after가 모던 끼워넣기
  • 큰 리스트는 DocumentFragment로 한 번에
  • closest로 가장 가까운 조상 찾기

다음 글(#2 이벤트 핸들링과 위임)에서는 이벤트 객체, 버블링과 캡처링, 그리고 효율적인 이벤트 위임 패턴을 다룹니다.

X