본문 바로가기
WEB Basic/JavaScript

Vanilla JavaScript Slider 자바스크립트 자동 슬라이드 구현

by Devinus 2022. 5. 22.

1. 분석

이전 무한 슬라이드 구현 포스팅에 이어서 자동 슬라이드를 구현해본다.

외부 라이브러리를 사용하지 않고 순수 자바스크립트만을 이용해 자동 슬라이드를 구현한다.

 

자동 슬라이드를 구현하기 위해선 기본적인 슬라이드 기능과 무한 슬라이드 기능이 구현된 상태에서 제대로 구현할 수 있다.

 

자동 슬라이드는 일정한 시간 간격으로 다음 슬라이드를 보여줘야 한다. 즉, 슬라이드 기능에서 다음 슬라이드를 보여주는 함수를 구현했다면 다음과 같이 `setInterval()`함수를 이용해서 일정 시간마다 다음 슬라이드를 보여주는 함수를 실행하도록 하면 된다.

// 기본적으로 슬라이드 루프 시작하기
let loopInterval = setInterval(() => {
  nextMove(); // 다음 슬라이드를 보여주는 함수
}, 3000);

자동 슬라이드에서 사용자가 지속적으로 보고 싶은 슬라이드가 존재할 수 있다.

따라서 추가적인 기능으로 슬라이드에 마우스가 올라간(mouseover) 경우 자동 슬라이드를 멈추는 기능을 추가하고, 슬라이드에서 마우스가 나간(mouseout) 경우 자동 슬라이드 기능을 다시 적용하는 코드를 다음과 같이 작성하면 된다.

// 슬라이드에 마우스가 올라간 경우 루프 멈추기
slide.addEventListener("mouseover", () => {
  clearInterval(loopInterval);
});

// 슬라이드에서 마우스가 나온 경우 루프 재시작하기
slide.addEventListener("mouseout", () => {
  loopInterval = setInterval(() => {
    nextMove(); // 다음 슬라이드를 보여주는 함수
  }, 3000);
});

이번 자동 슬라이드를 구현하기 위해선 구현에 사용한 setInterval, clearInterval과 같은 함수에 대한 이해가 필요하다.

위 코드에서 자동 슬라이드를 구현하기 위해 주로 사용된 `setInterval(func, delay)`함수는 전달된 func 콜백 함수를 delay 밀리초(milliseconds) 마다 실행시키는 함수이며, intervalID를 반환한다.

`setInterval()`함수로 생성된 interval은 변수에 할당하는 경우 변수명이 intervalID가 되며 `clearInterval(intervalID)`함수는 전달된 intervalID에 해당하는 interval을 종료시키는 함수다.

 

2. 구현 결과 미리보기

 

Vanilla js auto loop slide
1
2
3
4
5

     

    3. 구현

    3.1. HTML

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vanilla js auto loop slide</title>
        <link rel="stylesheet" href="./slide.css" />
      </head>
      <body>
        <div class="slide slide_wrap">
          <div class="slide_item item1">1</div>
          <div class="slide_item item2">2</div>
          <div class="slide_item item3">3</div>
          <div class="slide_item item4">4</div>
          <div class="slide_item item5">5</div>
          <div class="slide_prev_button slide_button">◀</div>
          <div class="slide_next_button slide_button">▶</div>
          <ul class="slide_pagination"></ul>
        </div>
        <script src="./slide.js"></script>
      </body>
    </html>

    3.2. CSS

    .slide {
      /* layout */
      display: flex;
      flex-wrap: nowrap;
      /* 컨테이너의 내용물이 컨테이너 크기(width, height)를 넘어설 때 보이지 않도록 하기 위해 hidden을 준다. */
      overflow: hidden;
    
      /* position */
      /* slide_button의 position absolute가 컨테이너 안쪽에서 top, left, right offset이 적용될 수 있도록 relative를 준다. (기본값이 static인데, static인 경우 그 상위 컨테이너로 나가면서 현재 코드에선 html을 기준으로 offset을 적용시키기 때문) */
      position: relative;
    
      /* size */
      width: 100%;
    
      /* slide drag를 위해 DOM요소가 드래그로 선택되는것을 방지 */
      user-select: none;
    }
    .slide_item {
      /* layout */
      display: flex;
      align-items: center;
      justify-content: center;
    
      /* position - 버튼 클릭시 left offset값을 적용시키기 위해 */
      position: relative;
      left: 0px;
    
      /* size */
      width: 100%;
      height: 300px;
      /* flex item의 flex-shrink는 기본값이 1이므로 컨테이너 크기에 맞게 줄어드는데, 슬라이드를 구현할 것이므로 줄어들지 않도록 0을 준다. */
      flex-shrink: 0;
    
      /* transition */
      transition: left 0.15s;
    }
    .slide_button {
      /* layout */
      display: flex;
      justify-content: center;
      align-items: center;
    
      /* position */
      position: absolute;
      /* 버튼이 중앙에 위치하게 하기위해 계산 */
      top: calc(50% - 16px);
    
      /* size */
      width: 32px;
      height: 32px;
    
      /* style */
      border-radius: 100%;
      background-color: #cccc;
      cursor: pointer;
    }
    
    .slide_prev_button {
      left: 10px;
    }
    .slide_next_button {
      right: 10px;
    }
    
    /* 각 슬라이드가 변경되는 것을 시각적으로 확인하기 쉽도록 각 슬라이드별 색상 적용 */
    .slide_item.item1 {
      background-color: darkgoldenrod;
    }
    .slide_item.item2 {
      background-color: aqua;
    }
    .slide_item.item3 {
      background-color: blueviolet;
    }
    .slide_item.item4 {
      background-color: burlywood;
    }
    .slide_item.item5 {
      background-color: cornflowerblue;
    }
    
    /* 페이지네이션 스타일 */
    ul,
    li {
      list-style-type: none;
      padding: 0;
      margin: 0;
    }
    .slide_pagination {
      /* layout */
      display: flex;
      gap: 5px;
    
      /* position */
      position: absolute;
      bottom: 0;
      /* left:50%, translateX(-50%)를 하면 가로 가운데로 위치시킬 수 있다. */
      left: 50%;
      transform: translateX(-50%);
    }
    .slide_pagination > li {
      /* 현재 슬라이드가 아닌 것은 투명도 부여 */
      color: #7fb5ff88;
      cursor: pointer;
      font-size: 25px;
    }
    .slide_pagination > li.active {
      /* 현재 슬라이드 색상은 투명도 없이 */
      color: #7fb5ff;
    }
    
    .slide_item_duplicate {
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      left: 0px;
      width: 100%;
      height: 300px;
      flex-shrink: 0;
      transition: left 0.15s;
    }

    3.3. JS

    // 슬라이크 전체 크기(width 구하기)
    const slide = document.querySelector(".slide");
    let slideWidth = slide.clientWidth;
    
    // 버튼 엘리먼트 선택하기
    const prevBtn = document.querySelector(".slide_prev_button");
    const nextBtn = document.querySelector(".slide_next_button");
    
    // 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
    let slideItems = document.querySelectorAll(".slide_item");
    // 현재 슬라이드 위치가 슬라이드 개수를 넘기지 않게 하기 위한 변수
    const maxSlide = slideItems.length;
    
    // 버튼 클릭할 때 마다 현재 슬라이드가 어디인지 알려주기 위한 변수
    let currSlide = 1;
    
    // 페이지네이션 생성
    const pagination = document.querySelector(".slide_pagination");
    
    for (let i = 0; i < maxSlide; i++) {
      if (i === 0) pagination.innerHTML += `<li class="active">•</li>`;
      else pagination.innerHTML += `<li>•</li>`;
    }
    
    const paginationItems = document.querySelectorAll(".slide_pagination > li");
    
    // 무한 슬라이드를 위해 start, end 슬라이드 복사하기
    const startSlide = slideItems[0];
    const endSlide = slideItems[slideItems.length - 1];
    const startElem = document.createElement("div");
    const endElem = document.createElement("div");
    
    endSlide.classList.forEach((c) => endElem.classList.add(c));
    endElem.innerHTML = endSlide.innerHTML;
    
    startSlide.classList.forEach((c) => startElem.classList.add(c));
    startElem.innerHTML = startSlide.innerHTML;
    
    // 각 복제한 엘리먼트 추가하기
    slideItems[0].before(endElem);
    slideItems[slideItems.length - 1].after(startElem);
    
    // 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
    slideItems = document.querySelectorAll(".slide_item");
    //
    let offset = slideWidth + currSlide;
    slideItems.forEach((i) => {
      i.setAttribute("style", `left: ${-offset}px`);
    });
    
    function nextMove() {
      currSlide++;
      // 마지막 슬라이드 이상으로 넘어가지 않게 하기 위해서
      if (currSlide <= maxSlide) {
        // 슬라이드를 이동시키기 위한 offset 계산
        const offset = slideWidth * currSlide;
        // 각 슬라이드 아이템의 left에 offset 적용
        slideItems.forEach((i) => {
          i.setAttribute("style", `left: ${-offset}px`);
        });
        // 슬라이드 이동 시 현재 활성화된 pagination 변경
        paginationItems.forEach((i) => i.classList.remove("active"));
        paginationItems[currSlide - 1].classList.add("active");
      } else {
        // 무한 슬라이드 기능 - currSlide 값만 변경해줘도 되지만 시각적으로 자연스럽게 하기 위해 아래 코드 작성
        currSlide = 0;
        let offset = slideWidth * currSlide;
        slideItems.forEach((i) => {
          i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
        });
        currSlide++;
        offset = slideWidth * currSlide;
        // 각 슬라이드 아이템의 left에 offset 적용
        setTimeout(() => {
          // 각 슬라이드 아이템의 left에 offset 적용
          slideItems.forEach((i) => {
            // i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
            i.setAttribute("style", `transition: ${0.15}s; left: ${-offset}px`);
          });
        }, 0);
        // // 슬라이드 이동 시 현재 활성화된 pagination 변경
        paginationItems.forEach((i) => i.classList.remove("active"));
        paginationItems[currSlide - 1].classList.add("active");
      }
    }
    function prevMove() {
      currSlide--;
      // 1번째 슬라이드 이하로 넘어가지 않게 하기 위해서
      if (currSlide > 0) {
        // 슬라이드를 이동시키기 위한 offset 계산
        const offset = slideWidth * currSlide;
        // 각 슬라이드 아이템의 left에 offset 적용
        slideItems.forEach((i) => {
          i.setAttribute("style", `left: ${-offset}px`);
        });
        // 슬라이드 이동 시 현재 활성화된 pagination 변경
        paginationItems.forEach((i) => i.classList.remove("active"));
        paginationItems[currSlide - 1].classList.add("active");
      } else {
        // 무한 슬라이드 기능 - currSlide 값만 변경해줘도 되지만 시각적으로 자연스럽게 하기 위해 아래 코드 작성
        currSlide = maxSlide + 1;
        let offset = slideWidth * currSlide;
        // 각 슬라이드 아이템의 left에 offset 적용
        slideItems.forEach((i) => {
          i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
        });
        currSlide--;
        offset = slideWidth * currSlide;
        setTimeout(() => {
          // 각 슬라이드 아이템의 left에 offset 적용
          slideItems.forEach((i) => {
            // i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
            i.setAttribute("style", `transition: ${0.15}s; left: ${-offset}px`);
          });
        }, 0);
        // 슬라이드 이동 시 현재 활성화된 pagination 변경
        paginationItems.forEach((i) => i.classList.remove("active"));
        paginationItems[currSlide - 1].classList.add("active");
      }
    }
    
    // 버튼 엘리먼트에 클릭 이벤트 추가하기
    nextBtn.addEventListener("click", () => {
      // 이후 버튼 누를 경우 현재 슬라이드를 변경
      nextMove();
    });
    // 버튼 엘리먼트에 클릭 이벤트 추가하기
    prevBtn.addEventListener("click", () => {
      // 이전 버튼 누를 경우 현재 슬라이드를 변경
      prevMove();
    });
    
    // 브라우저 화면이 조정될 때 마다 slideWidth를 변경하기 위해
    window.addEventListener("resize", () => {
      slideWidth = slide.clientWidth;
    });
    
    // 각 페이지네이션 클릭 시 해당 슬라이드로 이동하기
    for (let i = 0; i < maxSlide; i++) {
      // 각 페이지네이션마다 클릭 이벤트 추가하기
      paginationItems[i].addEventListener("click", () => {
        // 클릭한 페이지네이션에 따라 현재 슬라이드 변경해주기(currSlide는 시작 위치가 1이기 때문에 + 1)
        currSlide = i + 1;
        // 슬라이드를 이동시키기 위한 offset 계산
        const offset = slideWidth * currSlide;
        // 각 슬라이드 아이템의 left에 offset 적용
        slideItems.forEach((i) => {
          i.setAttribute("style", `left: ${-offset}px`);
        });
        // 슬라이드 이동 시 현재 활성화된 pagination 변경
        paginationItems.forEach((i) => i.classList.remove("active"));
        paginationItems[currSlide - 1].classList.add("active");
      });
    }
    
    // 드래그(스와이프) 이벤트를 위한 변수 초기화
    let startPoint = 0;
    let endPoint = 0;
    
    // PC 클릭 이벤트 (드래그)
    slide.addEventListener("mousedown", (e) => {
      startPoint = e.pageX; // 마우스 드래그 시작 위치 저장
    });
    
    slide.addEventListener("mouseup", (e) => {
      endPoint = e.pageX; // 마우스 드래그 끝 위치 저장
      if (startPoint < endPoint) {
        // 마우스가 오른쪽으로 드래그 된 경우
        prevMove();
      } else if (startPoint > endPoint) {
        // 마우스가 왼쪽으로 드래그 된 경우
        nextMove();
      }
    });
    
    // 모바일 터치 이벤트 (스와이프)
    slide.addEventListener("touchstart", (e) => {
      startPoint = e.touches[0].pageX; // 터치가 시작되는 위치 저장
    });
    slide.addEventListener("touchend", (e) => {
      endPoint = e.changedTouches[0].pageX; // 터치가 끝나는 위치 저장
      if (startPoint < endPoint) {
        // 오른쪽으로 스와이프 된 경우
        prevMove();
      } else if (startPoint > endPoint) {
        // 왼쪽으로 스와이프 된 경우
        nextMove();
      }
    });
    
    // 기본적으로 슬라이드 루프 시작하기
    let loopInterval = setInterval(() => {
      nextMove();
    }, 3000);
    
    // 슬라이드에 마우스가 올라간 경우 루프 멈추기
    slide.addEventListener("mouseover", () => {
      clearInterval(loopInterval);
    });
    
    // 슬라이드에서 마우스가 나온 경우 루프 재시작하기
    slide.addEventListener("mouseout", () => {
      loopInterval = setInterval(() => {
        nextMove();
      }, 3000);
    });