개발 기술/css 애니메이션 (with js)

carousel slider 만들기 (with js)

by GicoMomg 2021. 9. 27.

이번시간에는 mouseup, down, move이벤트를 사용해 carousel slider를 만들어보자!

See the Pen vanilla js carosal silder by KumJungMin (@kumjungmin) on CodePen.




1. html 구조

html 구조는 아래 그림과 같다.

<div id="slider">
  <button class="btn prev"><</button>
  <div class="wrapper">
    <div class="items">
      <div class="item"></div>
      <div class="item"><div class="content pink">1</div></div>
      <div class="item"><div class="content yellow">2</div></div>
      <div class="item"><div class="content skyblue">3</div></div>
      <div class="item"><div class="content orange">4</div></div>
      <div class="item"></div>
    </div> 
  </div>  
  <button class="btn next">></button>
</div>
  • silder : 전체 슬라이드 영역이다.
  • btn : 좌, 우측에 위치한 이동하기 버튼이다.
  • wrapper: 현재 슬라이드가 보여지는 부분이다.
  • items: item을 감싸는 큰 box이다. (슬라이드는 이 items의 위치(X축) 변경으로 이루어짐)
  • item: 각각 슬라이드 아이템을 지칭한다.(회색박스 item은 좌 우 여백을 주기 위한 용도)
  • content: 각각 슬라이드 페이지에 넣을 내용을 작성한다.




2. scss

1) silder의 width 지정하기

  • 슬라이드 영역의 너비를 지정한다.
#slider {
  position: relative;
  width: 30rem;       //this
  margin: 0 auto;
}



2) 좌 우 버튼 디자인 적용하기

  • 페이지 좌, 우 이동 버튼에 대한 너비, 색상 등 기본 스타일 지정이다.
.btn {
  position: absolute;
  width: 3rem;
  height: 100%;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  border: 0;
  background: rgba(250, 250, 250, 0.3);
  transition: 0.2s;
  cursor: pointer;
  font-size: 2rem;
  background: #c3c3c3;
  opacity: 0.3;
  &:hover {
   background: rgba(250, 250, 250, 0.5);  
  }
  &.prev {
    left: 0;
  }
  &.next {
    right: 0;
  }
}



3) wrapper에 overflow 지정하기

  • wrapper는 보여지는 영역으로 넘치는 부분은 숨기는 게 좋다.
  • wrapper에 overflow:hidden을 적용하여 보여지는 영역이외는 숨기도록 한다.
.wrapper {
  position: relative;
  width: 30rem;
  height: 18rem;
  overflow: hidden;     //this!
  padding: 3px 0; 
}



4) items의 너비, X축 위치는 얼마로?

이 슬라이드 이벤트에서 제일 중요한 값이 바로 items의 너비와 X축 위치이다.
items의 너비의 경우 충분한 공간을 주기 위함이며,
x축 위치의 경우 items의 left값을 변경하여 슬라이드를 넘기기 위해서다.

(1) items의 너비는?

  • items의 너비는 (전체 item - 1)% 으로 지정해준다.

.items {
  position: absolute;
  width: 500%;        //this
  top: 0;
  &.active {
   transition: 0.3s;
  }
}

(2) items의 X축 위치는?

  • 이 슬라이드는 아래 사진처럼 좌 우에 이전, 이후 슬라이드가 보여야 한다.
  • 이렇게 하기위해서는 items의 초기 X축 위치를 잘 지정해야한다.


  • 여기서 사용한 방법은 아래와 같다.

  • wrapper는 사용자가 보는 view영역이다.

  • 여기서 중요한 점은 이전, 이후 슬라이드가 보여야하므로 b의 너비를 구해야한다는 것이다.

  • 그림과 같이 (wrapper너비 - items너비) / 2를 하여 b를 구한다.

  • 그 다음 item의 너비에 b를 빼면 left의 초기값을 구할 수 있다.


(3) item에 스타일 지정하기

  • items에서 display: flex을 했으므로 item의 width: 100%으로 한다.
  • 그 이외는 스타일적인 css이므로 가볍게 보면 좋을 거 같다 :)
.item {
  width: 100%;    //this
  pointer-events: none;
  position: relative;
  padding: 0 1rem;
  .content {
    width: 100%;
    height: 18rem;
    border: 1px solid #c8c8c8;
    box-shadow: 0 1rem 2.8rem rgba(0, 0, 0, 0.05);
    border-radius: 1rem;
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 5rem;
    color: #fff;
  }
}




3. js

1) 필요한 요소 가져오기

const wrapper = document.querySelector('.wrapper');
const items = document.querySelector('.items');
const item = document.querySelectorAll('.item');
const next = document.querySelector('.next');
const prev = document.querySelector('.prev');
  • wrapper : 마우스 이벤트를 설정을 위해 필요하다.
  • items: 페이지 로드시 items의 width를 지정할 예정이다. (이벤트에 따라 items의 위치 변경)
  • item: item의 width값을 이용해, 전체적인 레이아웃 수치를 결정한다.
  • next, prev: 버튼 이벤트 설정을 위해 가져온다.



2) 필요한 변수들

let startX = 0;         //mousedown시 위치
let moveX = 0;          //움직인 정도
let currentIdx = 0;     //현재 위치(index)
let positions = [];
  • startX: mousedown시 위치를 저장한다.
  • moveX: mousedownmousemove이벤트를 통해 마우스가 움직인 정도를 저장한다.
  • currentIdx: 현재 보고 있는 슬라이드의 인덱스값을 저장한다. (초기값은 0)
  • positions: 각 슬라이드 페이지를 보기 위해 사용해야할 위치값(X축)을 저장한 배열이다.



3) 페이지 로드시 기본값 설정하기

페이지가 load되거나 resize이벤트가 발생할 때마다 items의 너비, x축 위치를 변경해줘야한다.


(1) items에 애니메이션 클래스(active) 제거

  • items에 active클래스가 적용되면 애니메이션 시간(transition)이 설정된다.
  • 하지만 resize, load될 때 transition이 적용되어 있으면, 재배치하는 과정이 보이게 된다;;
  • 재배치할 때는 transition이 발생하지 않도록 removeactive클래스를 제거해주도록 한다.
function initializeData() {
  const isActive = items.classList.contains('active');
  if (isActive) items.classList.remove('active');
  ...
}

(2) items의 위치와 너비 계산하기

items의 위치를 계산하기 위해서는 wrapper의 너비, item 너비가 필요하다!
한 번 알아보자~

function initializeData() {
  ...
  const wrapper = wrapper.clientWidth;      //[1]
  const itemWidth = item[1].clientWidth;    //[2]
  const b = (wrapper - itemWidth) / 2       //[3]
  const initX = Math.floor((itemWidth - b) * -1);  //[4]
  ...

[1] view영역을 담당하는 wrapper의 너비를 가져온다.
[2] item의 너비이다. (슬라이드 위치 간격으로도 쓰인다)
[3] 그림에서 나타난 bwrapper에서 item의 너비를 빼고, 2를 나눠 구한다.
[4] left의 초기위치값은 item너비에서 방금 구한 b를 빼고 -1을 곱한 값이 된다.


function initializeData() {
  ...
  let pos = [];
  for (let i=0; i<itemCount; i++) {     //[6]
    pos.push(initX - itemWidth * i);    //itemWidth가 위치간격으로 쓰임 
  }
  positions = pos   //[6]
  ...
}

[6] 반복문을 사용해 슬라이드별 위치값을 미리 배열에 저장해준다.


function initializeData() {
  ...
  items.style.width = (itemCount + 1)*100 + '%';   //[7]
  items.style.left = positions[currentIdx] + 'px'; //[8]
}

[7] items의 너비는 (여백용 item을 제외한)itemCount + 1100%곱하기로 구한다.
[8] items의 초기위치값(left)을 지정한다.


window.addEventListener('resize', initializeData);  //[9]
window.addEventListener('load', initializeData);    //[9]

[9] items의 너비, 위치값은 resize, load이벤트가 발생할 때마다 갱신해준다.



4) 좌 우 버튼 이벤트 설정하기

(1) next 버튼 이벤트

next.addEventListener('click', (e) => {
  if (currentIdx === itemCount - 1) return;             //[1]
  const isActive = items.classList.contains('active');  //[2]
  if (!isActive) items.classList.add('active');
  currentIdx = currentIdx + 1;                          //[3]
  items.style.left = positions[currentIdx] + 'px';      //[4]
});

[1] 현재 idxitemCount - 1 과 같으면 더이상 넘길 슬라이드가 없으므로 return 한다.
[2] active클래스가 미적용이면 itemsactive클래스를 추가해준다.
[3] 다음 슬라이드를 넘기는 이벤트이므로 currentIdx에 1을 더해준다.
[4] 변경된 currentIdx를 이용해 itemsleft값을 변경해준다.


(2) prev 버튼 이벤트

prev.addEventListener('click', (e) => {
  if (currentIdx === 0) return;                        //[1]
  const isActive = items.classList.contains('active'); //[2]
  if (!isActive) items.classList.add('active');
  currentIdx = currentIdx - 1;                         //[3]
  items.style.left = positions[currentIdx] + 'px';     //[4]
});

[1] 현재 idx0과 같으면 더이상 넘길 슬라이드가 없으므로 return 한다.
[2] active클래스가 미적용이면 itemsactive클래스를 추가해준다.
[3] 이전 슬라이드로 넘기는 이벤트이므로 currentIdx에 1을 빼준다.
[4] 변경된 currentIdx를 이용해 itemsleft값을 변경해준다.



5) 슬라이드 넘기기 이벤트 지정하기

슬라이드를 넘기기 위해 mousedownmousemovemouseup 이벤트 순서로 지정해줘야한다.
mousedown은 처음 슬라이드를 클릭했을 때,
mousemove는 클릭한 상태에서 마우스를 좌 혹은 우로 이동할 때,
마지막으로 mouseup을 했을 때 슬라이드 위치를 변경해준다.

  • 로직을 글로 작성하면 아래와 같다.
wrapper.onmousedown =(e)=> {
  처음 커서 위치를 저장
  active클래스 여부를 체크하고 없으면 해당 클래스를 items에 적용
  items.addEventListener('mousemove', 마우스 움직이기 이벤트);
  document.onmouseup =(e)=> {
    currentIdx와 items의 위치값을 변경
  }
}

(1) mousedown시에는...

mousedown이벤트에서는 현재 커서값을 startX에 저장하고,
itemsactive클래스가 적용되어있지 않으면 active클래스를 적용하는 역할을 한다.

wrapper.onmousedown =(e)=> {
  const rect = wrapper.getBoundingClientRect();       //[1]
  startX = e.clientX - rect.left;                     //[2]
  const isActive = items.classList.contains('active');
  if (!isActive) items.classList.add('active');       //[3]
  ...
}

[1] getBoundingClientRect를 사용해 화면 기준 wrapper의 위치를 구한다.
[2] e.clientX(화면기준 커서 위치값)에 rect.left를 빼서, wrapper기준 커서 위치값을 구한다.
[3] 만약 itemsactive클래스가 없다면 추가해준다.


(2) mousemove시에는...

mousemove이벤트는 mousedown이벤트가 발생한 상태에서 진행시키도록 한다.
mousemove이벤트에서는 커서가 움직인 정도를 moveX에 저장하고,
움직일 때마다 items의 위치값을 변경해준다.

wrapper.onmousedown =(e)=> {       //[1]
  ...
  items.addEventListener('mousemove', onMouseMove);  //[1]
  ...
}

[1] wrapper에서 mousedown이벤트가 발생한 상태에서, mousemove이벤트를 정의해준다.


function onMouseMove(e) {
  const rect = wrapper.getBoundingClientRect();    //[2]
  moveX = e.clientX - rect.left - startX;          //[3]
  if (currentIdx === 0 && moveX > 0) return;                 //[4]
  else if(currentIdx === itemCount - 1 && moveX < 0) return; //[5]
  const left = positions[currentIdx] + moveX;      //[6]
  items.style.left = left + 'px';                  //[7]
}

[2] getBoundingClientRect를 사용하여 wrapper의 위치값을 구한다.
[3] 현재커서위치 - wrapper위치 - 시작위치를 하여 커서가 움직인 정도를 구한다.
[4] 만약 현재 idx가 0이며, moveX > 0이면 return한다.
[5] 만약 현재 idx가 itemCount - 1이며, moveX < 0이면 return한다.
[6] currentIdx를 사용해 현재 슬라이드 위치값을 구하고, 이 값에 moveX를 더한다.
[7] 사용자가 드래그할 때마다 items의 위치값을 (6번에서 구한)left로 변경해준다.


(3) mouseup시에는...

mouseup이벤트에서는 mousemove이벤트와 기본 mouseup이벤트를 제거해준다.
그 다음 움직인 정도(moveX)에 따라 currentIdx를 변경하고 → 변경한 idx를 사용해 items 위치를 변경해준다.

wrapper.onmousedown =(e)=> {
  document.onmouseup =(e)=> {
    items.removeEventListener('mousemove', onMouseMove);    //[1]
    document.onmouseup = null;                              //[1]
  }
}

[1] mousemove와 mouseup 이벤트를 제거해준다.


wrapper.onmousedown =(e)=> {
  document.onmouseup =(e)=> {
    ...
    if (moveX > -70 && moveX <= 70) {   //[2]
      return items.style.left = positions[currentIdx] + 'px';
    }
    ...
  }
}

[2] 만약 moveX가 -70~70이면 움직이기 전 위치로 이동해준다.


wrapper.onmousedown =(e)=> {
  document.onmouseup =(e)=> {
    if (moveX > 0 && currentIdx > 0) {             //[3]
      currentIdx = currentIdx - 1; 
      items.style.left = positions[currentIdx] + 'px';
    }
    if (moveX < 0 && currentIdx < itemCount - 1){  //[4]
      currentIdx = currentIdx + 1;
      items.style.left = positions[currentIdx] + 'px';
    }
  }
}

[3] moveX>0 이고 idx>0이면 → idx를 1빼주고 위치를 갱신해준다. (이전 슬라이드로 이동)
[4] moveX<0 이고 idx < item-1이면 → idx를 1더하고 위치를 갱신해준다. (다음 슬라이드로 이동)



반응형

댓글