
DOM (Document Object Model)
: HTML 요소를 자바스크립트 객체 (JavaScript Object)처럼 조작 (Manipulate)할 수 있는 Model.
// div 요소 만들기
document.createElement('div')

현재
tweetDiv라는 div 요소는createElement에 의해 노드가 생성됐다. 하지만, DOM 트리에 연결되어 있지 않아서 화면에서는 보이지 않는 상태다.
DOM으로 HTML 엘리먼트 정보를 조회하기 위해선 querySelector 메서드를 이용한다. 여러 엘리먼트를 한 번에 가져오기 위해서는 querySelectorAll 메서드를 사용하면 유사 배열로서 조회할 수 있다.
// 클래스 이름이 tweet인 HTML 엘리먼트 중 첫 번째를 조회
const oneTweet = document.querySelector('.tweet')
// 클래스 이름이 tweet인 모든 HTML 엘리먼트를 유사 배열로 조회
const tweets = document.querySelectorAll('.tweet')
// 가장 많이 쓰이는 Selector 태그
HTML : "div"
id : "#tweetList"
class : ".tweet"
querySelector는 이전 버전 브라우저와 호환되지 않을 수 있다. 예전 방식의 DOM 조작법은 다음과 같다.
// id가 container인 HTML 엘리먼트를 조회
const getOneTweet = document.getElementById('container')
// 위와 동일
const queryOneTweet = document.querySelector('#container')
// 둘은 완벽하게 같다
console.log(getOneTweet === queryOneTweet) // true
노드를 DOM 트리에 연결시키기 위해서는 append라는 메서드를 사용해야한다.
// 변수 tweetDiv를 container에 할당하기
const container = document.querySelector('#container')
const tweetDiv = document.createElement('div')
container.append(tweetDiv)


append 메서드에 의해 DOM 트리에 잘 연결되었으나, div 태그 안에 아무 내용도 없기 때문에 아무것도 보이지 않은 상태다.
textContent 를 사용하여, 비어있는 div 엘리먼트에 문자열을 입력할 수 있다.
console.log(oneDiv) // <div></div>
oneDiv.textContent = 'dev';
console.log(oneDiv) // <div>dev</div>
하지만, 현재 div 엘리먼트에 클래스 이름이 없기 때문에 CSS 스타일링이 적용되지 않는다. div 엘리먼트에 class를 추가하기 위해선 classList.add 를 사용한다.
oneDiv.classList.add('tweet')
console.log(oneDiv) // <div class="tweet">dev</div>
이제 append로 class가 생성된 div를 container의 자식 요소로 추가한다
const container = document.querySelector('#container')
container.append(oneDiv)

위 방식 이외에도 setAttribute 메서드를 사용하면, class와 id 그리고 다른 속성까지 할당할 수 있다.
// HTML
<button>This is Button</button>
// JS (DOM 조작)
var b = document.querySelector("button");
b.setAttribute("name", "helloButton");
// 이후 HTML
<button name="helloButton">This is Button</button>
다음은 DOM을 이용하여 엘리먼트를 삭제하는 법이다. 먼저, 삭제하려는 엘리먼트의 위치를 아는 경우 remove 메서드로 해당 엘리먼트를 삭제한다.
// id가 container인 엘리먼트 선택
const container = document.querySelector('#container')
// 아래 tweetDiv를 추가
const tweetDiv = document.createElement('div')
container.append(tweetDiv)
// ** tweetDiv를 제거
tweetDiv.remove()

여러 개의 자식 엘리먼트를 삭제하는 방법으로 여러가지가 있다. innerHTML 을 이용하면 쉽게 구현할 수 있지만 권장되지 않는 방법이다.
// id가 container인 엘리먼트의 모든 자식을 삭제
document.querySelector('#container').innerHTML = '';
innerHTML 은 XSS(Cross-Site Scripting) 공격에 취약하기 때문에 사용을 지양해야한다. XSS 공격이란 게시판이나 웹 메일 등에 스크립트 코드를 삽입하여 개발자가 의도하지 않은 악의적인 기능이 작동하게 하는 치명적인 공격이다. HTML5에서 innerHTML 안에 삽입된 <script> 태그가 실행되지 않도록 수정했지만, 여전히 다른 공격 루트가 존재한다.
대신, 다른 삭제하는 방법들이 존재한다. removeChild를 반복문을 통해 여러 엘리먼트를 삭제할 수 있다.
const container = document.querySelector('#container');
// 첫 번째 자식 엘리먼트가 존재하면, container의 첫 번째 자식 엘리먼트를 제거
while (container.firstChild) {
container.removeChild(container.firstChild);
}

모든 자식 엘리먼트가 제거됐지만, “Tweet List”라는 제목까지 없어지는 부작용이 있다. 이를 해결하기 위해서 여러 방법이 있다.
const container = document.querySelector('#container');
while (container.children.length > 1) {
container.removeChild(container.lastChild);
}
const tweets = document.querySelectorAll('.tweet')
tweets.forEach(function(tweet){
tweet.remove();
})
// or
for (let tweet of tweets){
tweet.remove()
}


항상 React로만 웹 개발을 하다가 정말 오랜만에 DOM을 이용하여 간단한 회원가입 페이지를 구현해보았다. state라는 개념 없이, HTML / CSS를 조작하는 방식으로 개발을 하니 너무 적응이 안되고 불편했다. 각각의 HTML 엘리먼트를 조작하기 위해, Selector로 선택을 해야하고 classList.add 나 remove 와 같은 메서드를 호출하니 간단한 기능이지만 코드가 꽤나 길어졌다.
코드를 조금이라도 줄이기 위해서 유효성 검사 시, 클래스 이름을 조작하는 메서드의 코드들을 하나의 함수로 만들었다.
// 유효성 검사 실패 시, 실행하는 함수
const showFailure = (inputSelector, errorSelector) => {
// 인풋창 테두리를 빨간색으로 변경
inputSelector.classList.add("failure");
// 성공 시, 파란색이었던 인풋창 테두리를 제거
inputSelector.classList.remove("success");
// 에러 메시지를 빨간색으로 변경
errorSelector.classList.add("failure");
// 에러 메시지를 보이게 함
errorSelector.classList.remove("hide");
// 마지막 회원가입 버튼을 누를 시, boolean을 리턴하여 어떤 값이 잘못되었는지 알려줌
return false;
};