- 학습 내용
이번 시간에 학습할 내용은 프로그래머 관점에서 HTML을 바라보는 방법인 DOM(Document Object Model)이다. DOM은 브라우저 환경에서 자바스크립트를 이용해 HTML을 조작하는 모델이다. 예를 들어 HTML 문서에 이미 작성되어 있는 엘리먼트에 접근하거나 새로운 엘리먼트를 생성 또는 삭제할 수 있다. 즉, HTML요소를 Object처럼 조작하는 방법이다.
일단 HTML에서 자바스크립트를 적용하여 DOM을 사용하기 위해서는 <script>태그를 사용해야 한다.
<script src="script.js"></script>
이렇게 하면 해당 HTML에서 script.js의 파일의 코드를 사용할 수 있다.
이렇게 script태그를 넣으면 웹 브라우저는 작성된 코드를 해석하다가 script태그를 만나면 HTML 해석을 잠시 멈춘다. 그리고 script태그 요소를 먼저 실행한다. 이런 점으로 인하여 script태그를 어디에 넣느냐는 중요한 선택사항이다. 대표적으로 2가지 방법이 있다.
1) <head> 안쪽에 삽입하는 경우
script 요소의 파일은 두 가지 과정을 거쳐 실행되는데, 다운로드(fetching)와 실행(execution) 과정을 거쳐야 한다. 그런데 이 방법은 큰 단점이 있다. 만약 <head> 안에 script태그를 삽입하게 되면 head태그 내부에 불러온 script태그의 요소를 다 읽은 후에야 body 내부의 내용을 UI로 사용자에게 보여주게 된다. 물론 크기가 작은 웹의 경우에는 차이가 거의 보이지 않을 수 있지만 용량이 크고 무거워지면 HTML을 파싱하다가 끝내지 못한 화면을 사용자에게 보여주게 될 수 있다. 이뿐만 아니라 HTML이 파싱이 되기 전에 script파일을 실행시키기 때문에 DOM요소를 조작하면 존재하지 않는 DOM 요소에 접근하려는 시도가 되어버려 문제가 될 수 있다.
2) <body> 마지막에 삽입하는 경우
이 방법은 HTML을 다운받고 파싱하는 과정을 다 마친 뒤 script태그의 요소를 읽는다. 그래서 비교적 <head> 안에 넣는 방법보다 작동이 빠르다. 그러나 이 역시도 웹이 자바스크립트에 의존적인 경우 HTML이 파싱이 다됐다 하더라도 아무런 의미가 없을 수 있는 것이다.
3) async
위의 두 가지 방법의 단점을 보완하기 위해 다양한 방법들이 고안되었다. 그중 async는 첫 번째 방법과 같이 <head> 내에 삽입하는 방법이긴 하지만 async는 script를 만나면 HTML을 파싱함과 동시에 script를 다운받게 된다. 그리고 곧이어 script를 실행한다. 물론 실행하는 동안에는 HTML을 파싱하는 작업을 멈춘다. 이 방법은 자바스크립트에 의존적인 웹을 더 빨리 실행할 수 있게 한다는 장점이 있다. 그러나 이 역시도 첫 번째 방법과 유사한 문제가 생길 수 있다는 단점 역시 가지고 있다.
4) defer
defer 역시 <script>를 <head> 안에 삽입하는 방식이다. 그러나 이 경우에는 HTML을 파싱하다 script태그를 만나면 async와 마찬가지로 HTML을 파싱하면서 script를 동시에 다운받는다. 하지만 async와 다르게 다운이 되면 곧바로 실행을 하는 것이 아니라 HTML의 파싱을 끝내면 실행시킨다. 이것은 HTML의 파싱의 멈춤이 없게 하는 것이다. 그래서 defer는 HTML의 파싱의 멈춤이 없으면서 script파일의 실행 역시 느리지 않다. 이런 장점들로 현재 가장 강력한 방법이라고 할 수 있다.
이제 DOM을 조작하는 방법에 대해 알아보자. DOM은 document 객체에 구현되어 있다. 그리고 브라우저에서 작동되는 자바스크립트 코드에서는 어디서나 document 객체를 조회할 수 있다. 그래서 document를 통해 HTML의 각 요소에 접근할 수 있는 것이다.
먼저 자식 요소와 부모 요소를 조회하는 방법에 대해 알아보겠다. 자식 요소는 .children 또는 .childNodes로 찾을 수 있고, 부모 요소는 .parentNode 또는 .parentElement로 찾을 수 있다. children의 경우는 현재 요소의 자식 요소가 포함된 HTML Collection을 반환하며 비 노드 요소는 제외한다. childNodes의 경우에는 현재 요소의 자식 요소가 포함된 NodeList를 반환하며 NodeList에는 요소 노드뿐만 아니라 주석 노드와 같은 비 요소 노드도 포함한다. parentNode와 parentElement 역시 거의 동일하게 동작하지만 parentNode의 경우에는 비 노드인 요소를 제외하고 parentElement는 비 노드인 요소도 다 포함한다.
여기서 노드와 엘리먼츠의 차이에 대해 간단히 살펴보면 Node는 태그 노드와 텍스트 노드 등 전체를 가리키는 것이고, 엘리먼트는 텍스트 노드와 같은 것을 제외한 우리가 흔히 아는 태그인 <div>와 같은 형태의 태그만 가리키는 것이다.
이제 더욱 본격적으로 DOM으로 HTML을 조작하는 방법을 살펴보겠다. 보통 우리가 언어를 배우거나 새로운 개념을 배울 때 가장 빨리 학습하고 해야 하는 부분을 가리켜 CRUD라고 한다. 순서대로 만들고, 읽고(조회), 업데이트하고, 삭제하는 방법이다.
1) create
document.createElement('div')
이렇게 코드를 작성하면 새로운 element를 만들 수 있다. 그러나 이렇게 element를 만들고 나면 아무런 변화를 확인할 수 없다. 왜냐하면 생성된 element가 어떤 것에도 연결되어 있지 않기 때문이다.
2) append, appendChild
create에서 생성된 엘리먼트를 HTML의 트리 구조와 연결하기 위해서는 append를 사용해야 한다. 일단 기존에 생성한 엘리먼트를 변수에 할당해 주고 그 변수를 내가 연결하고 싶은 트리구조의 위치를 찾는다. 그러고 나서 append를 한다.
const tweetDiv = document.createElement('div')
document.body.append(tweetDiv)
appendChild 역시 같은 기능을 한다. 그러나 append와 차이가 있다. 기본적으로 append의 경우에는 노드를 추가하는 개수의 제약이 없다. 그러나 appendChild의 경우 노드 요소만 가능하지만 append의 경우 비 노드 요소 역시 추가가 가능하다. 그래서 노드만 따로 분류하는 경우가 아니면 보통은 append를 사용하는 것이 좋다.
3) querySelector, querySeletorAll
DOM으로 HTML 엘리먼트 정보를 조회하는 Read에 해당되는 querySelector이다. querySelector의 첫 번째 인자로 셀렉터를 전달하면 엘리먼트 정보를 조회할 수 있다. 예를 들어 querySelector에 '.twee'을 첫 번째 인자로 넣으면, 클래스 이름이 tweet인 HTML 엘리먼트 중 첫 번째 엘리먼트를 조회할 수 있다.
const oneTweet = document.querySelector('.tweet')
그런데 만약 tweet 클래스의 엘리먼트가 여러 개 있다면 조회하는 방법이 달라진다. 왜냐하면 querySelector를 사용하여 조회할 때 tweet 클래스의 엘리먼트가 여러 개 있다면 tweet 클래스인 하나의 엘리먼트만 조회되기 때문이다. 그럴 때 querySelectorAll을 사용할 수 있다. querySelectorAll을 사용하면 여러 개의 엘리먼트 정보를 동시에 가져올 수 있다. 주의할 것은 이렇게 조회한 엘리먼트들은 배열처럼 사용할 수 있어 for문 같은 것을 사용할 수 있지만 명백히는 배열이 아니라는 것이다. 이런 것들을 유사 배열 또는 배열형 객체라고 부른다.
4) textContent, classList.add
이번에는 Update이다. 앞서 엘리먼트를 생성하고 셀렉터를 조회하여 셀렉터에 생성한 엘리먼트를 연결해주었다. 하지만 생성한 엘리먼트는 아직 아무런 내용이 없기 때문에 여전히 어떠한 내용의 변화도 볼 수 없다. 이제는 문자열부터 추가해보도록 하겠다. textContent를 사용해서 빈 엘리먼트의 내용을 업데이트할 수 있다. 보안적인 문제가 있어 그리 선호되진 않지만 innerHTML을 사용하는 방법도 있다.
oneTweet.textContent = 'dev';
oneTweet.innerHTML = 'dev';
이제 문자열이 추가됐으니 스타일링 역시 설정할 수 있다. css스타일링은 classList나 idList 등 셀렉터리스트를 지정하여 설정한다. 즉, 기존에 작성했던 css의 스타일을 지정하는 것이다.
oneTweet.classList.add('tweet')
5) remove, removeChild
마지막으로 Delete인 삭제하는 방법이다. remove메소드를 사용하여 엘리먼트를 삭제할 수 있다.
oneTweet.remove();
만약 여러 개의 엘리먼트를 지우고자 할 때는 업데이트 때 사용했던 innerHTML을 사용할 수 있다.
document.querySelector('#container').innerHTML = '';
그러나 마찬가지로 보안적인 문제로 다른 방법을 더 지향한다. 반복문을 사용하여 지정한 자식 엘리먼트를 모두 삭제할 수 있다. 자식 엘리먼트를 지정하는 것은 removeChild 메서드를 사용하여 할 수 있다.
const container = document.querySelector('#container');
while(container.firstChild) {
container.removeChild(container.firstChild);
}
직접 클래스 이름을 찾아서 지우는 방법도 있다.
const tweets = document.querySelectorAll('.tweet')
for (let tweet of tweets) {
tweet.remove()
}
- 자주 사용하는 속성
태그이름 | tagName | 자식 엘리먼트 | children |
id | id | 부모 엘리먼트 | parentElement |
class목록 | classList (유사 배열) |
자식 노드 | parentElement |
class문자열 | className (텍스트) |
data-* 속성에 담긴 값 | dataset |
속성 객체 | attributes | 이벤트 | onclick, onmouseover, onkeyup 등 |
스타일 객체 | style | 좌표정보 (기준점에 따라 다름) |
offsetTop, offsetLeft, scrollTop, scrollLeft, clientTop, clientLeft |
엘리먼트에 담긴 내용 | innerHTML, innerText, textContent |
크기정보 (기준점에 따라 다름) |
offsetWidth, offsetHeight, scrollWidth, scrollHeight, clientWidth, clientHeight |
form 입력 값 | value |
- element vs node
<body>
<div id="practice" class="highlight red">
여기 엘리먼트가 하나 있습니다
<span>자식도 있습니다</span>
<span>자식도 여럿 있습니다</span>
</div>
</body>
속성 | 속성 이름 | |
자식 엘리먼트 | children | span, span |
자식 노드 | childNodes | text, span, text, span, text |
엘리먼트는 노드에 속해 있고, text는 노드이나 엘리먼트는 아니다.
- dataset
<div data-user="steve" data=role="moderator" data-user-id="1">
Steve Lee
</div>
$0.dataset.user // 'steve'
$0.dataset/role // 'moderator'
$0.dataset.userID // '1'
'html&css' 카테고리의 다른 글
[styled-components] adapting based on props (0) | 2023.03.30 |
---|---|
Styled-components (0) | 2023.03.30 |
CSS 중급1 (0) | 2021.07.16 |
CSS 기초2 (0) | 2021.06.28 |
CSS 기초1 (0) | 2021.06.25 |