ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS/Slider] 바닐라 자바스크립트로 무한 루프 슬라이드(Carousel) 구현하기
    Frontend 2019. 5. 31. 15:21
    무한 루프 슬라이드

     

     

    오늘은 자바스크립트로 구현해 본 무한루프 슬라이드를 정리해보려고 한다.

    예전에 제이쿼리를 사용해서 슬라이드를 구현해본 적은 있지만

    마지막 슬라이드에서 첫번째 슬라이드로 자연스럽게 옆으로 넘기는 이벤트는 구현하지 못했었다.

     

    이번엔 제이쿼리 사용없이 순수하게 바닐라 자바스크립트로 

    무한루프 기능과 페이지네이션 기능까지 만들어보려고 한다.

     

     


     

     

    <div id="container">
      <div class="slide_wrap">
        <div class="slide_box">
          <div class="slide_list clearfix">
            <div class="slide_content slide01">
              <p>1</p>
            </div>
            <div class="slide_content slide02">
              <p>2</p>
            </div>
            <div class="slide_content slide03">
              <p>3</p>
            </div>
            <div class="slide_content slide04">
              <p>4</p>
            </div>
            <div class="slide_content slide05">
              <p>5</p>
            </div>
          </div>
          <!-- // .slide_list -->
        </div>
        <!-- // .slide_box -->
        <div class="slide_btn_box">
          <button type="button" class="slide_btn_prev">Prev</button>
          <button type="button" class="slide_btn_next">Next</button>
        </div>
        <!-- // .slide_btn_box -->
        <ul class="slide_pagination"></ul>
        <!-- // .slide_pagination -->
      </div>
      <!-- // .slide_wrap -->
    </div>
    <!-- // .container -->

     

    먼저 HTML코드는 이렇다.

    div.slide_content 안에는 원하는 img태그를 추가하면 되는데,

    이미지 추가하기가 귀찮아서 슬라이드 각각의 배경색만 다르게 하여 만들 생각이다.

     

    페이지네이션은 일단 동적으로 추가할 예정이라서 ul 태그만 만들어두었다.

     

     

     

    슬라이드가 움직이는 원리

     

     

    이제 css를 작성해야 하는 데, 일단 슬라이드가 움직이는 원리는 위의 그림과 같다.

     

    div.slide_box > div.slide_list > div.slide_content

     

    이 노드 구조에서 div.slide_contentfloat: left로 딱 붙여놓는다.

    그리고 그걸 감싸는 div.slide_list는 슬라이드를 모두 합한 넓이를 가져야한다.

    그러면 슬라이드가 일렬로 쭉 늘어서게 된다.

     

    그러면 div.slide_list를 감싸고 있는 div.slide_box의 넓이를 div.slide_content와 같은 400px을 준다.

    그리고 overflow: hidden을 주면 슬라이드 한 개만 화면 상에 보이게 된다.

    이 상태에서 div.slide_listtransform: translate3d()로 400px씩 계속 움직이면 된다.

     

     

     

    .slide_wrap { position: relative; width: 400px; margin: auto; padding-bottom: 30px; }
    .slide_box { width: 100%; margin: auto; overflow-x: hidden; }
    .slide_content { display: table; float: left; width: 400px; height: 400px; }

     

    그 부분의 css 코드가 바로 위와 같다.

    div.slide_list의 넓이를 지정해주는 css 코드가 없는 이유는 

    그 부분은 자바스크립트로 처리할 예정이기 때문이다.

     

     

     

    const slideList = document.querySelector('.slide_list');  // Slide parent dom
    const slideContents = document.querySelectorAll('.slide_content');  // each slide dom
    const slideBtnNext = document.querySelector('.slide_btn_next'); // next button
    const slideBtnPrev = document.querySelector('.slide_btn_prev'); // prev button
    const pagination = document.querySelector('.slide_pagination');
    const slideLen = slideContents.length;  // slide length
    const slideWidth = 400; // slide width
    const slideSpeed = 300; // slide speed
    
    slideList.style.width = slideWidth * (slideLen) + "px";
    
    let curIndex = 0; // current slide index (except copied slide)
    
    /** Next Button Event */
    slideBtnNext.addEventListener('click', function() {
      if (curIndex <= slideLen - 1) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * (curIndex + 1)) + "px, 0px, 0px)";
      }
      curSlide = slideContents[++curIndex];
    });

     

    이제 자바스크립트 코드를 작성해보자.

     

    그러니까 위 생각대로 '다음' 버튼을 클릭하면 div.slideList가 왼쪽으로 400px씩 계속 이동하면 된다.

     

    그러니까 curIndex라는 변수를 0으로 잡아두고

    - slideWidth * (curIndex + 1)px 만큼 x축을 움직인 후,

    curIndex1 증가시키면 된다.

     

    그리고 transition300ms로 설정하면 x축 이동을 0.3초에 걸쳐 진행하므로

    부드럽게 움직이는 애니메이션 효과를 줄 수 있다.

     

    그런데 문제는 맨 마지막으로 이동했을 때다. (그러니까 curIndex가 4인 상태에서 오른쪽 버튼을 눌렀을 때)

    맨 마지막으로 이동한 후 오른쪽으로 이동하면 맨 처음으로 이동하게끔 만들어야 한다.

     

     

     

    const slideList = document.querySelector('.slide_list');  // Slide parent dom
    const slideContents = document.querySelectorAll('.slide_content');  // each slide dom
    const slideBtnNext = document.querySelector('.slide_btn_next'); // next button
    const slideBtnPrev = document.querySelector('.slide_btn_prev'); // prev button
    const pagination = document.querySelector('.slide_pagination');
    const slideLen = slideContents.length;  // slide length
    const slideWidth = 400; // slide width
    const slideSpeed = 300; // slide speed
    
    slideList.style.width = slideWidth * (slideLen) + "px";
    
    let curIndex = 0; // current slide index (except copied slide)
    
    /** Next Button Event */
    slideBtnNext.addEventListener('click', function() {
      if (curIndex < slideLen - 1) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * (curIndex + 1)) + "px, 0px, 0px)";
      } else {
        slideList.style.transform = "translate3d(0px, 0px, 0px)";
        curIndex = -1;
      }
      curSlide = slideContents[++curIndex];
    });

     

    그렇다고 이렇게 코드를 짜도 문제가 생긴다.

    왜냐면 이렇게하면 다시 1번 슬라이드로 이동하긴 하지만

    애니메이션이 오른쪽에서 다시 왼쪽으로 넘어오기 때문에 매우 부자연스럽다.

     

    우리가 원하는 애니메이션은

    5번 슬라이드에서 다시 1번 슬라이드로

    즉, 왼쪽에서 오른쪽으로 움직이는 것이기 때문이다.

     

     

    그래서 우리는 여기서 트릭을 써야한다.

    먼저 아래 그림처럼 맨 첫번째 슬라이드와 맨 마지막 슬라이드를 복제한 후

    1번 앞에는 5번 복제본을,

    5번 뒤에는 1번 복제본을 추가한다.

     

     

     

    이제 이 상황에서 5번 슬라이드일 때, '다음' 버튼을 클릭하면

    바로 오른쪽의 1번 복제본으로 자연스럽게 넘어가는 애니메이션을 0.3초 동안 수행한다.

    그리고 0.3초가 지나자마자 (원본) 1번 슬라이드 위치로 이동시킨다.

     

     

     

    const slideList = document.querySelector('.slide_list');  // Slide parent dom
    const slideContents = document.querySelectorAll('.slide_content');  // each slide dom
    const slideBtnNext = document.querySelector('.slide_btn_next'); // next button
    const slideBtnPrev = document.querySelector('.slide_btn_prev'); // prev button
    const slideLen = slideContents.length;  // slide length
    const slideWidth = 400; // slide width
    const slideSpeed = 300; // slide speed
    const startNum = 0; // initial slide index (0 ~ 4)
    
    slideList.style.width = slideWidth * (slideLen + 2) + "px";
    
    // Copy first and last slide
    let firstChild = slideList.firstElementChild;
    let lastChild = slideList.lastElementChild;
    let clonedFirst = firstChild.cloneNode(true);
    let clonedLast = lastChild.cloneNode(true);
    
    // Add copied Slides
    slideList.appendChild(clonedFirst);
    slideList.insertBefore(clonedLast, slideList.firstElementChild);
    
    slideList.style.transform = "translate3d(-" + (slideWidth * (startNum + 1)) + "px, 0px, 0px)";
    
    let curIndex = startNum; // current slide index (except copied slide)
    let curSlide = slideContents[curIndex]; // current slide dom
    curSlide.classList.add('slide_active');
    
    /** Next Button Event */
    slideBtnNext.addEventListener('click', function() {
      if (curIndex <= slideLen - 1) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * (curIndex + 2)) + "px, 0px, 0px)";
      }
      if (curIndex === slideLen - 1) {
        setTimeout(function() {
          slideList.style.transition = "0ms";
          slideList.style.transform = "translate3d(-" + slideWidth + "px, 0px, 0px)";
        }, slideSpeed);
        curIndex = -1;
      }
      curSlide.classList.remove('slide_active');
      curSlide = slideContents[++curIndex];
      curSlide.classList.add('slide_active');
    });
    
    /** Prev Button Event */
    slideBtnPrev.addEventListener('click', function() {
      if (curIndex >= 0) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * curIndex) + "px, 0px, 0px)";
      }
      if (curIndex === 0) {
        setTimeout(function() {
          slideList.style.transition = "0ms";
          slideList.style.transform = "translate3d(-" + (slideWidth * slideLen) + "px, 0px, 0px)";
        }, slideSpeed);
        curIndex = slideLen;
      }
      curSlide.classList.remove('slide_active');
      curSlide = slideContents[--curIndex];
      curSlide.classList.add('slide_active');
    });

     

    코드에 반영하면 위와 같다.

     

    '다음' 버튼을 눌렀을 때, curIndex와 slideLen - 1의 값이 같다면 오른쪽으로 0.3s 동안 이동한다.

    그리고 0.3s 이후에 슬라이드를 맨 첫 번째 슬라이드 위치로 이동하는 코드를 setTimeout으로 설정한다.

    맨 처음 위치로 이동할 때는 transition을 0ms로 초기화하여 애니메이션 효과를 제거하고 한 번에 이동해야한다.

     

    또한 중요한 점은 슬라이드 앞 뒤에 각각 1개씩 복사본을 붙였기 때문에

    최초로 코드를 실행할 때, slideList의 위치를 translate3d(-400px, 0px, 0px)으로 잡아줘야한다.

     

     

     

    // Copy first and last slide
    let firstChild = slideList.firstElementChild;
    let lastChild = slideList.lastElementChild;
    let clonedFirst = firstChild.cloneNode(true);
    let clonedLast = lastChild.cloneNode(true);
    
    // Add copied Slides
    slideList.appendChild(clonedFirst);
    slideList.insertBefore(clonedLast, slideList.firstElementChild);

     

    이 부분에 주목해보자. 

     

    // Copy first and last slide
    let firstChild = slideList.firstElementChild;
    let lastChild = slideList.lastElementChild;
    
    // Add copied Slides
    slideList.appendChild(clonedFirst);
    slideList.insertBefore(clonedLast, slideList.firstElementChild);

     

    나는 처음에 cloneNode()를 사용하지 않고 

    그냥 firstChild라는 변수에 slideList.firstElementChild;를 할당하고 slideList에 append시켰다.

     

    그랬더니 첫 번째 슬라이드의 복제본이 append된 것이 아니고

    첫 번째 슬라이드 자체가 append 되어버렸다(!)

     

    그렇다. firstChild에 할당된 것은 firstElementChild 자체의 주소값이다.

    firstElementChild를 slideList에 append시켰으므로 firstElementChild자체가

    돔 트리 내에서 위치를 이동한 것이다.

     

    그러므로 dom을 복제하기 위해선 cloneNode()라는 메소드를 사용해야 한다.

     

     

     

    // Add pagination dynamically
    let pageChild = '';
    for (var i = 0; i < slideLen; i++) {
      pageChild += '<li class="dot';
      pageChild += (i === startNum) ? ' dot_active' : '';
      pageChild += '" data-index="' + i + '"><a href="#"></a></li>';
    }
    pagination.innerHTML = pageChild;
    const pageDots = document.querySelectorAll('.dot'); // each dot from pagination

     

    슬라이드를 어느 정도 구현했으므로 이제 페이지네이션을 동적으로 추가해보려고 한다.

     

    이 때 중요한 점은 동적으로 생성된 돔에 접근하여 이벤트를 걸기 위해선

    엘리먼트가 동적으로 생성되고 난 후의 시점에서 돔에 접근해야한다는 점이다.

    (만약 동적 생성 이전에 해당 돔에 접근하는 코드를 작성하고 싶다면 좀 더 복잡한 방법을 사용해야 한다.)

     

     

     

    /** Next Button Event */
    slideBtnNext.addEventListener('click', function() {
      if (curIndex <= slideLen - 1) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * (curIndex + 2)) + "px, 0px, 0px)";
      }
      if (curIndex === slideLen - 1) {
        setTimeout(function() {
          slideList.style.transition = "0ms";
          slideList.style.transform = "translate3d(-" + slideWidth + "px, 0px, 0px)";
        }, slideSpeed);
        curIndex = -1;
      }
      curSlide.classList.remove('slide_active');
      pageDots[(curIndex === -1) ? slideLen - 1 : curIndex].classList.remove('dot_active');
      curSlide = slideContents[++curIndex];
      curSlide.classList.add('slide_active');
      pageDots[curIndex].classList.add('dot_active');
    });
    
    /** Prev Button Event */
    slideBtnPrev.addEventListener('click', function() {
      if (curIndex >= 0) {
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * curIndex) + "px, 0px, 0px)";
      }
      if (curIndex === 0) {
        setTimeout(function() {
          slideList.style.transition = "0ms";
          slideList.style.transform = "translate3d(-" + (slideWidth * slideLen) + "px, 0px, 0px)";
        }, slideSpeed);
        curIndex = slideLen;
      }
      curSlide.classList.remove('slide_active');
      pageDots[(curIndex === slideLen) ? 0 : curIndex].classList.remove('dot_active');
      curSlide = slideContents[--curIndex];
      curSlide.classList.add('slide_active');
      pageDots[curIndex].classList.add('dot_active');
    });
    
    /** Pagination Button Event */
    let curDot;
    Array.prototype.forEach.call(pageDots, function (dot, i) {
      dot.addEventListener('click', function (e) {
        e.preventDefault();
        curDot = document.querySelector('.dot_active');
        curDot.classList.remove('dot_active');
    
        curDot = this;
        this.classList.add('dot_active');
    
        curSlide.classList.remove('slide_active');
        curIndex = Number(this.getAttribute('data-index'));
        curSlide = slideContents[curIndex];
        curSlide.classList.add('slide_active');
        slideList.style.transition = slideSpeed + "ms";
        slideList.style.transform = "translate3d(-" + (slideWidth * (curIndex + 1)) + "px, 0px, 0px)";
      });
    });

     

    이제 '다음' 버튼과 '이전' 버튼을 클릭했을 때,

    나타나는 슬라이드의 인덱스 번호와 일치하는 페이지네이션 엘리먼트의 컬러를 변경해준다.

     

    각각의 페이지네이션 엘리먼트를 클릭했을 때도 이벤트를 준다.

     

     

     

    slide_test.html
    0.01MB

    반응형

    COMMENT