30개의 프로젝트로 배우는 프론트엔드 with VanillaJS (2-2) 이미지슬라이더

productuidev·2022년 8월 22일
0

FE Study

목록 보기
50/67
post-thumbnail
post-custom-banner

[fastcampus] 30개의 프로젝트로 배우는 프론트엔드 with VanillaJS (2-2)

(2) 바닐라 자바스크립트로 만드는 이미지 슬라이더

jQuery 라이브러리를 사용하지 않고 만드는 슬라이더

indicator 슬라이드 개수만큼 생성하기

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 같은 이슈는 단순히 값을 넣고 빼는 문제가 아닙니다. 이 성능 결과는 이런 결과가 있다 정도로만 생각해주세요.

출처 : DocumentFragment를 사용해보자(성능최적화)

이전/다음 버튼 클릭 시 현재 indicator 표시

다음은 생성된 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 클릭 시 해당 슬라이드로 이동 구현

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가 관건일텐데..)

profile
필요한 내용을 공부하고 저장합니다.
post-custom-banner

0개의 댓글