본문 바로가기
WEB Basic/JavaScript

Vanilla JavaScript Slider 자바스크립트 슬라이드 드래그(drag), 스와이프(swipe) 구현

by Devinus 2022. 5. 10.

1. 분석

이전 슬라이드 페이지네이션 구현 포스팅에 이어서 드래그 기능을 추가한 슬라이드를 구현하려 한다.

이번에도 마찬가지로 Jquery나 다른 API를 사용하지 않고 순수 자바스크립트만을 이용해 슬라이드 드래그(스와이프)를 구현하려 한다.

 

드래그(스와이프)로 슬라이드를 조작하기 위해서 자바스크립트의 DOM 이벤트(MDN 이벤트 리스트)를 이용한다.

드래그는 PC에서 마우스 버튼 누르기(mousedown), 마우스 버튼 떼기(mouseup) 이벤트를 이용해서 드래그를 구현하고, 스와이프는 모바일기기에서의 터치 시작(touchstart), 터치 끝(touchend) 이벤트를 이용해서 스와이프를 구현한다.

시작 위치(startPoint)와 끝 위치(endPoint)를 각 이벤트마다 할당받아서 시작 위치와 끝 위치를 비교해서 이전, 이후 슬라이드 이동을 결정한다.

이 과정에서 자주 사용되는 이전 슬라이드 이동(prevMove), 이후 슬라이드 이동(nextMove) 코드가 반복되기 때문에 함수로 만들어 재사용한다.

// 다른 코드들은 생략 ...
// 드래그(스와이프) 이벤트를 위한 변수 초기화
let startPoint = 0;
let endPoint = 0;

// PC 클릭 이벤트 (드래그)
slide.addEventListener("mousedown", (e) => {
  console.log("mousedown", e.pageX);
  startPoint = e.pageX; // 마우스 드래그 시작 위치 저장
});

slide.addEventListener("mouseup", (e) => {
  console.log("mouseup", e.pageX);
  endPoint = e.pageX; // 마우스 드래그 끝 위치 저장
  if (startPoint < endPoint) {
    // 마우스가 오른쪽으로 드래그 된 경우
    console.log("prev move");
    prevMove();
  } else if (startPoint > endPoint) {
    // 마우스가 왼쪽으로 드래그 된 경우
    console.log("next move");
    nextMove();
  }
});

// 모바일 터치 이벤트 (스와이프)
slide.addEventListener("touchstart", (e) => {
  console.log("touchstart", e.touches[0].pageX);
  startPoint = e.touches[0].pageX; // 터치가 시작되는 위치 저장
});
slide.addEventListener("touchend", (e) => {
  console.log("touchend", e.changedTouches[0].pageX);
  endPoint = e.changedTouches[0].pageX; // 터치가 끝나는 위치 저장
  if (startPoint < endPoint) {
    // 오른쪽으로 스와이프 된 경우
    console.log("prev move");
    prevMove();
  } else if (startPoint > endPoint) {
    // 왼쪽으로 스와이프 된 경우
    console.log("next move");
    nextMove();
  }
});

드래그와 스와이프 각각 구현하는 이유는 PC와 모바일의 클릭, 터치 이벤트가 다르기 때문에 각각 구현해줘야 PC나 모바일 상관 없이 슬라이드 드래그(스와이프)가 작동하기 때문이다.

 

또한 드래그(스와이프)를 원활히 작동시키기 위해 slide 컨테이너 클래스에 `user-select: none`속성을 추가한다.

.slide {
  /* 다른 속성들은 생략 ... */
  /* slide drag를 위해 DOM요소가 드래그로 선택되는것을 방지 */
  user-select: none;
}

 

2. 구현 결과 미리보기

 

Vanilla js slide with drag(swipe)
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 slide with drag(swipe)</title>
        <link rel="stylesheet" href="./slide.css" />
      </head>
      <body>
        <div class="slide slide_wrap">
          <div class="slide_item">1</div>
          <div class="slide_item">2</div>
          <div class="slide_item">3</div>
          <div class="slide_item">4</div>
          <div class="slide_item">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:nth-child(1) {
      background-color: darkgoldenrod;
    }
    .slide_item:nth-child(2) {
      background-color: aqua;
    }
    .slide_item:nth-child(3) {
      background-color: blueviolet;
    }
    .slide_item:nth-child(4) {
      background-color: burlywood;
    }
    .slide_item:nth-child(5) {
      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;
    }

    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");
    
    // 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
    const 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");
    
    function nextMove() {
      currSlide++;
      // 마지막 슬라이드 이상으로 넘어가지 않게 하기 위해서
      if (currSlide <= maxSlide) {
        // 슬라이드를 이동시키기 위한 offset 계산
        const offset = slideWidth * (currSlide - 1);
        // 각 슬라이드 아이템의 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--;
      }
    }
    function prevMove() {
      currSlide--;
      // 1번째 슬라이드 이하로 넘어가지 않게 하기 위해서
      if (currSlide > 0) {
        // 슬라이드를 이동시키기 위한 offset 계산
        const offset = slideWidth * (currSlide - 1);
        // 각 슬라이드 아이템의 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++;
      }
    }
    
    // 버튼 엘리먼트에 클릭 이벤트 추가하기
    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 - 1);
        // 각 슬라이드 아이템의 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) => {
      console.log("mousedown", e.pageX);
      startPoint = e.pageX; // 마우스 드래그 시작 위치 저장
    });
    
    slide.addEventListener("mouseup", (e) => {
      console.log("mouseup", e.pageX);
      endPoint = e.pageX; // 마우스 드래그 끝 위치 저장
      if (startPoint < endPoint) {
        // 마우스가 오른쪽으로 드래그 된 경우
        console.log("prev move");
        prevMove();
      } else if (startPoint > endPoint) {
        // 마우스가 왼쪽으로 드래그 된 경우
        console.log("next move");
        nextMove();
      }
    });
    
    // 모바일 터치 이벤트 (스와이프)
    slide.addEventListener("touchstart", (e) => {
      console.log("touchstart", e.touches[0].pageX);
      startPoint = e.touches[0].pageX; // 터치가 시작되는 위치 저장
    });
    slide.addEventListener("touchend", (e) => {
      console.log("touchend", e.changedTouches[0].pageX);
      endPoint = e.changedTouches[0].pageX; // 터치가 끝나는 위치 저장
      if (startPoint < endPoint) {
        // 오른쪽으로 스와이프 된 경우
        console.log("prev move");
        prevMove();
      } else if (startPoint > endPoint) {
        // 왼쪽으로 스와이프 된 경우
        console.log("next move");
        nextMove();
      }
    });