JavaScript DOM 요소 다루기
예제 기준 · JavaScript : 브라우저 기본 DOM API (별도 라이브러리 없음)
목록에 항목을 추가하거나 삭제할 때는 DOM 요소를 생성하고 원하는 위치에 연결합니다. 요소 생성·삽입·삭제부터 dataset으로 데이터 읽기, classList로 스타일 상태 바꾸기까지 살펴봅니다.
각 절의 코드는 해당 HTML 요소가 만들어진 뒤 독립적으로 사용합니다. 실행 예제는 script를 defer로 로드합니다. 본문은 주요 조작을 보여 주며, 화면의 결과·안내 출력과 초기화 코드는 필요한 부분만 설명합니다.
DOM 요소 생성하기
document.createElement('div')는 새로운 Element를 만듭니다. 생성만으로 화면에 추가되지는 않습니다. textContent로 내용을 넣고, 화면에 연결된 부모 요소에 append해야 문서에 들어갑니다.
생성·삽입·삭제 예제의 공통 구조는 다음과 같습니다. stage 안의 target이 ‘기준 요소’이며, 생성한 항목에는 dom-item 클래스를 붙입니다.
<div id="stage" class="dom-stage">
<div id="target" class="dom-target">기준 요소</div>
</div>
<button type="button" id="add">요소 추가</button>
let sequence = 0;
function createItem() {
sequence += 1;
const item = document.createElement('div');
item.classList.add('dom-item');
item.textContent = 'Test ' + sequence;
return item;
}
document.getElementById('add').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').append(item);
});
‘요소 추가’를 누를 때마다 Test 1, Test 2 순서로 target 내부 끝에 추가됩니다. 화면은 stage 안의 dom-item 개수와 마지막 생성 번호를 함께 표시합니다. ‘초기화’는 stage의 자식을 비우고 기준 요소를 새로 만든 뒤 번호를 0으로 되돌립니다.
원하는 위치에 요소 삽입하기
append와 prepend는 호출한 요소 내부에 자식으로 넣습니다. before와 after는 호출한 요소를 기준으로 앞뒤 형제 위치에 넣습니다.
| 호출 | 삽입 위치 |
|---|---|
| target.append(item) | target 내부 마지막 |
| target.prepend(item) | target 내부 처음 |
| target.before(item) | target 바로 앞, 같은 부모 stage 안 |
| target.after(item) | target 바로 뒤, 같은 부모 stage 안 |
위의 createItem 함수와 sequence 선언을 함께 사용합니다. 실행 예제에는 ID가 append·prepend·before·after인 버튼이 있으며, 버튼마다 새 요소를 만들어 삽입합니다.
document.getElementById('append').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').append(item);
});
document.getElementById('prepend').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').prepend(item);
});
document.getElementById('before').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').before(item);
});
document.getElementById('after').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').after(item);
});
초기화 후 append(), prepend(), before(), after()를 한 번씩 누르면 stage에는 Test 3, 기준 요소, Test 4가 놓입니다. 기준 요소 내부에는 Test 2, ‘기준 요소’ 텍스트, Test 1 순서로 들어갑니다. 화면의 항목 수는 내부·외부 위치를 모두 포함하여 4입니다.
DOM 요소 삭제하기
remove()는 호출한 요소를 DOM에서 제거합니다. 삭제 예제는 항목을 만들 때 ‘Test 1 삭제’처럼 번호가 포함된 버튼을 함께 붙이고, 그 버튼에서 해당 항목을 제거합니다.
let sequence = 0;
function createItem() {
sequence += 1;
const item = document.createElement('div');
item.classList.add('dom-item');
item.textContent = 'Test ' + sequence;
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.textContent = 'Test ' + sequence + ' 삭제';
removeButton.addEventListener('click', function () {
item.remove();
});
item.append(removeButton);
return item;
}
document.getElementById('add').addEventListener('click', function () {
const item = createItem();
document.getElementById('target').append(item);
});
삭제 버튼만 지우는 것이 아니라 버튼을 포함한 item 전체를 제거합니다. 실행 화면에서는 삭제 후 현재 항목 수도 갱신합니다. Test 1·Test 2를 만든 뒤 Test 1을 지우면 항목 수는 1, 마지막 생성 번호는 2입니다. 다시 추가하면 Test 3이 됩니다.
이 코드는 생성 시 보관한 item을 사용하므로 부모를 다시 찾을 필요가 없습니다. 버튼만 알고 있다면 부모를 확인한 뒤 제거할 수도 있습니다. 다음은 버튼이 dom-item의 직접 자식인 현재 구조에 맞춘 대체 코드이며, 위 삭제 핸들러 대신 사용합니다.
removeButton.addEventListener('click', function () {
const parent = removeButton.parentElement;
if (parent !== null && parent.classList.contains('dom-item')) {
parent.remove();
}
});
data-* 속성과 dataset 사용하기
dataset은 HTML의 data-* 속성을 JavaScript에서 읽고 바꾸는 인터페이스입니다. 실행 예제는 data-id="1", data-label="Apple"인 요소와 초기값이 Banana인 입력란을 사용합니다.
<input id="label" type="text" value="Banana" maxlength="80">
<div id="target" class="dom-target" data-id="1" data-label="Apple">
Apple
</div>
<button type="button" id="change">dataset 값 변경</button>
const target = document.getElementById('target');
console.log(target.dataset.id); // '1'
console.log(typeof target.dataset.id); // string
console.log(target.dataset.label); // Apple
document.getElementById('change').addEventListener('click', function () {
target.dataset.label = document.getElementById('label').value;
target.textContent = target.dataset.label;
console.log(target.getAttribute('data-label'));
});
‘dataset 값 변경’을 누르면 data-label과 표시 텍스트가 입력값으로 바뀝니다. dataset 변경만으로 글자가 자동 갱신되는 것은 아니며, 예제의 display 함수가 textContent도 바꿉니다. 출력에는 dataset.id, dataset.label, getAttribute 값, id 값의 typeof가 표시됩니다. 초기화하면 label은 Apple, 입력란은 Banana로 돌아갑니다.
존재하는 data-* 속성을 dataset으로 읽으면 문자열입니다. 숫자처럼 보이는 '1'도 문자열이며, 속성이 없으면 undefined입니다. 숫자를 계산에 쓰려면 변환과 유효성 확인을 별도로 해야 합니다.
data-를 제외한 이름에서 하이픈 뒤의 영문 소문자는 대문자로 바뀝니다. 따라서 data-user-id는 dataset.userId로 접근합니다. 다음은 이름 변환을 보여 주는 보충 코드이며 실행 화면에는 data-user-id가 없습니다.
const user = document.createElement('div');
user.dataset.userId = '123';
console.log(user.dataset.userId); // '123'
console.log(user.getAttribute('data-user-id')); // '123'
classList로 class 제어하기
classList는 class 문자열을 직접 조합하지 않고 클래스를 추가·삭제·토글할 때 사용합니다. 실행 예제의 target은 기본 dom-target 클래스를 유지하면서 is-active와 is-secondary를 제어합니다.
const target = document.getElementById('target');
document.getElementById('add').addEventListener('click', function () {
target.classList.add('is-active');
});
document.getElementById('remove').addEventListener('click', function () {
target.classList.remove('is-active');
});
document.getElementById('toggle').addEventListener('click', function () {
target.classList.toggle('is-active');
});
document.getElementById('replace').addEventListener('click', function () {
const replaced = target.classList.replace('is-active', 'is-secondary');
console.log('replace 결과: ' + replaced);
});
‘강조 추가’는 is-active를 붙이고 ‘강조 삭제’는 그것만 제거합니다. ‘강조 토글’은 없으면 추가하고 있으면 제거합니다. 아래는 실제 스타일의 색상 변수를 값으로 풀어 쓴 규칙입니다.
.js-demo .is-active {
background-color: #c2d8dc;
font-weight: 700;
}
.js-demo .is-secondary {
background-color: #c8ccd0;
border-color: #d26444;
}
강조 상태는 옅은 청록색 배경과 굵은 글씨입니다. ‘강조 → 보조색 교체’를 누르면 is-active가 is-secondary로 바뀌어 회색 배경과 주황색 테두리가 적용됩니다. is-active가 없으면 replace는 false를 반환하고 보조색을 새로 추가하지 않습니다.
각 버튼 처리 후 화면은 전체 class와 contains 결과를 갱신합니다. contains는 특정 클래스가 있는지 확인하는 boolean 값을 반환합니다.
const target = document.getElementById('target');
console.log(target.classList.contains('is-active'));
console.log(target.classList.contains('is-secondary'));
교체 후 ‘강조 삭제’를 눌러도 is-secondary는 남습니다. 다시 강조를 추가하면 두 클래스가 함께 존재할 수 있습니다. 이때 뒤에 선언된 보조색 배경이 적용되고 강조의 굵은 글씨는 유지됩니다. ‘초기화’는 두 상태 클래스를 모두 제거하여 기본 스타일로 돌아갑니다.
toggle의 반환값과 두 번째 인수를 활용하면 변경 후 상태를 확인하거나 특정 상태로 맞출 수 있습니다. 다음은 실행 화면의 버튼 처리와 별개인 보충 코드입니다.
const target = document.getElementById('target');
const active = target.classList.toggle('is-active');
console.log(active); // 변경 후 클래스가 있으면 true
target.classList.toggle('is-active', true); // 반드시 추가
target.classList.toggle('is-active', false); // 반드시 제거
DOM 조작 시 주의할 점
단순 텍스트를 표시할 때는 textContent를 우선 사용합니다. 문자열을 HTML로 해석하지 않으므로 태그처럼 보이는 입력도 텍스트로 표시합니다. innerHTML은 실제 HTML 구조를 해석해야 할 때 사용하며, 외부 입력을 검증·정제 없이 넣으면 XSS 같은 문제가 생길 수 있습니다.
다음 보충 코드는 target이 존재할 때 사용할 수 있습니다. 화면에는 strong 요소가 아니라 꺾쇠를 포함한 문자열이 표시됩니다.
const target = document.getElementById('target');
target.textContent = '<strong>Apple</strong>';
textContent에 값을 대입하면 기존 자식도 텍스트로 대체됩니다. 생성한 항목이나 삭제 버튼이 들어 있는 부모에 대입하면 그 자식이 사라지므로 바꿀 대상을 확인해야 합니다.
이미 DOM에 있는 요소를 append하면 복제되지 않고 이동합니다. 다음은 생성 예제에서 항목을 하나 이상 추가한 뒤 사용할 수 있는 보충 코드입니다. 첫 항목이 target 내부에서 stage의 마지막 자식으로 이동합니다.
const stage = document.getElementById('stage');
const item = document.querySelector('#target .dom-item');
if (item !== null) {
stage.append(item);
}
DOM 변경을 불필요하게 반복하면 스타일 계산과 화면 갱신 작업도 늘어날 수 있습니다. 변경할 값을 먼저 정리하고 필요한 부분만 갱신하며, 특히 크기·위치를 읽는 작업과 변경을 반복해서 섞지 않도록 합니다.
정리
| 작업 | 사용 방법 | 확인할 점 |
|---|---|---|
| 생성 | createElement·textContent | 생성만으로 문서에 연결되지는 않음 |
| 삽입 | append·prepend·before·after | 내부 자식 위치와 앞뒤 형제 위치 구분 |
| 삭제 | remove | 제거할 요소를 정확히 선택 |
| 데이터 | dataset | data-* 이름 변환과 문자열 값 |
| 스타일 상태 | classList | CSS 규칙과 클래스 존재 여부 확인 |
