유데미 강의 JavaScript 완벽 가이드 : 초급 + 고급 마스터 과정을 들으면서 자바스크립트를 다시 한 번 익히는 중이다. 계속되는 포스팅에서 강의를 듣고 미처 몰랐던 부분, 심화 이해가 필요한 부분에 대해 주제별로 간단하게 기록하겠다.
DOM (Document Object Model)은 웹 브라우저가 HTML 페이지를 로드한 후에 그 내용을 표현하는 방식이다.
모든 요소는 노드이지만, 모든 노드가 요소는 아니다.
DOM 트리의 각각의 부분을 가리키는 용어. 즉, DOM에서 노드는 HTML 문서의 개별 부분을 나타내는 객체이다. 노드는 여러 가지 타입이 있을 수 있는데 요소 노드(Element Node), 속성 노드(Attribute Node), 텍스트 노드(Text Node) 등이 있다.
노드 중에서도 특별히 '요소 노드' 혹은 간단히 '요소'는 HTML 태그 하나를 가리키는 객체다. 예를 들면, <p> , <div>, <body>, <head> 등의 태그. 요소는 다른 요소 노드, 텍스트 노드 등의 부모 또는 자식이 될 수 있다.
주의하자. 리스트의 변경에 대한 것이지 단일 노드에 대한 내용이 아니다.
// <input id="input" value="default"/>
const input = document.getElementById('input');
input.value = 'hi';
// 이렇게 프로퍼티를 변경하면 브라우저에서 렌더링하는 뷰는 즉시 업데이트되어 사용자에게 변경된 값('hi')을 보여주게 된다.
// 하지만 해당 HTML 코드의 value는 변하지 않는다.(단방향)
<div>
<nav></nav>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
<input/>
</div>
// 자식요소 탐색
const ul = document.querySelector('ul');
const ul.firstElementChild;
// 부모요소 탐색
const liFirst = document.querySelector('li');
liFirst.parentNode // ul node
liFirst.closest('div') // div node
// 형제 요소 탐색
const ul = document.querySelector('ul');
ul.previousElementSibling // nav node
ul.nextElementSibling // input node
// 삽입
const ul = document.querySelector('ul');
const li = document.createElement('li')
ul.appendChild(li)
// 삭제
// 1.
const ul = document.querySelector('ul');
const li.remove();
// 2.
const ul = document.querySelector('ul');
ul.parentElement.removeChild(ul) // 부모에 접근해서 삭제
const deleteMovieHandler = (movieId) => {
deleteMovieModal.classList.add('visible');
toggleBackdrop();
const cancleBtn = deleteMovieModal.querySelector('.btn--passive');
let confirmBtn = deleteMovieModal.querySelector('.btn--danger');
confirmBtn.replaceWith(confirmBtn.cloneNode(true));
confirmBtn = deleteMovieModal.querySelector('.btn--danger');
cancleBtn.removeEventListener('click', closeMovieDeletionModal);
cancleBtn.addEventListener('click', closeMovieDeletionModal);
confirmBtn.addEventListener('click', deleteMovie.bind(null, movieId));
};
newMovieEl.addEventListener('click', deleteMovieHandler.bind(null, id));
newMovieEl의 클릭이벤트에 deleteMovieHandler를 바인딩했다. deleteMovieHandler는 화면에 확인 모달창을 띄우고 사용자의 버튼(cancle, confirm) 클릭에 따라 취소되거나 newMovieEl을 삭제한다.
이때 newMovieEl를 클릭한 뒤 취소 또는 확인(삭제)를 여러번 반복하면 cancleBtn과 confirmBtn에 이벤트 리스너가 계속 등록된다. cancleBtn의 경우에는 항상 같은 동작을 수행하기 때문에 크게 문제되진 않지만 (그럼에도 removeEventListener를 하는 게 좋은 습관) confirmBtn의 경우 매번 다른 id에 대한 삭제 함수가 등록된다. 그러니까 취소를 여러번 클릭한 뒤, 삭제 버튼을 클릭하면 이전까지 등록된 삭제 함수가 전부 실행되는 문제가 발생한다.
그러므로 confirmBtn에 이벤트 바인딩을 하기전에 이전에 있던 함수를 삭제해야하지만 위 코드의 경우처럼 이벤트 핸들러가 bind나 화살표 함수 등을 사용해 생성된 콜백이라면 removeEventListener를 바로 적용할 수 없다.
대신 replaceWith로 노드를 복사하고 변수에 새 노드를 할당해주는 방식을 사용했다. 이 방식이 조금 어려울 수 있어 과정을 비유한 설명을 추가한다.
- confirmBtn에는 낡은 공이 실로 연결되어있고 현재 낡은 공은 두번째 서랍에 보관되어 있다.
- replaceWith(confirmBtn.cloneNode(true));를 실행해서 두번째 서랍에 있던 낡은 공을 멀리 던지고 낡은 공을 똑 닮은 새공으로 교체한다. (이벤트 리스너 제거)
- confirmBtn은 여전히 멀리 던져진 낡은 공과 실로 연결되어 있다.
- confirmBtn = deleteMovieModal.querySelector('.btn--danger'); 낡은 공과 연결된 실을 끊고 두번째 서랍에 있는 새 공과 연결한다.
// 요소의 위치와 너비값을 제공
getBoundingClientRect()
// 요소의 부모를 기준으로 최상단 지점과 요소까지의 거리
offsetTop, (...)
// border의 두께
clientTop, (...)
// 요소의 전체 너비와 높이
offsetWidth, offsetHeight
// border를 제외한 요소의 너비와 높이
clinetWidth, clientHeight
// 박스에 보이지 않는 부분(스크롤)을 포함한 요소의 전체 높이
scrollHeight
// 최상단에서 스크롤한만큼의 값
scrollTop
// 스크롤이 있는 경우 스크롤바까지 포함한 크기
window.innerWidth
window.innerHeight
// -> 대신 이걸 써라 (스크롤바 제외)
document.documentElement.clientWidth
document.documentElement.clientHeight
// 특정 위치로 스크롤 이동하기
scrollTo(x, y) // 고정값
scrollBy(x, y) // 상대값(현재 스크롤 위치에서 +-)
scrollTo({top:50, behavior:'smooth}) // 객체를 보낼 수도 있음
// 호출된 요소가 현재 뷰포트(viewport)에 나타나도록 스크롤을 조정
el.scrollIntoView({ behavior: 'smooth' });