본문 바로가기

UI 디자인 애니메이션 연구소

캐러셀 라이브러리에서 보던 드래그 가능한 슬라이드, 원리 부터 구현까지!!

반응형

 

이전 애니메이션 |  액자 모양 로고 애니메이션

 

HTML과 CSS 만으로 이런 것도 가능해?? 액자 모양 로고 애니메이션

이전 애니메이션 | 스크롤링 헤드라인, 마키 어디가 끝이지? 끝없이 흘러가는 마키 UI 애니메이션이전 애니메이션 | 스크롤링 헤드라인 스크롤링 헤드라인 이라 불리는 뉴스티커 UI, 원리부터

duklook.tistory.com

 
 


 
 

드래그 가능한 슬라이드

보통 모바일 환경에서든 웹 환경에서든 사용자가 마우스나 패드를 이용해서 드래그를 하면 슬라이드가 따라 움직이는 것을 볼 수 있습니다. 오늘은 그 기능을 구현해보는 시간을 가져볼까 합니다. 아래는 구현된 예시인데요. 한 번 시작전에 어떻게 동작하는지 한 번 확인해보세요!

 
 

드래그 가능한 슬라이드의 원리

일단, 기능을 구현하기 전에는 해당 기능이 어떤 원리로 동작하는지 파악해야 합니다. 그래야 단계별로 부품을 만들고, 그 부품을 조립함으로써 하나의 동작하는 제품을 만들 수 있으니 말이죠.
 
일단 눈에 보이는 동작 원리는 크게 설명할게 없습니다. 버튼으로 이동하는 슬라이드를 만들어보셨다면, 슬라이드가 변경되는 원리 자체는 아실겁니다. 다만, 마우스가 드래그 될 때 자연스럽게 슬라이드가 움직이는 점, 그리고 마우스를 놓을 때, 슬라이드가 자연스럽게 다음 슬라이드로 이동하는 점들이 새로운 개념일 겁니다. 그래서 이것에 대해서 초점을 맞춰 설명해보겠습니다.
 

움직이는 원리 | mousedown, mousemove, mouseup 이벤트의 개념

해당 슬라이드를 구현하려면 mousedown, mousemove, mouseup 이라는 이벤트에 대해서 이해하셔야 합니다.
 

mousedown

우선 mousedown 은 마우스의 좌측 버튼을 처음 누른 순간에 발생하는 이벤트 입니다.  click 이벤트와 헷갈리실수도 있는데, click 이벤트는 마우스를 클릭한 이후 동작하는 것이고, mousedown은 마우스를 클릭하는 시점에서 동작하는 이벤트 입니다.
 
이 차이점을 꼭 기억해주세요.
 

 

mousemove

mousemove 이벤트는 말그대로 마우스를 이리저리 움직일 때 마다 매번 발동하는 이벤트 입니다. 여러분이 쓰고 계시는 마우스를 움직이면 그 움직이는 시간 동안 계속해서 이벤트가 등록되어 호출 됩니다.

 
 

mouseup

mouseup 이벤트는 마우스를 누른 상태에서 떼어 내는 순간 동작하는 이벤트 입니다. 이건 크게 설명할게 없으므로 바로 넘어가도록 하겠습니다.
 
 
이 외에도 이벤트 호출과 관련해서 자세한 내용을 알아보고 싶으시다면, ( https://developer.mozilla.org/en-US/docs/Web/API/Element/mousedown_event )  MDN 문서를 확인해 주세요.
 
 

움직이는 원리 | 동작의 흐름

그럼 앞서 살펴본 이벤트들이 서로 어떻게 유기적으로 동작하는지 알아보겠습니다. 우선 앞서 제가 등록해둔 영상을 다시 살펴보시면, 마우스를 누른 상태에서 움직이니까 슬라이드도 동시에 움직였고, 움직인 상태에서 마우스를 떼니 다음 슬라이드 혹은 제자리로 자연스럽게 돌아오는 것을 볼 수 있었습니다. 
 
이게 왜 가능했을까요? 답은 앞서 살펴본 이벤트의 동작을 이어보면 이해가 됩니다.
 

mouse 의 현재 상태를 저장할 boolean 타입의 변수

우선 mousedown 을 통해서 현재 마우스가 down 된 상태인지 아닌지를 나타내는 변수가 하나 필요합니다. 해당 변수는 boolean 타입으로써 true/false 를 할당할 수 있고, 최초의 상태는 false 로 되어 있습니다. 
 
여기서는 이해를 위해 해당 변수를 isPress 라고 명명하겠습니다. let isPress = false 로 초기화되어 있다고 이해하시면 됩니다.
 

mousedown 이 되면  isPress에 저장된 값이 true 로

mousedown 이벤트가 실행될 때, 우리는 콜백함수를 하나 호출하게 됩니다. 해당 함수가 호출될 때 isPress 의 상태를 true 로 변경 합니다.
 

isPress가 true 인 동안에만 mousemove 이벤트가 호출

isPress 가 true 인 동안에만 mousemove 이벤트에 연결된 콜백함수가 호출되도록 합니다. 해당 콜백함수는 마우스가 움직이는 동안에 계속해서 호출되겠죠.
 

mouseup 이 되는 순간 isPress 를 false 로

사용자가 마우스를 누른 상태로 이동하다가 떼어버리면, 이 때 isPress 의 값을 false로 변경 시킵니다. 앞서 mousemove 의 경우에는 isPress가 true 인 경우에만 실행되도록 했으므로, false 가 되는 순간 mousemove로 인해 발생하는 이벤트가 중지됩니다.
 


 
 
우선은 아주 단편적인 부분이지만 핵심적인 동작의 원리에 대해서 알아보았습니다. 이제 부터는 코드로 직접 구현해보면서 알아보도록 합시다.
 

HTML

HTML 마크업은 아래와 같이 container > slider > slide 형식이 되도록 만들어주세요. container 는 위치를 잡고, slider 는 slide 의 래퍼로서 실제 slide 대신 움직이는 친구입니다. 왜 그런지는 JS 에서 알아봅시다.

<div class="container">
  <div class="slider">
    <div class="slide"><span>슬라이드1</span></div>
    <div class="slide"><span>슬라이드2</span></div>
    <div class="slide"><span>슬라이드3</span></div>
    <div class="slide"><span>슬라이드4</span></div>
    <div class="slide"><span>슬라이드5</span></div>
  </div>
</div>

 

CSS

CSS 부분은 딱히 설명드릴 부분이 없습니다. 매번 포스트 마다 설명을 하였으나, 어려운 코드도 아니니 글만 길어지고 해서 생략하는 방향으로 바꿨습니다. 

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  width: 100%;
  height: 100vh;
}

.container {
  overflow: hidden;
  width: 100%;
  height: 100%;

  .slider {
    display: flex;
    flex-shrink: 0;
    transform: translate(0);
    width: 100%;
    height: 100%;
  }
}
.container .slider .slide {
  user-select: none;
  min-width: 100%;
  height: 100%;
}

.slide {
  display: flex;
  justify-content: center;
  align-items: center;
  border: 2px solid black;
  font-size: 3.5rem;
}

 
 


 
 
전체적으로 코드를 적용하면 슬라이드 1 이라는 커다란 글자가 화면의 중앙에 정렬되어 있고, slider 와 slide 가 뷰포트 전체를 차지하고 있는 것을 확인할 수 있습니다.
 

 

JS

DOM 요소 가져오기 및 초기 상태 설정

우선 container, slider, slide 를 모두 가져와서 변수에 할당해 줍니다.

const slider = document.querySelector(".slider");
const slides = document.querySelectorAll(".slide");
const slideLen = slides.length;

 
 
그 후 현재 슬라이드를 나타내는(페이지) 변수와 마우스의 down, up 여부를 나타내는 변수를 각각 만들어서 초기화해줍니다. 이 때 마우스 관련 변수는 한번 객체로 감싸서 저장하였는데, let  키워드를 사용한 변수에 저장해도 무관합니다.

const currentState = {
  page: 0
};


const playState = {
  isMove: false
};

 

slider에 마우스 이벤트 등록하기

앞서 slider 의 경우 실제 slide 대신하여 움직이는 역할을 하는 래퍼 요소라고 소개하였습니다. 따라서 해당 요소에 이벤트 리스너를 등록하여 향후 처리를 위한 준비를 해줍니다.

slider.addEventListener("mousedown",(e)=>{});
slider.addEventListener("mousemove", (e) => {});
slider.addEventListener("mouseup", (e)=>{});

 
mousedown, mousemove, mouseup 을 모두 등록했다면 위와 같은 형태가 되어야 합니다.
 


그럼 이제부터 하나하나 구현해보도록 합시다

mousedown 시 동작 구현하기

mousedown은 앞서 말했듯이 마우스를 누르는 동안 한 번 발생하는 이벤트라고 하였습니다. 즉, 이 때 우리가 필요한 정보는 마우스를 눌렀을 때의 위치 정보이고, 필요한 동작은 playState의 isMove 프로퍼티의 값으로 true를 할당해주어야 합니다.

slider.addEventListener("mousedown", (e) => {
  playState.isMove = true; // 마우스 누르는 중임을 나타냄
  const start = e.clientX; // 현재 마우스의 좌표
  pointState.startPoint = e.clientX;
});

 
바로 아셨듯이 start 라는 변수에 현재 마우스 좌표 정보를 담고, 다시 이를 pointState의 startPoint 변수에 할당하고 있는 것을 볼 수 있습니다. 이렇게 하는 이유는 위치 좌표를 모든 mouse 이벤트 내 콜백함수에서 재사용해야 하기 때문입니다. 
 
따라서 우리는 다음과 같이 위치정보를 저정하고 있는 전역상태를 관리하는 객체를 하나더 만들어 주어야 합니다. 이들은 순서대로 시작 위치(=startPoint), 끝 위치(endPoint), 움직인 위치(movePoint), 시작 위치에서 움직인 위치 까지의 거리(distance) 를 나타냅니다.  -> 정확히는 지점(=point) 가 맞겠지만 편의 상 위치로 하겠습니다.

const pointState = {
  startPoint: 0,
  endPoint: 0,
  movePoint: 0,
  distance: 0
};

 

 mousemove 시 동작 구현하기

이번에는 mousemove 이벤트가 등록되면서 호출하는 함수의 동작을 구현해볼 차레입니다. 해당 함수는 호출이 되면서 움직이는 위치 좌표를 추적하기 때문에, 우리는 이 정보를 이용해서 시작 지점에서 움직인 거리, 현재 위치 등을 알 수 있게 됩니다. 
 
뭔가 엄청난 동작을 할 것 같은 이벤트였는데, 사실은 별게 없습니다. e.clientX 는 mousemove 이벤트가 동작하면서 갱신되는 현재위치 좌표를 담고 있습니다. 거기서 앞서 start 지점의 좌표정보를 빼주게 되면(현재위치 - 시작위치 = 움직인 거리), 움직인 거리(distance) 를 알 수 있게 되는데, 이 값을 이용하면 화면의 전체를 차지하는 슬라이드의 중간을 기준으로 왼쪽과 오른쪽 방향을 구분할 수 있게 됩니다. 

slider.addEventListener("mousemove", (e) => {
  if (!playState.isMove) return;
  pointState.distance = e.clientX - pointState.startPoint;

  handleMouseMove(e);
});

function handleMouseMove(e) {
  const sliderWidth = e.clientX - pointState.startPoint + window.innerWidth * -currentState.page;
  e.currentTarget.style.transform = `translateX(${sliderWidth}px)`;
}

 
 
그리고 handleMouseMove() 함수가 내부적으로 호출이 되고 있습니다. 이 함수는 슬라이드의 현재 위치를 마우스가 움직일 때 같이 움직이도록 해주는 로직입니다. 보시면 현재 위치에서 이동한 거리 에 window.innerWidth * -currentState.page. 를 계산해주고 있는데, window.innerWidth 를 사용하는 이유는 해당 slide 요소 하나의 크기와 뷰포트 전체 넓이가 동일하기 때문입니다. 여기서 currentState.page 는 현재 슬라이드의 위치를 나타내는 index 인데, 해당 값이 업데이트 되는 시기는 mousemove 가 아니라 mouseup 이벤트가 호출되는 시점이므로 나중에 알아보도록 합시다.

function handleMouseMove(e) {
  const sliderWidth = e.clientX - pointState.startPoint + window.innerWidth * -currentState.page;
  e.currentTarget.style.transform = `translateX(${sliderWidth}px)`;
}

 
 

distance 는 이동한 거리를 나타내는 동시에 슬라이드의 좌-우 이동 방향의 척도가 된다.

앞서 distance 에 이동한 거리에 대한 값을 할당하고 있었습니다. 이는 방향을 판단하는 척도가 되기도 하는데, 왜 그런 것인지 그 이유를 설명해보겠습니다.


 
우선 전체 window.innerWidth 가 300px 를 가정하고 해보겠습니다.  mousedown 이벤트가 호출될 때 e.clientX 는 240px 입니다. 반면, 해당 지점에서 이동 할 때 마다 호출되는 mousemove 이벤트의 경우 e.clientX 는 동적으로 변경이 되고, 마지막에 도달한 위치가 50px 정도가 됩니다.  이 때, 두 사이의 거리는 이동한 거리에서 처음 거리를 빼주면 50-240 = -190px 가 됩니다. 이 값이 distance 프로퍼티에 실시간으로 할당되고 있습니다.

 
그럼 여기서 반대가 되면 어떻게 될까요? -190 이었던 distance 가 190 가 되었습니다. 눈치 채셨겠지만, 리는 -와 + 의 차이에 따라 슬라이드의 전환 및 방향을 특정지을 겁니다.

 
 
참고로 currentState.page 에 담긴 인덱스를 window.innerWidth 와 곱해주면 슬라이드가 변경되는 이유가 slide 하나의 크기가 innerWidth 와 동일하기 때문입니다. 예를 들어 page 가 0 이라면  transform의 translateX 에 할당할 시 0px 이므로 제자리에 위치하지만,  -1인 경우에는 300px * -1 = -300px 가 되므로 slide1 을 좌측으로 밀어내고, -300px 지점에 있는 slide2 가 화면에 보이게 됩니다.

 
 

mouseup 시 동작 구현하기

사실 상 앞서 이벤트 호출에 대한 동작의 종지부 이자 제일 중요한 부분입니다. mouseup 는 마우스에서 사용자가 손을 떼는 순간 1번 이벤트를 호출합니다. 이 때 playState.isMove 를 false 로 지정하게 되면 마우스를 이동하더라도 더 이상 mousemove 이벤트에 등록된 함수가 호출되지 않습니다. 그리고 앞서 이동한 거리에서 계산된 거리인 distance 를 기준으로 슬라이드를 좌측으로 아니면 우측으로 이동시킬지 결정하는 역할을 수행합니다.
 
동작이 많기 때문에, 함수를 별도로 선언하여 사용할 겁니다.

slider.addEventListener("mouseup", handleMouseUp);

 
함수를 살펴보면, 앞서 언급했듯이 playState 를 false 로 만들어 주고 있습니다. 그 후 pointState.endPoint 에 현재 끝지점 좌표인 e.clientX 를 할당하고 있습니다. 그리고 트랜잭션를 걸어주고 있는데, 이는  모든 계산이 끝나고 슬라이드의 상태를 결정지을 때, 부드럽게 이동하는 애니메이션을 구현하기 위해서 입니다.

function handleMouseUp(e) {
  playState.isMove = false;
  pointState.endPoint = e.clientX;
  
  slider.style.transition = "transform 0.1s ease";

}

 
그리고 위 함수에 한 가지 더 추가해야 할 로직이 있습니다. 바로 distance 를 사용하여 슬라이드의 방향을 결정하는 로직입니다.  해당 로직을 보면 distance 가 100 보다 크면 page 를 1 줄이고, -100 보다 작거나 같으면 page 를 1 늘리는 동작을 분기처리하고 있습니다. 마지막으로는 trnasform 의 translate 를 사용하여 슬라이드의 위치를 변경하는 동작을 수행하고 있습니다.

  const distance = pointState.distance;
  if (distance > 100 && currentState.page > 0) {
    --currentState.page;
  }
  if (distance <= -100 && currentState.page < slideLen - 1) {
    ++currentState.page;
  }
  e.currentTarget.style.transform = `translate(${-100 * currentState.page}%)`;

 
 
distance 는 앞서 mousemove 예제에서 살펴보았듯이,  우측→ 좌측 으로 드래그 하면 50-240 이 되면서 -190 이 되었고, 좌측 → 우측 으로 드래그 하면 240-50 이 되면서 190 이 되었습니다. 이 원리를 기반으로 distance 가 100 보다 크다면, 슬라이드를 우측으로 이동시키고, -100 보다 작다면 음수이므로 슬라이드를 좌측으로 이동시킵니다. 이 때 페이지 이동의 기준에 따라  페이지를 실질적으로 변경하는데 사용하는 currentState.page 에 할당되는 인덱스를 조정하기만 하면 됩니다.

왼쪽에서 오른쪽으로 드래그 하면 양수 이므로 슬라이드는 우측 으로 이동하며, 오른쪽에서 왼쪽으로 드래그 하면, 음수 이므로 슬라이드는 좌측으로 이동합니다.

 

100과 -100 을 경계로 둔 이유

여기서 100과 -100 을 경계로 둔 이유는 재량의 문제입니다. 저의 경우에는 저 값의 범위가 되었을 때 자연스러운 슬라이드전환이 되어서 사용하는 겁니다.
 
이 부분을 보면 mousemove 의 함수 내에서와 로직이 다른 것을 볼 수 있습니다. translate의 경우에는 첫 번째 인자가 x 축 좌표를 나타내므로 translateX 와 동일하며, 다른 점은 mousemove 에서는 px 단위로 움직였지만, 여기서는 퍼센트 단위로 움직이고 있다는 점입니다. 이는 간단하게 생각하면 됩니다. 앞서 mousemove 는 페이지의 위치를 확정짓는 것이 아니라 드래그 시 자연스러운 움직임을 주기 위해서 요소의 위치를 변화시켜 준것에 불과합니다.  반면 mouseup 에서 사용된 퍼센트 단위의 페이지 이동이 실질적으로 페이지를 전환하는데 사용되는 핵심 입니다.

e.currentTarget.style.transform = `translate(${-100 * currentState.page}%)`;

 
이 부분이 왜 중요한지는 완성된 코드를 보시면서 연구해보시면 많은 도움이 되실거라 생각합니다. 원리 자체가 복잡하거나 어려운 것이 없으므로 제가 이렇게 글을 장황하게 적은것 보다 직접 코드로 보고 건드려보는게 더 이해하기 쉬울 겁니다.
 

구현 결과

See the Pen Untitled by youngwan2 (@youngwan2) on CodePen.

 
 

마무리

오늘은 캐러셀 라이브러리에서 자주사용되는 드래그 슬라이드 애니메이션을 구현해보았습니다. 다만 위 결과는 웹 화면상에서만 동작하는 코드인데요.  사실 로직은 공통되는데 이벤트만 다를 뿐이라 모바일 상에서도 동작하는 코드도 어여울 것이 전혀 없습니다. 어떤 이벤트를 등록해야 하는지, 구현된 코드를 어떻게 하면 더 재사용성 있게 작성할 수 있을지 고민해보는 시간을 가지는 것도 좋을 것 같다고 봅니다.

 

다음 애니메이션 | 바닐라 스크립트를 이용한 스크롤 트리거 애니메이션

 

바닐라 자바스크립트를 이용한 스크롤 트리거 애니메이션의 원리와 구현

이전 애니메이션 | 드래그 가능한 슬라이드 구현하기 캐러셀 라이브러리에서 보던 드래그 가능한 슬라이드, 원리 부터 구현까지!!이전 애니메이션 |  액자 모양 로고 애니메이션 HTML과 CSS 만으

duklook.tistory.com

 

반응형