[front] 코딩인터뷰를 저격하는 DOM
DOM
BOM & DOM & Node
Window 객체
- Global Context(전역공간) 이자, 브라우저 창을 나타내는 객체
전역변수나 전역 함수의 경우 window 프로퍼티 처럼 작동하게 됨
- 중요 프로퍼티
- innerWidth, innerHeight, screenX, screenY, scrollBy(), scrollTo()
screen 객체
- 사용자 환경의 디스플레이(모니터) 정보를 가지는 객체
- 중요 프로퍼티
- availHeight, availWidth, width, height, orientation
location 객체
- 사용자가 보고있는 페이지의 URL을 다루는 객체
- 중요 프로퍼티
- href, reload, replace
navigator 객체
- 웹브라우저 및 브라우저 환경 정보를 가지는 객체
- 중요 프로퍼티
- userAgent
DOM(Document Object Model)
- 자바스크립트의 계층화된 트리
document 노드
- 웹 페이지마다 존재하는 객체. 웹 페이지 안의 모든 컨텐츠를 다루는 시작점
- 중요 프로퍼티
- title, url, doctype, documentElement, head, body, getElementById, createElement, querySelector, readyState
단계별 readyState 값의 변화
element노드
- 웹 페이지 안의 각 html 태그 요소 의미
- 중요 프로퍼티
- querySelector, classList, dataset, id, innerHTML, parentNode, nextSibling, previousSibling
정리
- BOM 은 브라우저 기능을 객체처럼 다루는 모델
- window, screen, location, navigator 객체 등이 있다.
- DOM 은 자바스크립트 노드 객체의 계층화 된 트리
- 노드의 종류에는 document, element, text, comment 등이 있다.
브라우저의 렌더링
웹 브라우저의 기본 구조
단계 1. 파싱
HTML을 파싱하여 DOM으로 변환합니다.
- 오타 혹은 잘못된 문법을 사용한 경우 예외처리를 진행
- link, img 같은 태그를 만나면 리소스를 다운함
- script 태그를 만나면 DOM 파싱을 중지하고 자바스크립트를 해석함.
단계 2. 스타일 계산
CSS을 파싱하여, CSSOM으로 변환합니다.
- CSSOM 정보를 통해 돔 노드에 대한 스타일을 결정함.
- 결정된 스타일은 크롬 개발자 도구의 computed 항목에서 확인 가능함.
- 확정된 스타일을 나타냄.
단계 3. 레이아웃
레이아웃 트리(렌더링 트리)를 생성합니다.
- 돔과 계산된 스타일을 따라가며 요소의 크기나 좌표와 같은 정보를 담은 레이아웃 트리를 생성합니다.
- 화면에 표현되는 정보만 트리에 담기게 된다. (display:none X, 가상요소 O)
단계 4. 페인트
레이아웃 트리(렌더트리)가 생성 되면 이 트리를 따라 페인트 기록이 생성 됨.
- 페인트 기록에는 요소를 렌더링 하는 순서 저장
- 지금까지 정보 바탕으로 한 페이지를 여러개의 레이어로 나눈 뒤 그 위에 텍스트, 색 , 이미지, 보더, 그림자 등의 모든 시각적 부분을 그리는 작업 진행
각각의 레이어를 스크린에 픽셀로 표현 하고 (레스터링) 나누었던 레이어들을 합성하여 페이지를 그린다. 이를 컴포지팅이라고 한다.
정리
- 파싱 : HTML을 파싱하여 트리 모델 생성
- 스타일 계산 : DOM + CSSOM = 스타일 확정
- 레이아웃 : 요소의 크기와 좌표 정보가 계산됨
- 페인트 : 레이어 위에 시각적인 부분 작성
- 컴포지팅 : 각각의 레이어를 픽셀화 하고 다시 합성
리페인트 & 리플로우
Reflow란
- 생성된 DOM 노드의 레이아웃 변경 시 영향을 받는 모든 노드(부모, 자식)의 수치를 다시 계산하여, 레아이웃 트리(렌더)를 재생성 하는 작업을 말합니다.
Reflow를 발생하게 하는 속성
- width, height, padding, margin, float, position 등등
- 레이아웃에 영향을 주는 모든 속성
- color, border-radius, background, box-shadow 등등
- 시각적으로 보여지는 모든 속성
Repaint란
- Reflow 과정이 끝나고 재생성된 레이아웃 트리(렌더트리)를 다시 레이어에 그리는 작업
정리하면, 리플로우와 리페인트는 렌더링과정에서 레이아웃 단계와 페인트 단계를 다시(Re) 거치는 과정.
그렇다면 왜 리플로우와 리페인트는 렌더링 속도에 중요한 영향을 미치는가.
렌더링 과정은 순차적으로 진행
- 각각의 렌더링 과정은 반드시 전 단계의 데이터가 필요함
- 만약 전 단계에 변화가 일어날 시 다음 단계에 모두 영향을 미친다.
해당 요소가 60fps 로 움직일 때 우리 눈에 자연스러워 보일 것. 이떄 요소의 레이아웃이 변경 되기 때문에 렌더링 엔진은 60장의 프레임에 0.1초 간격으로 리플로우, 리페인트 과정 수행.
이 과정 중 0.1초 안으로 리플로우, 리페인트를 끝내지 못하면 우리 눈에는 애니메이션이 버벅이는 것 처럼 보인다.
결국 reflow,repaint 해야할 요소가 많으면 많아질 수록 자연스럽지 못한 애니메이션을 그리게 되며, 브라우저의 전체적인 성능에 영향을 미친다.
해결방법
1. CSS Transfrom 속성을 사용
- transform을 사용하여 만드는 애니메이션은 cpu 대신 gpu를 사용하여 화면 렌더링을 처리한다.
2. requestAnimationFrame 함수를 사용
- requestAnimationFrame 함수는 자바스크립트를 통해 일어나는 애니메이션 정보를 브라우저에 매 프레임마다 미리 알려준다.
자바스크립트 애니메이션이 프레임의 시작 시 실행되도록 보장된다.
정리
- reflow 란 생성된 DOM 노드의 레이아웃 변경 시 레이아웃 트리(렌더트리)를 재생성 하는 작업
- repaint란 Reflow 과정이 끝나고 재생성된 레이아웃 트리를 다시 레이어에 그리는 작업
- reflow, repaint 작업이 많아질수록 렌더링 성능이 저하됨.
- 성능 저하를 막기 위해 CSS transform, JS requestAnimationFrame 함수를 사용
이벤트 흐름
만약 브라우저가 사용자의 입력을 받게 되면
- 브라우저 화면에서 이벤트 발생
- 이 떄 브라우저 관점에서 이벤트란 마우스 클릭 뿐 아니라 휠의 움직임, 포인터 이동, 화면 터치 등 모든 종류의 사용자 제스처를 말한다.
- 이벤트가 발생 시 브라우저가 제일 먼저 하는 일은 이벤트 대상을 찾는 일
- 이벤트가 발생한 좌표에 무엇이 있는지 확인 하기 위해 렌더링 과정 중 하나인 페인트 기록 살펴봄
- 캡쳐링 단계
- 페인트 기록을 통해 좌표를 알아낸 브라우저는 해당 좌표에 위치한 요소의 이벤트 리스너를 실행함. 이 과정을 캡처링 단계라 한다.
- 이때 타겟요소의 가장 최상위 window 객체로부터 캡쳐링 단계의 이벤트 리스너가 등록이 되어 있는지 확인하고 있다면 실행한다. 그리고 계속 자식 요소로 전파되며 만나는 캡쳐링 이벤트 리스너를 실행 후 결국 타겟 요소까지 이동한다.
- 버블링 단계
- 캡쳐링이 끝나고, 최초에 이벤트가 발생했던 요소에 버블링 이벤트 리스너가 있다면 실행한다. 그리고 다음 직계 부모요소에 버블링 이벤트 리스너가 있다면 실행시키며, 가장 최상위 요소 window 객체까지 계속 전파된다.
정리
- 브라우저에 이벤트 발생시 브라우저는 좌표를 페인트 기록을 통해 찾고 윈도우 요소부터 하위 요소로 이동해가면서 캡쳐링 이벤트를 실행하는 캡쳐링 단계, 다시 타겟에서 최상으로 올라가는 버블링 단계가 있다
이벤트 위임
요소에 이벤트를 등록하는 일반적인 방법은 “addEventLister()” 를 이용한다.
- 그렇다면 만약 100개 요소에 이벤트를 등록하고 싶다면?
- 일일이 addEventListner를 연결해야 하나?
앞에서 배운 이벤트 흐름을 잘 이용시
- 단 1개의 이벤트 리스너로 수 많은 요소의 이벤트를 처리할 수 있도록 만들 수 있다.
만약 이벤트 리스너가 div 요소에 있고 사용자가 부모요소 div 자식인 button 태그를 클릭했다면
- 브라우저는 이벤트가 발생한 button 태그를 찾기 시작할 것이고 이벤트 캡쳐링과 버블링을 통해 button태그의 부모요소인 div의 이벤트 리스너를 실행시킨다.
이 때 event 객체에는 돔에서 일어나는 이벤트의 정보가 들어있다
- event.currentTarget은 이벤트가 등록된 요소를 가리킨다
- 이는 이벤트 리스너 안의 this가 참조하는 대상과 동일
- 그리고 이벤트가 최초에 발행한 요소는 event.target에 참조된다.
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="parent">
<!-- "generate item" 버튼을 클릭하면 새로운 아이템을 생성하는 버튼 -->
<button type="button">generate item</button>
<ul>
<!-- 초기 아이템으로 화면에 표시될 리스트 아이템 -->
<li>initial item</li>
</ul>
</div>
<script>
// .parent 클래스를 가진 요소를 찾아 변수 parent에 할당
var parent = document.querySelector(".parent");
// .parent 요소에 클릭 이벤트 리스너 추가
parent.addEventListener('click', function (event) {
// 클릭된 요소의 태그 이름이 "button"인 경우
if (event.target.tagName.toLowerCase() === "button") {
// 새로운 리스트 아이템 생성하고 텍스트를 "hello world~"로 설정
const item = document.createElement("li");
item.innerText = "hello world~";
// 부모 요소(.parent)의 하위 ul 요소에 새로운 아이템 추가
parent.querySelector("ul").appendChild(item);
}
// 클릭된 요소의 태그 이름이 "li"인 경우
if (event.target.tagName.toLowerCase() === "li") {
// 콘솔에 "hit!!" 메시지 출력
console.log('hit!!');
};
});
</script>
</body>
</html>
즉, 이벤트를 발생시키고 싶은 요소를 이벤트 리스너가 설치된 부모 요소의 자식으로 배치한다면 그 요소가 몇 개든 상관 없이 이벤트를 등록할 수 있다. 또한 요소가 동적으로 생성되어 계속 추가되어도 같은 기능을 유지한다.
이렇게 이벤트 흐름을 활용하여 단일 이벤트 리스너가 여러개의 이벤트 대상을 처리할 수 있게 하는 프로그래밍을 이벤트 위임이라고 한다.
정리
이벤트 흐름은 특정 이벤트가 발생 시, 해당 이벤트가 발생한 요소를 찾는 과정에서 만나는 모든 이벤트 리스너를 실행
이러한 이벤트 흐름의 특징을 이용하여 단일 이벤트 리스너가 여러개의 이벤트 대상을 처리할 수 있게 만드는 프로그래밍 방법을 이벤트 위임이라고 한다.
Dom의 조각 documentFragment
처음 자바스크립트로 돔을 만들 시 보통 document.createElement()로 생성하고 appendChild() 를 이용해 바로 DOM 에 등록하게 됨
<script>
for (let i = 0; i < 10; i++) {
let divEl = document.createElement("div");
divEl.innerText = "hello~ this is " + i;
document.body.appendChild(divEl);
}
</script>
이 경우 DocumentFragment를 사용 시 메모리 상에만 존재하는 경량화 된 DOM 생성 가능
const docFrag = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
var divEl = document.createElement("div");
divEl.innerText = "hello~ this is " + i;
docFrag.appendChild(divEl);
}
document.body.appendChild(docFrag);
- div 요소를 하나 만들어서 거기다 노드 트리를 만드는 것과 차이?
//const docFrag = document.createDocumentFragment();
const test = document.createElement('div');
for (let i = 0; i < 10; i++) {
var divEl = document.createElement("div");
divEl.innerText = "hello~ this is " + i;
//docFrag.appendChild(divEl);
test.appendChild(divEl);
}
//document.body.appendChild(docFrag);
document.body.appendChild(test);
DocumentFragment 의 특징
// 비어 있는 DocumentFragment를 생성하고 docFrag 변수에 할당합니다.
const docFrag = document.createDocumentFragment();
// 0부터 9까지 총 10번 반복하는 루프를 시작합니다.
for (let i = 0; i < 10; i++) {
// 새로운 div 요소를 생성하고 divEl 변수에 할당합니다.
var divEl = document.createElement("div");
// div 요소의 텍스트 내용을 설정합니다.
divEl.innerText = "hello~ this is " + i;
// 생성한 div 요소를 DocumentFragment에 추가합니다.
docFrag.appendChild(divEl);
// DocumentFragment에 현재까지 추가된 자식 노드들을 출력합니다.
console.log(docFrag.childNodes);
}
// DocumentFragment에 추가된 모든 div 요소들을 문서의 body에 추가합니다.
document.body.appendChild(docFrag);
// DocumentFragment가 비어 있음을 확인하기 위해 다시 자식 노드들을 출력합니다.
console.log(docFrag.childNodes);
- DocumentFragment를 DOM 노드에 추가한다고 해도 DocumentFragment 노드는 등록되지 않고 그 자식 노드들만 추가된다.
- DocumentFragment를 DOM 에 추가하면 DocumentFragment 노드의 자식 요소들은 더 이상 메모리 상에 존재하지 않는다.
이러한 특징으로 요소를 여러개의 각기 다른 부모 요소에 집어 넣을 때 더 깔끔한 코딩이 가능하다.
<script>
// 비어 있는 DocumentFragment를 생성하고 frag 변수에 할당합니다.
const frag = document.createDocumentFragment();
// 빈 배열 elements를 선언합니다.
let elements = [];
// 100번 반복하는 루프를 시작합니다.
for (let i = 0; i < 100; i++) {
// div 요소를 생성하고 el 변수에 할당합니다.
const el = document.createElement("div");
// 생성된 div 요소를 배열 elements에 추가합니다.
elements.push(el);
}
// 클래스 이름이 "container"인 모든 요소를 선택하여 cont 변수에 할당합니다.
const cont = document.querySelectorAll(".container");
// "container" 클래스를 가진 각 요소에 배열 elements의 복제된 요소들을 추가합니다.
for (let i = 0; i < cont.length; i++) {
// 배열 elements의 모든 요소를 복제하여 현재 "container" 요소에 추가합니다.
for(let j = 0; j < elements.length; j++){
cont[i].appendChild(elements[j].cloneNode(true));
}
}
</script>
위 처럼 구성된 코드를
<body>
<!-- 3개의 동일한 클래스 "container"를 가진 div 요소가 있습니다. -->
<div class="container">
</div>
<div class="container">
</div>
<div class="container">
</div>
<script>
// 비어 있는 DocumentFragment를 생성하고 frag 변수에 할당합니다.
const frag = document.createDocumentFragment();
// 100번 반복하는 루프를 시작합니다.
for (let i = 0; i < 100; i++) {
// div 요소를 생성하고 el 변수에 할당합니다.
const el = document.createElement("div");
// 이미지 요소를 생성하고 img 변수에 할당합니다.
const img = document.createElement("img");
// 이미지의 소스(src)를 'koreanFlag.png'로 설정합니다.
img.src = 'koreanFlag.png';
// div 요소에 이미지 요소를 자식으로 추가합니다.
el.appendChild(img);
// DocumentFragment에 현재 생성된 div 요소를 추가합니다.
frag.appendChild(el);
}
// 클래스 이름이 "container"인 모든 요소를 선택하여 cont 변수에 할당합니다.
const cont = document.querySelectorAll(".container");
// "container" 클래스를 가진 각 요소에 복제된 DocumentFragment를 추가합니다.
for (let i = 0; i < cont.length; i++) {
// 복제된 DocumentFragment를 현재 "container" 요소에 추가합니다.
cont[i].appendChild(frag.cloneNode(true));
}
</script>
</body>
이 처럼 구성이 가능하다. 이중 포문을 쓸 필요도 없고, 배열을 새로 만들 필요도 없었다.
정리
- DocumentFragment 노드는 오직 메모리상에만 존재하는 경량화 DOM
- DocumentFragment 를 DOM 노드에 추가한다고 해도 DocumentFragment 노드는 추가되지 않고 그 자식 노드만 추가된다.
- DocumentFragment를 DOM 에 추가할 떄 DocumentFragment의 자식 노드는 더 이상 생성한 메모리 상의 위치에 존재하지 않는다. 만약 이를 유지시키고 싶다면 cloneNode를 통해 복제하는 법이 있다.