«   2021/10   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            
Tags
more
Archives
Today
0
Total
243
관리 메뉴

개발블로그

도착지는 하나지만, 가는 길은 하나가 아니다. 본문

도착지는 하나지만, 가는 길은 하나가 아니다.

학교옆메추리 2020. 8. 15. 15:51

도착지

이번 우아한테크캠프 3기 네번째 프로젝트의 요구사항 중 하나인 Carousel 구현 및 카테고리별 상품 리스트 섹션을 개발하면서 알게 된 점을 공유하려고 합니다.

Carousel  Requirements

먼저 Craousel의 요구사항을 설명드리겠습니다.

Crousel Requirement

웹 페이지에서 흔하게 볼 수 있는 무한 캐러셀 기능입니다. 지정한 term마다 다음 메뉴로 슬라이드되어 보여지며, 마지막 페이지 이후에는 다시 첫 번째 페이지가 등장하여 메뉴가 무한히 있는 것 처럼 보입니다.

 

추가로 사용자의 액션을 고려해야 합니다. 사용자는 스와이핑 액션을 통하여 캐러셀의 슬라이드를 왼쪽, 오른쪽으로 조정할 수 있습니다.

 

도착지로 가는 길 - 1

Carousel을 구현하는 방법에는 정말 여러가지가 있을 텐데요. 처음의 저는 이전의 Drag&Drop을 구현했던 경험을 살려 구현을 시작하였습니다.

 

첫 번째 고개

첫 시작으로 우리의 캐러셀이 일정 시간마다 다음 슬라이드로 이동할 수 있도록 만드는 부분입니다. 

보이는 것은 전부가 아니다.

width: 100%; overflow: hidden; 이 적용된 박스를 두고 내부의 긴 리스트를 이리저리 움직이는 방법을 선택했습니다. 

 

setInterval 함수와 transform 속성을 이용하여 일정 시간마다 캐러셀을 슬라이드 크기만큼 왼쪽으로 이동시켜주면 사용자에게는 이미지가 슬라이딩 되는 것처럼 느껴질 것입니다!

 

두 번째 고개

자 이제 무한한 길이의 슬라이드처럼 보이게 하는 부분입니다! 여러가지 방법을 떠올려 보았습니다.

 

1. 오른쪽으로 슬라이딩되면 제일 왼쪽 슬라이드를 떼어 맨 오른쪽에 붙이고 왼쪽은 그 반대로 하는 방식

=> 왠지 tranform으로 움직이는 제 방식에서 엘리먼트가 제거되고 붙여지면 그 위치가 순간적으로 변하는 것까지 처리해 주어야 할 것 같습니다. 물론 아닐 수도 있지만요!

2. 들어온 이미지 배열이 1 2 3 4 5 일 때, 5 1 2 3 4 5 1 배열로 변형합니다. 뒷 부분에서 슬라이딩되어 5 -> 1로 도착하는 순간 앞의 1로 위치를 바꿔치기합니다. 반대로 앞부분에서 반대로 슬라이딩되어 5 <- 1로 도착하는 순간 뒤쪽의 5로 바꿔치기 합니다.

=> 훨씬 더 구현하기  좋은 방법이라고 생각했습니다! 일단 DOM을 조작하지 않기 때문에 성능상의 이점도 챙길 수 있겠군요!

 

결국 이번 갈림길에서 저는 2번이라는 길을 선택합니다.

 

두 번째 고개 - 문제해결

슬라이딩 애니메이션을 위하여 작성된 css 코드입니다.

.slider-container {
  transition: transform 400ms ease;
}

interval 마다 슬라이딩에서 굉장히 부드러운 이동을 볼 수 있지만, 위에서 말씀드린 위치 바꿔치기 에는 적합하지 않습니다. 사용자의 눈에 보이지 않도록 교체되어야 하는데, transition 때문에 맨 끝에서 반대편 끝으로 빠르게 슬라이딩 되며 이동합니다 😭😭😭

 

결국 코드에서 위치 바꿔치기 가 수행될 때에는 transitioin 속성을 제거했다가, 다시 붙이는 방법을 사용함으로써 해결할 수 있었습니다.

 

세 번째 고개

아직 사용자와의 인터렉션을 제공하는 부분이 남았습니다. 그저 무한히 움직이는 캐러셀을 원하셨다면 이미 끝났습니다! 나가셔도 좋습니다.

 

사용자가 슬라이딩 할 수 있다. 라는 기능을 구현하고자 합니다.

1. 사용자가 슬라이드를 움직일 수 있도록 해야 합니다. ( Drag )

2. 사용자가 마우스(손가락)에서 손을 떼는 순간의 위치에서 끝나는 것이 아니라, 그 시점에 화면에서 가장 많은 비율을 차지하고 있던 슬라이드만이 화면에 보일 수 있도록 위치를 조정해 주어야 합니다. ( Drop ) 

const transitionEndHandler = (e) => {
  // 다시 자동 슬라이드 기능을 활성화합니다.
  setInterval(startCarouselInterval, INTERVAL_TIME)
    
  carouselElm.removeEventListener('transitionend', transitionEndHandler)
}

const pointerUpHandler = (e) => {
  // TODO: 슬라이드의 결과적으로 이동할 올바른 위치를 계산

  // TODO에서 계산된 위치로의 transition 이동이 끝나면 실행될 함수.
  carouselElm.addEventListener('transitionend', transitionEndHandler)

  // pointerup 이벤트가 발생하여 인터렉션이 끝나면 move, up이벤트는 더이상 필요가 없습니다.
  carouselElm.removeEventListener('pointermove', pointerMoveHandler)
  carouselElm.addEventListener('pointerup', pointerUpHandler)
}

const pointerMoveHandler = (e) => {
  // TODO: 사용자의 움직임을 따라 슬라이드가 움직이도록 합니다.
}

const pointerDownHandler = (e) => {
  // 인터렉션 중에는 자동 슬라이드 기능이 꺼져 있어야 합니다.
  clearInterval(sliderInterval)
	
  // pointerdown 이벤트가 발생했을 때에만 move, up이벤트가 필요합니다.
  carouselElm.addEventListener('pointermove', pointerMoveHandler)
  carouselElm.addEventListener('pointerup', pointerUpHandler)
}

carouselElm.addEventListener('pointerdown', pointerDownHandler)

pointerdown, pointermove, pointerup이벤트를 사용하여 구현해 보려고 했습니다. 모바일 웹 뷰를 구현하는 프로젝트이므로 mouse 이벤트와 touch 이벤트를 하나의 이벤트로 잡을 수 있는 pointer계열 이벤트를 사용합니다.

 

MDN pointer events: https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events

 

pointerdown 이벤트가 발생하면 clearInterval 함수를 통해 누르고 있는 동안에는 자동 슬라이딩이 되지 않도록 해야 합니다. 그 후pointermove 이벤트마다 pointer의 x축 움직임을 파악하여 translate 속성을 조정합니다. 또한 pointerup 이벤트가 발생하면 계산을 통하여 적절한 슬라이드가 보여지도록 합니다. 또한 인터렉션이 종료되고 슬라이드가 제 자리를 찾으면 다시 자동으로 슬라이딩 되어야 하기 때문에 transitionend 이벤트를 활용하여 다시 인터벌을 실행시킬 수 있도록 합니다.

 

세 번째 고개 - 문제해결

여기서 같은 문제가 또 발생합니다. transition 때문인데요, 사용자가 슬라이드를 누른채로 이동할 때에는 즉각적인 움직임을 기대하지만 우리의 캐러셀은 그렇지 못합니다. 같은 방법으로 pointerdown 에서는 transition 속성을 제거하고, pointerup 에서는  다시 적용함으로써 문제를 해결 할 수 있습니다.

const transitionEndHandler = (e) => {
  // 다시 자동 슬라이드 기능을 활성화합니다.
  setInterval(startCarouselInterval, INTERVAL_TIME)
    
  carouselElm.removeEventListener('transitionend', transitionEndHandler)
}

const pointerUpHandler = (e) => {
  // TODO: 슬라이드의 결과적으로 이동할 올바른 위치를 계산
  
  // 추가된 부분: 인터렉션이 종료되었으므로 transition이 다시 필요합니다.
  carouselElm.style.transition: 'transform 400ms ease';

  // TODO에서 계산된 위치로의 transition 이동이 끝나면 실행될 함수.
  carouselElm.addEventListener('transitionend', transitionEndHandler)

  // pointerup 이벤트가 발생하여 인터렉션이 끝나면 move, up이벤트는 더이상 필요가 없습니다.
  carouselElm.removeEventListener('pointermove', pointerMoveHandler)
  carouselElm.addEventListener('pointerup', pointerUpHandler)
}

const pointerMoveHandler = (e) => {
  // TODO: 사용자의 움직임을 따라 슬라이드가 움직이도록 합니다.
}

const pointerDownHandler = (e) => {
  // 인터렉션 중에는 자동 슬라이드 기능이 꺼져 있어야 합니다.
  clearInterval(sliderInterval)
    
  // 추가된 부분: 인터렉션 중에는 슬라이드의 transition이 있으면 안됩니다.
  carouselElm.style.transition: 'none';
	
  // pointerdown 이벤트가 발생했을 때에만 move, up이벤트가 필요합니다.
  carouselElm.addEventListener('pointermove', pointerMoveHandler)
  carouselElm.addEventListener('pointerup', pointerUpHandler)
}

carouselElm.addEventListener('pointerdown', pointerDownHandler)

 

어찌저찌 구현을 했다고 가정하고, carousel의 기본이 얼추 완성되었습니다. GPU의 도움을 받아 더욱 smooth한 애니메이션을 보여주기 위해 will-change라는 속성을 추가할 수 있습니다.

 

추가적으로 넘을 고개들

UX 향상을 위해 추가적인 구현이 필요합니다. 저는 UX를 공부해본 적이 없으며, 제 경험과 실제로 사람들이 많이 사용하는 웹 페이지들을 참고하면서 생각한 것들입니다. 이 이야기는 어디까지나 제 생각이며, 값의 차이가 있을 수 있고, 직접 구현해 보지 않고 말씀드리는 것이기 때문에 오해의 소지나 틀린점이 있을 수 있습니다. 그냥 이런 것들도 고려할 문제구나... 라는 생각만으로 충분한 것 같습니다.

 

첫 번째로 움직임의 각도를 계산할 필요가 있습니다. 사용자는 페이지를 위해 위, 아래로 스크롤링 하기 위해 제스쳐를 활용합니다. 이 제스쳐는 PC에서 처럼 x축, y축으로 발생하는 것이 아니라 대각선으로 발생할 것입니다. (자신이 스크롤을 어떻게 하고 있는지 생각해보세요.)

사용자가 캐러셀에 손가락을 누르고 아래쪽 대각선으로 움직입니다. 현재 우리의 캐러셀은 이벤트를 잡아서 대각선 중 x축만의 움직임을 잡아 움직일 것입니다. (아주 조금 움직일 것입니다.) 이는 UX를 해칠 수 있기 때문에 우리는 누른 시작점과 초기 move위치의 각도를 계산하여 x축에서 위아래 60도까지는 우리의 캐러셀을 이동하는 것으로, 그 범위를 넘으면 페이지 스크롤을 이동하는 것으로 판단하여 move이벤트를 제거할 수 있겠습니다.

 

두 번째로, 사용자의 화면에서 우리의 캐러셀이 벗어났을 때에는 자동 슬라이드 이벤트를 멈춰야 합니다. 스크롤을 내리다가 봤던 그 슬라이드가 순간 떠올라 다시 위로 스크롤 했다고 가정합시다. 슬라이드가 계속 이동중이고 8개의 슬라이드가 있다고 가정하면, 사용자는 해당 슬라이드를 찾기 위해 시간을 허비해야 합니다. 이를 막기 위함입니다. scroll 이벤트를 활용하는 방법도 있지만, 브라우저에서 제공하는 intersectionobserver 를 활용하여 캐러셀이 화면에 나타났는지, 아닌지를 구별 할 수 있습니다. clearInterval/setInterval 을 활용하여 구현면 될 것 같습니다.

 

MDN intersectionobserver: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

 

세 번째로, 링크이동를 위한 클릭과 슬라이드 이동을 위한 클릭을 구별해야 할 필요가 있습니다. 우리의 손가락은 항상 정확히 한 군데를 터치하지 않습니다. 살짝의 이동(몇 px정도)이 포함될 수 있다는 것입니다. 그렇기 때문에 아주 적은 범위의 이동은 링크 이동을 위한 클릭이라고 판단하는 코드가 있어야 할 것 같습니다.

 

 

마치며...

이야기가 너무 길어졌습니다. 다음 글로 넘어가 2번째 구현 방법을 설명드리며 장/단점 비교와 함께 느낀점까지 공유하겠습니다.

0 Comments
댓글쓰기 폼