jQuery 라이브러리를 사용하지 않고 만드는 슬라이더
index.html
<div class="indicator-wrap" id="indicator-wrap">
<ul></ul>
</div>
indicator에 따라 슬라이더가 활성화되도록 기능을 구현한다. indicator는 ul 안에 li가 슬라이더 개수만큼 동적으로 생성되어야 하기 때문에 JS을 이용해서 생성해보도록 한다.
src/js/ImageSlider.js
먼저 public field에 indicatorWrapEl을 추가하고, assignElement에도 indicatorWrapEl을 생성해서 index.html의 #indicator-wrap을 탐색할 수 있도록 한다. createIndicator 메서드를 만들어서, indicator 슬라이드 개수만큼 생성하는 스크립트를 작성한다. constructor에도 createIndicator를 넣어서 DOM에 생성되도록 한다. 이 때 순서는 탐색 순서에 유의하여 this.assignElement(); 아래에 놓는다.
export default class ImageSlider {
...
indicatorWrapEl;
...
constructor() {
...
this.createIndicator();
}
assignElement() {
...
this.indicatorWrapEl = this.sliderWrapEl,querySelector('#indicator-wrap');
}
...
createIndicator() {
const docFragment = document.createDocumentFragment();
for (let i = 0; i < this.#slideNumber; i += 1) {
const li = document.createElement('li');
li.dataset.index = i; // data-index 순서값
docFragment.appendChild(li);
}
this.indicatorWrapEl.querySelector('ul').appendChild(docFragment); // ul > li li li
}
createDocumentFragment()
노드를 생성하는 메서드
DocumentFragment 노드를 생성해서 사용하면 라이브 DOM 트리 외부에 경량회된 문서 DOM을 만들 수 있다. 미리 메모리상에 특정 노드의 형태를 생성해놓은 뒤 그 노드를 실제 DOM에 추가해서 사용하고 싶을 때 추가하면 실제 적용이 된다. 메모리상에서만 존재하는 빈 문서 템플릿같은 개념 (메모리상에서 형태가 존재할 경우에는 적용도 안된 생태이고 아무런 존재감도 없다)
출처 : Document.createDocumentFragment, createDocumentFragment()는 무슨 기능을 할까?
HTML과 DOM 정의
HTML과 DOM을 구체적으로 정의 내리면 다음과 같습니다. HTML은 웹 페이지의 문서 구조를 생성하는 태그 시스템이고 DOM은 HTML이 가지는 문서 구조를 다룰 수 있는 인터페이스입니다.
DOM은 HTML의 구조를 계층형 트리 형태로 정의합니다.
그리고 브라우저는 HTML이 가진 구조를 Style Sheet인CSS를 사용해 화면을 그려 사용자가 쉽게 웹 페이지를 이용
할 수 있도록 합니다. DOM은 원래 XML 문서를 정의하기 위한 인터페이스였지만, HTML에서도 사용할 수 있도록확장
되었습니다.DocumentFragment는
DOM의 단편적인 부분을 정의할 수 있는 노드
입니다. 전문적인 용어를 넣어 설명하면부모가 없는 최소화된 경량화된 문서 객체
라고도 이야기합니다. DocumentFragment는 기본적으로 DOM과 동일하게 동작하지만, HTML의 DOM 트리에는 영향을 주지 않으며,메모리에서만 정의
됩니다.
const documentFragment = new DocumentFragment();
const ulElement = document.createElement("ul");
documentFragment.appendChild(ulElement);
["one", "two", "three", "four", "five"].forEach((text) => {
const liElement = document.createElement("li");
liElement.textContent = text;
ulElement.appendChild(liElement);
});
document.body.appendChild(documentFragment);
DocumentFragment는 활성화된 DOM의 일부가 아닙니다. 처음에 이야기한 것처럼 DocumentFragment는 DOM에 반영하기 전까지는 메모리상에서만 존재합니다.
즉 DocumentFragment에 변경이 일어나도 DOM의 구조에는 변경이 일어나지 않기 때문에 브라우저가 화면을 다시 랜더링 하지 않습니다. 이 말은 Reflow나 Repaint가 일어나지 않는다는 말과도 같습니다.
DocumentFragment는 메모리상에서만 만들어지고 화면에 영향을 주지 않기 때문에 n번의 수정이 일어나도 반영을 한 번만 한다면 Reflow나 Repaint는 한 번만 일어납니다. (정확히 한 번만 일어나지는 않습니다.)
innerHTML과 DocumentFragment를 사용했을 때 어느 쪽이 더 빠른 속도를 보여줄까요?
innerHTML이 DocumentFragment에 비해 빠른 속도를 보여줍니다. 이 부분은 단순히 값만을 기준으로 측정한 사항이므로 실제로 개발되고 사용하는 코드와는 다릅니다.
프로젝트에서는 요구조건에 따라 기능이 동적으로 정의되는 경우가 많습니다.
단순히 Element를 넣고, 빼고 하는 것뿐이 아니라 속성을 정의하고 class를 추가하는 등 여러 가지 작업을 합니다. 즉, 성능 측정 결과가 있다고 알려드리는 부분일 뿐 성능 최적화를 위해서는 DocumentFragment를 사용하는 것이 좋습니다.브라우저가 화면을 랜더링 할 때 이야기되는 Reflow나 Repaint 같은 이슈는 단순히 값을 넣고 빼는 문제가 아닙니다.
이 성능 결과는 이런 결과가 있다 정도로만 생각해주세요.
다음은 생성된 indicator가 클릭하거나 슬라이더가 이동할 때 어느 위치에 있는지를 표시해보자. 제공된 CSS에서 현재는 비활성화일 때 반투명한 흰색의 원으로 표시하고 있는데, 이전/다음버튼을 눌렀을 때 현재 슬라이드의 indicator가 활성화되면 불투명한 흰색의 원으로 표시되도록 setIndicator() 메서드를 추가한다.
src/js/ImageSlider.js
constructor() {
...
this.createIndicator();
this.setIndicator();
}
moveToRight() {
...
this.setIndicator();
}
moveToLeft() {
...
this.setIndicator();
}
setIndicator() {
this.indicatorWrapEl.querySelector('li.active')?.classList.remove('active');
this.indicatorWrapEl
.querySelector(`ul li:nth-child(${this.#currentPostion + 1})`)
.classList.add('active');
}
현재 createIndicator() 메서드를 통해 생성한 indicator의 각 li에는 data-index로 슬라이드의 순서값이 들어 있는데, 초기에는 모두 비활성화 상태로 되어 있다. 이 data-index에 따라서 현재 슬라이드 위치에서 활성화되도록 스크립트를 구성한다. ?.classList를 넣는 이유는 처음에는 초기화 상태에서 활성화를 표시하는 active 클래스가 없을 수도 있으므로 옵셔널 체이닝을 넣어준다. 그런 다음 몇 번째를 활성화해줄 것인지를 구현해야 하는데, li의 data-index는 0부터 시작하지만 li:nth-child는 1부터 시작한다. 따라서 슬라이드의 현재 위치를 탐색하는 #currentPosition에 1을 더해서 해당 슬라이드인 li의 순서를 찾고 그런 후에 active 클래스를 추가하여 활성화되게끔 한다. 그런 후 constructor()에 만든 setIndicator()를 넣어주고, 이전버튼과 다음버튼을 누를 때도 해당 기능이 구현되도록 this.setIndicator()를 모두 넣어준다.
indicator 클릭 시 해당 슬라이드로 이동하게끔 addEvent()에 onClickIndicator 이벤트를 걸어준다.
addEvent() {
this.nextBtnEl.addEventListener('click', this.moveToRight.bind(this));
this.previousBtnEl.addEventListener('click', this.moveToLeft.bind(this));
this.indicatorWrapEl.addEventListener(
'click',
this.onClickIndicator.bind(this),
);
}
onClickIndicator(event) {
const indexPosition = parseInt(event.target.dataset.index, 10);
if (Number.isInteger(indexPosition)) {
this.#currentPostion = indexPosition;
this.sliderListEl.style.left = `-${
this.#slideWidth * this.#currentPostion
}px`;
this.setIndicator();
}
}
onClickIndicator(event)의 경우, console.log(indexPosition)을 통해 확인 시 li data-index 외에 div, ul 같이 다른 영역도 같이 잡혀 console 확인 시 undefinded가 뜨는 경우가 있다. indexPosition은 처음 String으로 잡혀 있으므로 parseInt를 통해 숫자(10진법)으로 변경해준다. (eslint rules) 그런데 아까 말한 다른 영역은 parseInt(undefined)를 넣으면 NaN (Not-a-Number)가 된다. 따라서 조건문을 통해서 NaN을 가려내고 li data-index만 탐색하여 이벤트를 수행하게 한다. Number의 static 메서드인 isInterger를 통해 해당 값이 정수라면 슬라이드 위치(left)가 이동되도록 다른 메서드에서 사용한 -(slideWidth * currentPosition)을 넣어준다. 그런 후 똑같이 this.setIndicator()를 넣어서 클릭 시 동작하게 한다.
<script> Number.isInteger(123) //true Number.isInteger(-123) //true Number.isInteger(5-2) //true Number.isInteger(0) //true Number.isInteger(0.5) //false Number.isInteger('123') //false Number.isInteger(false) //false Number.isInteger(Infinity) //false Number.isInteger(-Infinity) //false Number.isInteger(0 / 0) //false </script>
바닐라 뜯어보기 😀
친구(프리랜서/자바개발자)가 리액트 프로젝트에 갔다. 거기서도 역시 시작부터 클래스형 말고 함수형 쓰라고 이야기. 그 외에도 컴포넌트도 css 구현 이야기 하는 거보니 아마도 styled component인 게 아닐까 싶었는데 리액트에 맞게 js 조금 더 하면 별로 어려울 거 같지 않다고 이야기.. 나도 언젠가.. (아무래도 api, db, query 이해가 있는 개발자가 리액트나 뷰 공부하는 게 더 쉽겠지.. ui가 관건일텐데..)