티스토리 뷰

Tip and Error/Javascript

Touch Slider (Image)

geonwoopaeng@gmail.com 2023. 6. 5. 16:18

Touch Slider (Image)

Touch Slider Image 기능을 구현하면서 Image 특정 부분에 넣어 Image Touch Slider 구현하기 등 여러 문제를 겪어 다음에는 같은 실수를 반복하지 않고 다른 분들도 효율적인 구현을 할 수 있도록 작성한 글입니다. ( Svelte를 기반으로 제작된 글입니다. )

slider2

 

Tech

  • Svelte
  • JavaScript / Html / CSS

 

과정

Touch slider은 Image를 쭉 나열해 두고 overflow: hidden으로 이미지를 숨기고 mouse eventtransform: translateX()를 이용하여 image 위치 고정(image index * -보여지는 너비(width)) 및 dragging 하는 모습을 보여줍니다.

1. Image 고정 및 Html/CSS 구조

Html / CSS를 간략화 해서 구조를 파악하겠습니다.

[ CSS ]

.div_slider_preview_container_cls {
  width: 350px;
  height: 200px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  text-align: center;
}
.div_image_container_cls {
  width: 100%;
  height: 100%;
  display: inline-flex;
  position: relative;
  transform: translateX(0);
  transition: transform 0.3s ease-out;
  cursor: grab;
}
.img_preview_cls {
  width: 100%;
  object-fit: contain;
  flex-shrink: 0;
}

[ Html ]

<div class="div_slider_preview_container_cls">
  <div class="div_image_container_cls">
    {#each ref_image_arr as image_src, index}
    <img src="{image_src}" class="img_preview_cls" />
    {/each}
  </div>
</div>

.div_image_container_cls 내부에서 .img_preview_cls를 쭉 나열하고 .div_slider_preview_container_clsoverflow: hidden을 넣어 특정 부분에서만 그림이 보이게 하는 식으로 구조를 짜게 됩니다.

그림을 통해 더 자세히 알아보도록 하겠습니다.

Image1: .img_preview_cls

.img_preview_cls만 따로 때서 보면 다음과 같이 그리며 object-fit: contain을 통해 image를 틀 안에 가둬 둘 수 있습니다.

slider_image2

Image2: 전체 구조

전체 구조를 보면 위의 .img_preview_cls을 여러 개 붙어 있으며 보이는 부분과 안 보이는 부분을 색으로 구분하는 것을 볼 수 있습니다.

초록색: .div_slider_preview_container_cls으로 그림이 보이는 부분
빨간색: .div_slider_preview_container_clsoverflow: hidden부분으로 가려진 부분

slider_image

이렇게 만들어 뒤에 나오는 mouse event, transform: translateX()를 이용하여 Image를 움직입니다.

 

2. Slider Event (mouse event, transform: translateX())

Htmlmouse event를 추가하면 다음과 같습니다.
(Svelte에 추가해서 on:*과 같은 형태로 추가됩니다.)

[ Html ]

<div class="div_slider_preview_container_cls">
  <div bind:this="{ref_this_preview}" class="div_image_container_cls">
    {#each ref_image_arr as image_src, index} <img src={image_src} alt=""
    on:dragstart|preventDefault on:mousedown={event =>
    preview_mouse_start(event, index)} on:mouseup={preview_mouse_end}
    on:mouseleave={preview_mouse_end} on:mousemove={preview_mouse_move}
    class="img_preview_cls" /> {/each}
  </div>
</div>

내부 함수에 대한 설명에 앞서 mouse event에 대한 설명을 간단히 하고 가겠습니다.

mouse event는 말 그대로 mouse를 가지고 하는 이벤트를 말합니다. (버블링 주의)

> mousedown: 마우스 왼쪽 버튼을 누를 때 발생
> mouseup: 마우스 버튼을 누르고 있다가 뗄 때 발생
> mouseleave: 마우스 커서가 해당 element를 나올 때 발생 (이벤트 버블링 X)
> mousemove: 마우스 커서가 해당 element에서 움직일 때 발생 (UI 느리게 한다)

 

- 사용 변수

mouse event에서 사용하는 함수에서 사용하는 변수에 대해 알고 가겠습니다.

let ref_image_arr = ['1.jpg', '2.jpg', '3.jpg']; // image 저장 배열 저장 변수
let ref_this_preview; // Svelte의 bind:this에 연결된 변수
let preview_start_position = 0; //왼쪽 상단을 기준으로 마우스 위치의 x 좌표 값 저장 변수 (mouse 시작 x 좌표)
let preview_current_index = 0; // 현재 image의 index값 저장 변수
let preview_animation_id = 0; // requestAnimationFrame() 사용 id 저장 변수
let preview_is_dragging = false; // drag를 실행했는지 check 변수

let preview_current_translate = 0; // 현재 translateX 위치
let preview_prev_translate = 0; // 이전 translateX 위치

* 추가 (requestAnimationFrame())
: 애니메이션을 수행하고 싶다고 브라우저에 알리고 다음 다시 그리기 전에 브라우저가 지정된 함수를 호출하여 애니메이션을 업데이트하도록 요청하는 함수 (부드러운 애니메이션을 위해 사용하는 함수)

[ 사용 이유 ]

  • 브라우저에서 최적화할 수 있으므로 애니메이션이 더 부드러워집니다.
  • 비활성 탭의 애니메이션이 중지되어 CPU가 식을 수 있습니다.
  • 배터리 친화적입니다.

 

- Mouse event 내부 함수

[ preview_mouse_start ]
mouse event가 시작될 때 실행되는 함수로 현재 위치, drag 체크, animation을 실행시키는 함수입니다.

function preview_mouse_start(event, index) {
  preview_current_index = index;
  preview_start_position = event.pageX;
  preview_is_dragging = true;
  preview_animation_id = requestAnimationFrame(animation);
}

[ preview_mouse_end ]
mouse event 가 마무리될 때 위치 imageindex를 파악하고 이를 보여주는 부분인 .div_slide_preview_container_cls의 마이너스 너비(width: 350)와 곱하여 image가 딱 보이게 고정하는 역할을 하는 함수입니다.

function preview_mouse_end() {
  preview_is_dragging = false;
  cancelAnimationFrame(preview_animation_id);

  const move_by = preview_current_translate - preview_prev_translate; // translateX 움직인 거리
  if (move_by < -100 && preview_current_index < ref_image_arr.length - 1)
    preview_current_index += 1;
  if (move_by > 100 && preview_current_index > 0) preview_current_index -= 1;

  preview_current_translate = preview_current_index * -350;
  preview_prev_translate = preview_current_translate;
  ref_this_preview.style.transform = `translateX(${preview_current_translate}px)`;
}

[ preview_mouse_move ]
Slider의 image가 움직이는 모션을 보여주기 위한 함수입니다.

function preview_mouse_move(event) {
  if (!preview_is_dragging) return;

  const current_position = event.pageX;
  preview_current_translate =
    preview_prev_translate + current_position - preview_start_position;
}

[ animation ]
애니메이션을 나타내는 함수입니다.

function animation() {
  ref_this_preview.style.transform = `translateX(${preview_current_translate}px)`;
  preview_is_dragging && requestAnimationFrame(animation);
}

 

전체 코드

<style>
.div_slider_preview_container_cls {
  width: 350px;
  height: 200px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  text-align: center;
}
.div_image_container_cls {
  width: 100%;
  height: 100%;
  display: inline-flex;
  position: relative;
  transform: translateX(0);
  transition: transform 0.3s ease-out;
  cursor: grab;
}
.img_preview_cls {
  width: 100%;
  object-fit: contain;
  flex-shrink: 0;
}
</style>

<div class="div_slider_preview_container_cls">
    <div bind:this={ref_this_preview} class="div_image_container_cls">
        {#each ref_image_arr as image_src, index}
            <img src={image_src}
                 alt=""
                 on:dragstart|preventDefault
                 on:mousedown={event => preview_mouse_start(event, index)}
                 on:mouseup={preview_mouse_end}
                 on:mouseleave={preview_mouse_end}
                 on:mousemove={preview_mouse_move}
                 class="img_preview_cls"
             />
         {/each}
    </div>
</div>

<script>

let ref_this_preview; // Svelte의 bind:this에 연결된 변수
let ref_image_arr = [
    "1.jpg",
    "2.jpg",
    "3.jpg",
]; // image 저장 배열 저장 변수

let preview_start_position = 0; //왼쪽 상단을 기준으로 마우스 위치의 x 좌표 값 저장 변수 (mouse 시작 x 좌표)
let preview_current_index = 0; // 현재 image의 index값 저장 변수
let preview_animation_id = 0; // requestAnimationFrame() 사용 id 저장 변수
let preview_is_dragging = false; // drag를 실행했는지 check 변수

let preview_current_translate = 0; // 현재 translateX 위치
let preview_prev_translate = 0; // 이전 translateX 위치

function preview_mouse_start(event, index) {
    preview_current_index = index;
    preview_start_position = event.pageX;
    preview_is_dragging = true;
    preview_animation_id = requestAnimationFrame(animation);
}

function preview_mouse_end() {
    preview_is_dragging = false;
    cancelAnimationFrame(preview_animation_id);

    const move_by = preview_current_translate - preview_prev_translate; // translateX 움직인 거리

    //image index 파악하기
    if (move_by < -100 && preview_current_index < ref_image_arr.length - 1) preview_current_index += 1;
    if (move_by > 100 && preview_current_index > 0) preview_current_index -= 1;

    // 보여줘야 할 image 계산
    preview_current_translate = preview_current_index * -350;
    preview_prev_translate = preview_current_translate;
    ref_this_preview.style.transform = `translateX(${preview_current_translate}px)`;
}

function preview_mouse_move(event) {
    if (!preview_is_dragging) return;

    const current_position = event.pageX;
    preview_current_translate = preview_prev_translate + current_position - preview_start_position;
}

function animation() {
    ref_this_preview.style.transform = `translateX(${preview_current_translate}px)`;
    preview_is_dragging && requestAnimationFrame(animation);
}
</script>

 

마치며

Drag and Drop과 Touch Slider을 구현하게 되었는데 Drag and Drop은 splice를 적절하게 사용하면 쉽게 구현이 가능한 부분이 있었습니다.
그러나 Touch Slider는 구조부터 고민하면서 움직이는 부분과 고정되는 부분을 잘 파악하는 데 어려움을 겪어 재미있게 글을 작성할 수 있었고 다시 한번 정리가 되는 기회였습니다. 그러나 requestAnimationFrame과 같은 것들을 내장 함수 & 변수들을 잘 사용하지 못한 것, CSS를 깨끗하게 정리하여 개발하지 못한 점, 공식 문서에 대한 약간의 꺼리낌등 아쉬운 점이 있어 더욱 개선을 해 나가야 할 것 같습니다.

Pagination button을 추가하고 싶으면 image 개수만큼 button을 만들고 mousedown, mouseup을 추가해주고 position: absolute를 잘 사용하면 만드실 수 있습니다.

긴 글 읽어주셔서 감사합니다. :)

 

Ref

Touch Slider • REPL • Svelte
https://www.w3schools.com/jsref/obj_mouseevent.asp
https://javascript.info/mouse-events-basics
https://hianna.tistory.com/492
https://webisfree.com/2020-03-19/[%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8]-requestanimationframe()%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%B0%8F-%EC%98%88%EC%A0%9C

반응형
공지사항
최근에 올라온 글