[그림] HTML과 DOM의 구조
DOM은 프로그래머 관점에서 바라 본 HTML 입니다. DOM을 이해하고 조작할 수 있으면, HTML을 단순한 문서에서 웹 앱으로 업그레이드할 수 있습니다. DOM은 브라우저 환경에서 자바스크립트를 이용해 HTML을 조작할 수 있습니다. HTML 문서에 이미 작성되어 있는 엘리먼트에 접근하거나, 새로운 엘리먼트를 생성 또는 삭제할 수 있습니다.
DOM은 Document Object Model의 약자로, HTML 요소를 Object(JavaScript Object)처럼 조작(Manipulation)할 수 있는 Model입니다. 즉, 자바스크립트를 사용할 수 있으면, DOM으로 HTML을 조작할 수 있습니다.
HTML을 조작할 수 있다는게 무슨 뜻일까요? DOM을 위해 여러 뛰어난 웹 개발자들이 모여, HTML을 철저히 분석했습니다. 분석한 내용으로 HTML의 아주 작은 부분까지 접근할 수 있는 구조(Model; Structure)를 만들어 냈습니다. 이렇게 만들어진 구조를 이용하여 HTML로 구성된 웹 페이지를 동적으로 움직이게 만들 수 있습니다. 앞서 학습한 조건문과 반복문, 배열, 객체를 활용하면 SNS에서 새롭게 생성되는 게시물을 저장하고 분류하는 작업을 할 수 있습니다.
HTML에 JavaScript를 적용하기 위해서는 <script>
태그를 이용합니다. 아래의 경우 HTML 파일과 같은 디렉토리에 존재하는 myScriptFile.js
을 불러옵니다.
<script src="myScriptFile.js"></script>
웹 브라우저가 작성된 코드를 해석하는 과정에서 <script>
요소를 만나면, 웹 브라우저는 HTML 해석을 잠시 멈춥니다. HTML 해석을 잠시 멈춘 웹 브라우저는 <script>
요소를 먼저 실행합니다. <script>
요소는 등장과 함께 실행된다는 사실을 꼭 기억하세요.
이
<script>
태그를 추가하는 두 가지 대표적인 사례가 존재합니다. 하나는 <head>
태그에 추가하는 방법, 다른 하나는 </body>
가 끝나기 전에 추가하는 방법입니다.
<script>
태그를 head에 넣는 것과 body가 끝나기 전에 넣는 것의 차이
브라우저의 동작 방식
여기서 주목해야 할 부분은 1, 2, 3의 순서입니다. HTML을 파싱한 다음 DOM 트리를 생성하죠.그런데 브라우저는 HTML 태그들을 읽어나가는 도중 <script>
태그를 만나면 파싱을 중단하고 javascript 파일을 로드 후 javascript 코드를 파싱합니다. 완료되면 그 후에 HTML 파싱이 계속 됩니다.
이로인해 HTML태그들 사이에 script 태그가 위치하면 두가지 문제가 발생합니다.
* 위와 같은 상황을 막기 위해 script 태그는 body 태그 최하단에 위치하는 게 가장 좋습니다.
<html>
<body>
<div id="nav">
<div class="logo"></div>
<div class="menu-wrapper">
<div class="menu"></div>
<div class="menu"></div>
<div class="menu"></div>
<div class="profile-photo"></div>
</div>
</div>
<div id="news-contents">
<div class="news-content-wrapper">
<div class="news-picture"></div>
<div class="news-title"></div>
<div class="news-description"></div>
</div>
</div>
<div id="footer"></div>
</body>
</html>
위 질문에 대한 답을 구하는 방법은 두 가지가 있습니다. HTML을 코드를 보며 답변하는 방법과, 컴퓨터가 답변을 찾을 수 있도록 코드를 작성하는 방법입니다. 컴퓨터가 실행할 수 있도록 코드로 옮기는 방법은 HTML 상에서는 쉽지 않습니다.
HTML은 프로그래밍을 위해서 만들어진 언어가 아니기 때문에 이전에 배웠던 조건문이나 반복문을 사용할 수 없고, 정보를 저장하기에도 적합한 언어가 아닙니다. 그래서 자바스크립트라는 프로그래밍 언어와 DOM을 활용하여 HTML에 접근하고 조작합니다.
body 엘리먼트의 자식 엘리먼트(element)는 총 몇 개인가요?
body 엘리먼트의 자식 엘리먼트는 총 3개입니다. id가 nav
, news-contents
, footer
인 3가지 엘리먼트입니다.
그림으로 표현하면, 쉽게 이해할 수 있습니다. 그러나 컴퓨터에게는 이 사실을 어떻게 전달할까요? 자바스크립트에서 DOM은
document
객체에 구현되어 있습니다. 브라우저에서 작동되는 자바스크립트 코드에서는, 어디에서나 document
객체를 조회할 수 있습니다. 한번, body
를 찾아보겠습니다.
DOM 구조를 조회할 때에는
console.dir
이 유용합니다. console.dir
은 console.log
와 달리 DOM을 객체의 모습으로 출력합니다. 앞으로 console.log
와 console.dir
의 차이를 구분해서 사용할 일이 계속 있습니다.
console.dir
을 이용해 document.body
를 조회해보니, 너무나 많은 속성이 나타납니다. 앞서 학습한 내용을 상기해보면, HTML 엘리먼트에 지정할 수 있었던 다양한 속성이 이미 객채 내에 존재한다고 생각하면 됩니다.
문제에 대한 답을 찾기 위해, 찾고자 하는 자식 엘리먼트를 찾아보세요.
console.dir(document.body)
를 통해 출력된 객체에서, children
속성을 찾을 수 있습니다. children
속성에 nav
, news-contents
, footer
가 자식으로 있는 것을 확인할 수 있습니다. 물론 document.body.children
으로 바로 조회할 수도 있습니다.
id의 이름이 news-contents 인 div 엘리먼트의 부모 엘리먼트는 무엇인가요?
id가
news-contents
인 div
엘리먼트는 body
엘리먼트의 자식 엘리먼트입니다. 반대로 body
엘리먼트는 id가 news-contents div
엘리먼트의 부모 엘리먼트입니다. 이 관계를 자바스크립트에서 확인합니다. id가 news-contents
인 엘리먼트를 조회하려면, document.body.children
의 첫 번째 엘리먼트를 조회합니다.
document.body
의 children
을 조회할 때마다, 매번 document.body
로부터 찾아가는 일은 정말 번거롭습니다. 따로 변수 선언을 해서 이 정보를 저장해두면, 주소를 참조하기때문에 언제든지 접근할 수 있습니다. 변수 newsContents
를 따로 선언하여 id가 news-contents
인 엘리먼트를 할당합니다.
* 부모 엘리먼트 찾는 법
getElementById
를 이용해 해당 ID를 갖는 요소에 접근합니다.
document.getElementById("요소의 Id값");
부모는 parentNode 속성을 통해 접근합니다.
id의 이름이 nav인 div 엘리먼트를 포함해서, 모든 자식 엘리먼트의 class 이름을 console.log를 사용하여 확인하는 방법을 수도코드(pseudocode)로 작성하세요.
DOM 구조에서도 회사의 조직도와 유사한 모습을 발견할 수 있습니다. body가 가장 상위에 있고, 아래에 여러 구성요소가 부모-자식 관계를 가지고 있습니다.
이런 자료 구조를 컴퓨터 공학에서는 트리 구조라고 합니다. 트리 구조의 가장 큰 특징은 부모가 자식을 여러 개 가지고, 부모가 하나인 구조가 반복되는 점입니다. 즉, 부모가 가진 하나 또는 여러 개의 자식 엘리먼트를 조회하는 코드를 작성한다면, 여러 번 반복해서 실행하는 코드가 필요합니다.
function consoleLogAllElement(element){
// nav의 class 이름을 console.log 합니다.
// nav의 자식 엘리먼트가 있는지 검색합니다. (logo, menu-wrapper)
//logo의 class 이름을 console.log 합니다.
//logo의 자식 엘리먼트가 있는지 검색합니다. (없음)
//menu-wrapper의 class 이름을 console.log 합니다.
//menu-wrapper의 자식 엘리먼트가 있는지 검색합니다. (menu, menu, menu, profile-photo)
//첫 번째 menu의 class 이름을 console.log 합니다.
//첫 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
//두 번째 menu의 class 이름을 console.log 합니다.
//두 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
//세 번째 menu의 class 이름을 console.log 합니다.
//세 번째 menu의 자식 엘리먼트가 있는지 검색합니다. (없음)
//profile-photo의 class 이름을 console.log 합니다.
//profile-photo의 자식 엘리먼트가 있는지 검색합니다 (없음)
//자식 엘리먼트를 모두 탐색했음으로, 함수 실행이 종료됩니다.
//자식 엘리먼트를 모두 탐색했음으로, 함수 실행이 종료됩니다.
}
document 객체에는 많은 속성과 메소드가 존재합니다. 모든 속성과 메소드를 외워야 할까요? 지금 당장 전부 알아야 할 필요는 없습니다. 지금 집중할 부분은 CRUD(Create, Read, Update and Delete) 입니다. 앞으로 어떤 종류의 개발이나 컴퓨터 언어를 배우시더라도 가장 먼저 CRUD에 집중해야 합니다. CRUD를 먼저 이해하는 것이 새로운 언어를 가장 빠르게 학습하는 방법입니다. (CRUD를 이해한 다음에는 다양한 trivia를 알아야 합니다.)
document.createElement('div')
[코드] 새로운 div element를 만듭니다.
자바스크립트에서 어떤 작업의 결과를 담으려면 어떻게 해야 할까요? 변수를 선언하고 어떤 작업의 결과를 변수에 할당합니다. 여기서는 div element를 변수 tweetDiv 에 할당합니다.
const tweetDiv = document.createElement('div')
tweetDiv 라는 요소는 현재 공중부양 중입니다. DOM의 구조를 나타내는 트리 구조를 하나 그립니다. 아무것도 연결이 되어있지 않은 하나의 노드를 그립니다.
위 그림처럼 공중에 떠있는 엘리먼트를 확인하기 위해서는 APPEND 해야합니다. APPEND 를 이용해 실제 웹 페이지 상에도 보이는 것을 확인할 수 있습니다.
이전 콘텐츠 CREATE에서 생성한 tweetDiv를 트리 구조와 연결합니다.
CREATE에서는 다음과 같이 tweetDiv 를 생성했습니다.
const tweetDiv = document.createElement('div')
[코드] 변수 tweetDiv에 새로운 div 엘리먼트를 할당합니다.
CREATE에서 만든 tweetDiv 라는 변수는 아직 "공중부양"을 하고 있습니다.
이번 콘텐츠에서는 append 라는 메소드를 사용해서, 변수 tweetDiv 를 body 에 넣어보겠습니다.
document.body.append(tweetDiv)
[코드] 변수 tweetDiv에 담긴 새로운 div 엘리먼트를 body 엘리먼트에 append 합니다.
자바스크립트에서 원시 자료형인 변수의 값을 조회하기 위해서는, 변수의 이름으로 직접 조회할 수 있습니다. 참조 자료형인 배열은 index를, 객체는 key를 이용해 값을 조회할 수 있습니다. 그러나 DOM은 조금 특별한 방법을 사용해야 합니다. DOM으로 HTML 엘리먼트의 정보를 조회하기 위해서는 querySelector의 첫 번째 인자로 셀렉터(Selector)를 전달하여 확인할 수 있습니다. 셀렉터로는 HTML 태그("div"
), id("#tweetList"
), class(.tweet
) 세 가지가 가장 많이 사용됩니다.
참고로 querySelector는 셀렉터를 조회한다는 의미를 가지고 있습니다. query의 의미가 "질문하다"라는 것을 알고 있다면 역할을 쉽게 유추하실 수 있습니다. 이 query라는 단어는 개발자 간에 "ㅇㅇㅇ를 조회한다"라는 의미를 "쿼리를 날리다"라는 jargon(특정 영역에서만 사용되는 단어)로 굳어졌기 때문에 알고 있어야 합니다.
querySelector
에 '.tweet'
을 첫 번째 인자로 넣으면, 클래스 이름이 tweet
인 HTML 엘리먼트 중 첫 번째 엘리먼트를 조회할 수 있습니다.
const oneTweet = document.querySelector('.tweet')
[코드] querySelector로 클래스 이름이 tweet인 HTML 엘리먼트를 조회합니다.
HTML 문서에는 클래스 이름이 tweet 인 엘리먼트가 여러 개 있는 데, 변수 oneTweet
에 할당된 엘리먼트는 단 하나입니다. 여러 개의 엘리먼트를 한 번에 가져오기 위해서는, querySelectorAll
을 사용합니다. 이렇게 조회한 HTML 엘리먼트들은 배열처럼 for문을 사용하실 수 있습니다. 주의하세요! 앞서 조회한 HTML 엘리먼트들은 배열이 아닙니다! 이런 '배열 아닌 배열'을 유사 배열, 배열형 객체 등 다양한 이름으로 부릅니다. 정식 명칭은 Array-like Object 입니다. Array-like Object 같이 개념을 설명하는 용어는 영어로도 명확하게 기억해두는 게 좋습니다.
const tweets = document.querySelectorAll('.tweet')
[코드] querySelectorAll로 클래스 이름이 tweet 인 모든 HTML 엘리먼트를 유사 배열로 받아옵니다.
querySelector
와 querySelectorAll
만 알아도 대부분의 엘리먼트를 조회할 수 있습니다. 다만, 여러분이 어떤 회사에 개발자로 입사한 다음, 아래와 같이 get
으로 시작하는 DOM 조회 메소드를 볼 수도 있습니다. 이런 메소드는 querySelector
와 비슷한 역할을 하는 오래된 방식이라고만 이해하면 됩니다. 만약 이전 버전의 브라우저(인터넷 익스플로러) 호환성을 신경 써야 한다면, 이런 옛날 방식을 사용해야 할 수도 있습니다. 실제 동작은 동일하니 이런 메소드가 있다는 것 정도는 알아두세요.
const getOneTweet = document.getElementById('container')
const queryOneTweet = document.querySelector('#container')
console.log(getOneTweet === queryOneTweet) // true
[코드] getElementById와 querySelector로 각각 받아 온 container 요소는 하나의 요소입니다.
CREATE에서 생성한 div 엘리먼트를 container에 넣을 준비를 마쳤습니다. 다음 코드를 입력하면, container의 맨 마지막 자식 엘리먼트로 tweetDiv를 추가합니다.
const container = document.querySelector('#container')
const tweetDiv = document.createElement('div')
container.append(tweetDiv)
[코드] tweetDiv를 container의 마지막 자식 요소로 추가합니다.
[그림] id가 container인 엘리먼트의 마지막 자식 요소로 tweetDiv를 추가합니다.
앞선 콘텐츠 CREATE, APPEND, READ를 통해 새로운 DOM 객체를 만들고, 기존의 DOM 객체에 붙이고, DOM 객체를 선택해서 조회하는 방법을 학습했습니다. 이번 콘텐츠 UPDATE에서는 기존에 생성한 빈 div 태그를 업데이트하여, 보다 다양한 작업을 할 수 있습니다. textContent
를 사용해서, 비어있는 div 엘리먼트에 문자열을 입력합니다.
console.log(oneDiv) // <div></div>
oneDiv.textContent = 'dev';
console.log(oneDiv) // <div>dev</div>
[코드] textContent를 이용해 문자열을 입력합니다.
앞서 생성한 div 엘리먼트를 container에 append 했을 때, CSS 스타일링이 적용되지 않았습니다. CSS 스타일링이 적용될 수 있도록, div 엘리먼트에 class
를 추가합니다.
oneDiv.classList.add('tweet')
console.log(oneDiv) // <div class="tweet">dev</div>
[코드] classList.add를 이용해 'tweet' 클래스를 추가합니다.
생성한 엘리먼트에 텍스트를 채웠고, 클래스를 추가하여 스타일링을 적용했습니다. 이번에는 append를 이용해 container의 자식 요소로 추가합니다.
const container = document.querySelector('#container')
container.append(oneDiv)
[코드] append를 이용해 container의 자식 요소에 oneDiv를 추가합니다.
새롭게 추가한 엘리먼트는 클래스 tweet 의 스타일이 적용된 상태로 출력됩니다.
CRUD의 Delete, 삭제하는 법을 학습합니다. 삭제하는 방법에도 여러 가지가 있습니다. 먼저 삭제하려는 엘리먼트의 위치를 알고 있는 경우에 사용하는 방법입니다. 앞서 생성하고 추가한 tweetDiv
를 삭제합니다. remove
메소드를 사용하세요.
const container = document.querySelector('#container')
const tweetDiv = document.createElement('div')
container.append(tweetDiv)
tweetDiv.remove() // 이렇게 append 했던 엘리먼트를 삭제할 수 있다.
[코드] id가 container인 엘리먼트 아래에 tweetDiv를 추가하고, remove로 삭제합니다.
[그림] tweetDiv를 container의 자식으로 추가한 뒤, 삭제합니다.
여러 개의 자식 엘리먼트를 지우려면, 어떻게 해야 할까요? innerHTML
을 이용하면, 아주 간단하게 모든 자식 엘리먼트를 지울 수 있습니다. 컨테이너의 모든 자식 엘리먼트를 지우려면, 다음과 같이 입력합니다.
document.querySelector('#container').innerHTML = '';
[코드] id가 container인 엘리먼트 아래의 모든 엘리먼트를 지웁니다.
innerHTML
을 이용하는 방법은 분명 간편하고 편리한 방식이지만, innerHTML은 보안에서 몇 가지 문제를 가지고 있습니다. 이 방법을 대신할 다른 메소드를 사용합니다. removeChild
는 자식 엘리먼트를 지정해서 삭제하는 메소드입니다. 모든 자식 엘리먼트를 삭제하기 위해, 반복문(while, for, etc.)을 활용할 수 있습니다. 다음의 코드는 자식 엘리먼트가 남아있지 않을 때까지, 첫 번째 자식 엘리먼트를 삭제하는 코드입니다.
const container = document.querySelector('#container');
while (container.firstChild) {
container.removeChild(container.firstChild);
}
[코드] container의 첫 번째 자식 엘리먼트가 존재하면, 첫 번째 자식 엘리먼트를 제거합니다.
removeChild
와 while
을 이용해 자식 요소를 삭제하면, 제목에 해당하는 H2 "Tweet List"까지 삭제됩니다. 이를 방지하기 위한 방법은 여러 가지가 있습니다. 자식 요소가 담고 있는 문자열을 비교해 "Tweet List"만 남기거나, 새로운 변수를 생성하고 Tweet List를 할당해뒀다가 반복문이 끝난 뒤에 새롭게 추가할 수도 있습니다. 또는 자식 엘리먼트를 하나만 남기게 할 수도 있습니다.
const container = document.querySelector('#container');
while (container.children.length > 1) {
container.removeChild(container.lastChild);
}
[코드] container의 자식 엘리먼트가 1개만 남을 때까지, 마지막 자식 엘리먼트를 제거합니다.
또는 직접 클래스 이름이 tweet인 엘리먼트만 찾아서 지우는 방법도 있습니다.
const tweets = document.querySelectorAll('.tweet')
tweets.forEach(function(tweet){
tweet.remove();
})
// or
for (let tweet of tweets){
tweet.remove()
}
[코드] 클래스 이름이 tweet인 엘리먼트만 찾아서 제거합니다.