프로젝트/기능 정리

[JavaScript, CSS, jQuery] SpringBoot : 캐러셀, 이미지 터치 슬라이드

민뇌 2023. 7. 18. 10:14

목적

  • 화면 터치를 감지하여 터치 드래그 시 이미지 슬라이드 전환 기능 구현

 

이전 글에서 한 화면에 꽉 차는 한 페이지 자체를 슬라이드 하는 기능을 만들어봤다.

 

페이지 슬라이드 기능에서는 터치를 감지해서 화면을 전환하지 않았지만, 상세 글 보기에 나오는 이미지 캐러셀은 인스타그램 게시물처럼 횡방향 터치 이동 시 전환되도록 만들고 싶었다.

 

아직 데이터가 없어서 이미지 소스나 개수 등을 동적으로 생성해서 처리하진 않은 상태지만, 슬라이드 기능이 메인이므로 정리해보도록 하겠다.

 

완성본은 아래와 같다.

 

결과 gif

 

[HTML]

먼저 html 태그 구조는 아래와 같다.

<div id = "detail_title_wrap">
    <p>야경 본 날</p>
    <a>
        <span>반포 한강공원</span>
    </a>
</div>
<div id="detail_img_slider">
    <div id = "slide_cont">
        <div class = "slide-cont-img" id = "slide_cont_img_1">
            <img src="../image_bundle/detail_sample.jpg">			
        </div>
        <div class = "slide-cont-img" id = "slide_cont_img_2">
            <img src="../image_bundle/three-macarons.png">
        </div>
        <div class = "slide-cont-img" id = "slide_cont_img_3">
            <img src="../image_bundle/gal_sample.jpg">
        </div>
        <div class = "slide-cont-img" id = "slide_cont_img_4">
            <img src="../image_bundle/three-macarons.png">
        </div>
    </div>
</div>
<div id = "detail_radio_wrap">
    <input type = "radio" name = "slide-radios" class = "slide-radio" onclick="return(false);" checked>
    <input type = "radio" name = "slide-radios" class = "slide-radio" onclick="return(false);">
    <input type = "radio" name = "slide-radios" class = "slide-radio" onclick="return(false);">
    <input type = "radio" name = "slide-radios" class = "slide-radio" onclick="return(false);">
</div>

제목영역과 라디오 버튼은 흐름을 모두 보기 위해 가져왔고, 메인인 슬라이드 이미지 영역은 아래 그림과 같다.

 

슬라이드 이미지 영역

 

태그 ID를 표시하기 위해 영역 테두리를 다르게 표시한 것이고 영역의 실제 넓이는 세 <div> 모두 같도록 했다.

 

위와 같은 구조로 태그를 만들어 줬고, CSS 속성을 부여해 아래와 같이 만들었다.

 

캐러셀 구조

 

이렇게 다음 이미지를 옆에 위치시킨 후 감춰두고,

 

왼쪽 방향 드래그 이벤트가 발생하면 왼쪽으로 하나씩, 오른쪽 방향 드래그 이벤트가 발생하면 오른쪽으로  하나씩 이동해서 메인 화면에 한 개의 이미지만 보이도록 하는 것이 최종 결과물이다.

 

[CSS]

우선 가장 밖에서 슬라이드를 감싸고 있는 영역부터 차례로 보자.

 

[detail_img_slider]

#detail_img_slider{
    width : 100vw;
    height : 100vw;
    overflow : hidden;	
}

종횡방향 크기를 정해주고, 넘치는 부분이 발생하면 숨기도록 했다. 

 

이렇게 해줘야 위 캐러셀 구조 이미지처럼 오른쪽으로 길게 이미지를 나열했을 때, 화면 밖에 위치하는 이미지들이 숨겨진다.

 

[slide_cont]

이 부분은 나중에 JS에서 동적으로 클래스를 부여하게 되므로, 아래서 보도록 하겠다.

 

[slide-cont-img]

.slide-cont-img{
    position : absolute;	
    width : 100vw;
    height : 100vw;
}
.slide-cont-img img{
    width : 100%;
    height : 100%;
}

각 이미지를 감싸고 있는 <div> 태그인데, position을 absolute로 주고 넓이와 높이를 지정해줬다.

 

각각의 이미지는 지정된 영역을 모두 사용하도록 100%를 줬다.

 

[각 이미지들]

 #slide_cont_img_1{
    left : 0;
 }
 #slide_cont_img_2{
    left : 100%;
 }
 #slide_cont_img_3{
    left : 200%;
 }
 #slide_cont_img_4{
    left : 300%;
 }

마지막으로 각 이미지들에 left를 100%씩 더해서 오른쪽으로 밀어줬다.

 

 

[JavaScript]

이제 JS에서 터치를 감지하고 이미지를 이동시켜 보자.

 

전체 코드는 아래와 같다.

$(function(){
    let index = 0;
    const slideCont = document.getElementById('slide_cont');
    const radioButton = document.getElementsByName('slide-radios');
    const slideCount = $('#slide_cont').children().length;
    let screenX = screen.width;
    let curTouch;
    let moveTouch;
    let distance;
    let moveX;
    let curLeft = 0;

    $('#detail_img_slider').on('touchstart', function(e){
        //첫 포인트 찍힌 x좌표
        curTouch = e.originalEvent.touches[0].pageX;
    });
    $('#detail_img_slider').on('touchmove', function(e){
        moveTouch = e.originalEvent.touches[0].pageX;
        //움직인 거리, 왼쪽으로 당기면 + / 오른쪽으로 당기면 - 라서 부호 바꿔줌
        distance = -(curTouch-moveTouch);
        moveX = curLeft + distance;
        slideCont.style.transform = "translateX("+(moveX)+"px)";
    });
    $('#detail_img_slider').on('touchend', function(e){
        slideCont.classList.add('slide-transition');
        if(distance < -screenX/4){
            if(index == slideCount - 1) {
                slideCont.style.transform = "translateX(" + (-screenX * index) + "px)";
                return;
            };
            index += 1;
            radioButton[index].checked = true;
        }else if(distance >= screenX/4 && index != 0){
            index -= 1;
            radioButton[index].checked = true;			
        }

        slideCont.style.transform = "translateX(" + (-screenX * index) + "px)";
        moveX = 0;
        distance = 0;
    });

    slideCont.addEventListener("transitionend", function(){
        curLeft = slideCont.getBoundingClientRect().left;
        slideCont.classList.remove('slide-transition');
    });
});

선언한 변수부터 보도록 하겠다.

let index = 0; //현재 이미지 인덱스
const slideCont = document.getElementById('slide_cont'); // 이미지 모두를 감싸고 있는 div
const radioButton = document.getElementsByName('slide-radios'); // 라디오 버튼
const slideCount = $('#slide_cont').children().length; // 이미지 모두를 감싸고 있는 div의 자식 개수, 즉 이미지 개수
let screenX = screen.width; // 현재 보이는 화면 width
let curTouch; // 첫 터치 x좌표 저장 변수
let moveTouch; // 터치 후 드래그 시 드래그된 x 좌표 저장 변수
let distance; // 드래그로 움직인 거리 저장 변수
let moveX; // 현재 이미지 좌표에 거리를 더해준 x좌표 저장 변수
let curLeft = 0; // 현재 이미지의 left 좌표 저장 변수

변수에 대한 설명은 주석으로 표시했다.

 

[touchstart]

$('#detail_img_slider').on('touchstart', function(e){
    //첫 포인트 찍힌 x좌표
    curTouch = e.originalEvent.touches[0].pageX;
});

touchstart는 화면에 터치가 된 순간 한 번 발생한다.

 

슬라이더 영역 전체를 감싸는 <div> 태그에 터치가 발생하면, 발생한 부분의 X좌표를 가져와 curTouch에 저장했다.

 

[touchmove]

$('#detail_img_slider').on('touchmove', function(e){
    moveTouch = e.originalEvent.touches[0].pageX;
    //움직인 거리, 왼쪽으로 당기면 + / 오른쪽으로 당기면 - 라서 부호 바꿔줌
    distance = -(curTouch-moveTouch);
    moveX = curLeft + distance;
    slideCont.style.transform = "translateX("+(moveX)+"px)";
});

touchmove는 터치 후 움직일 때마다 발생한다.

 

움직일 때마다 그 위치에 해당하는 X좌표를 가져와 moveTouch에 담았다.

 

distance는 첫 위치가 저장된  curTouch에서 moveTouch를 빼서 거리를 구해준 값이다.

 

터치 예시 이미지

 

예를 들어, 위 이미지처럼 touchstart가 350px에서 발생했을 때, curTouch는 350이 된다.

 

손을 떼지 않은 상태로 왼쪽으로 드래그를 했다고 가정하면 touchmove 이벤트가 계속 발생하고,

 

발생할 때마다 moveTouch의 값은 350 - 349 - 348 .... - 171 - 170이 된다.

 

그 차이값을 distance로 뒀기에 왼쪽으로 드래그하면 (첫터치 - 점점 작아지는 수)가 되어 양수,

 

오른쪽으로 드래그하면 (첫터치 - 점점 커지는 수)가 되어 음수가 된다. 

 

근데 이미지를 지금 left 100%씩 더해줘서 픽셀로 생각해보면(width 360px 기준) 아래와 같은 위치를 갖는다.

 

캐러셀 픽셀

 

이 상태에서 두 번째 이미지가 화면에 보이려면 두 번째 이미지의 left 값이 0이 되어야 한다.

 

즉, x 방향으로 -360만큼 이동시켜줘야 한다는 것이다.

 

반대로 오른쪽으로 이동시키고자 한다면 +360을 해주면 될 것이다.

 

그런데 아까 설명의 계산대로면 왼쪽으로 이동시키고자 했을 때 curTouch-moveTouch는 양수가 되고, 오른쪽으로 이동시키고자 하면 음수가 되어 부호가 반대다.

 

그래서 -(curTouch-moveTouch)를 통해 부호를 바꿔줬다.

 

마지막으로 moveX는 현재 보이는 화면의 left 값을 가져와서 distance를 더해준 값이다.

 

예를 들어, 현재 이미지 1번이 화면에 보인다고 할 때,

 

터치 시작이 350px, 드래그 끝 시점이 170px이면, distance는 -(curTouch-moveTouch)로 -180이 된다.

 

이미지 1번의 left값이 0이므로 여기에 distance를 더해주면 0 + (-180) = -180 으로 moveX의 값은 -180이 된다.

 

이 값만큼 X방향으로 translate 시키면 이미지가 왼쪽으로 이동하는 것처럼 보이게 되는 것이다.

 

x 축 traslate 예시

 

여기까지 설명을 보면 각 이미지를 translate 시키는 것으로 생각할 수 있는데,

 

slide-cont-img에 left 100%를 주고 position을 absolute로 해둔 것이기 때문에 그 부모 요소인 slide_cont의 X값을 바꿔줘야 한다.

 

translate 설명 예시

 

이렇게하면 드래그를 통해 이미지를 왔다갔다 움직일 수 있게된다.

 

[touchend]

보통 이미지를 넘겨서 볼 수 있는 게시물들을 접해봤다면, 이미지와 이미지 사이에 애매하게 드래그를 해놓는다고 그 상태 그대로 있지 않는다는 것을 알 것이다.

 

어느 정도 이미지를 드래그해서 옮기면 자연스레 다음 이미지로 넘어가고, 별로 드래그 하지 않았다면 원래 이미지로 돌아오는 것이 자연스럽다.

 

이를 마지막에 터치를 끝냈을 때 호출되는 touchend를 통해 제어해주면 된다.

$('#detail_img_slider').on('touchend', function(e){
    slideCont.classList.add('slide-transition');
    if(distance < -screenX/4){
        if(index == slideCount - 1) {
            slideCont.style.transform = "translateX(" + (-screenX * index) + "px)";
            return;
        };
        index += 1;
        radioButton[index].checked = true;
    }else if(distance >= screenX/4 && index != 0){
        index -= 1;
        radioButton[index].checked = true;			
    }

    slideCont.style.transform = "translateX(" + (-screenX * index) + "px)";
    moveX = 0;
    distance = 0;
});

먼저 터치가 종료되면 이미지들을 감싸고 있던 영역인 slide_cont에 스타일을 부여해줬다.

.slide-transition{
   transition: transform 0.5s cubic-bezier(0.87, 0, 0.13, 1);
}

이동이 발생할 때 위 설정대로 움직이도록 해준다(베지어 설정은 여기). 

 

이렇게 터치가 끝나고 할당한 이유는 처음부터 설정해 놓으면 드래그 한 픽셀만큼 사진 이동이 안되고 바로 transition이 적용되어 다음 이미지로 넘어가기 때문이다(즉, 조작감이 없이 클릭하면 넘어가는 느낌).

 

다음은 조건에 따라 index와 체크된 라디오 버튼을 바꿔줬다.

 

첫 번째 조건은 distance가 -screenX/4보다 작은지 확인하고 있는데, 왼쪽으로 드래그 했을 때 distance는 음수가 된다고 했다.

 

screenX는 현재 화면의 가로 길이를 나타내는데, 4로 나눴으니 360px이라고 가정하면 90px이 되겠다.

 

종합적으로 보면, 왼쪽으로 드래그 했을 때 distance는 음수가 되는데 왼쪽으로 드래그 한 값이 -90px보다 더 작으면,

 

즉, 90보다 더 많이 드래그 했으면 이하 코드를 실행하겠다는 의미다.

 

index는 0부터 시작하므로 (이미지 개수 - 1)만큼의 범위를 갖는다.

 

이 인덱스에 현재 화면 가로 길이를 곱해주면 각 이미지의 left 값을 구할 수 있게되고(0×360, 1×360, 2×360, 3×360),

 

 앞서 설명대로 left 값만큼 빼줘야 왼쪽으로 이동하기 때문에 -screenX × index 한 값을 translateX의 인자로 넣어줬다.

 

마지막엔 transition이 완료되고 호출되는 이벤트를 등록했다.

slideCont.addEventListener("transitionend", function(){
    curLeft = slideCont.getBoundingClientRect().left;
    slideCont.classList.remove('slide-transition');
});

이동이 완료 된 후 현재 left 값을 가져와서 curLeft에 담았고, transition 스타일을 제거해줬다.

 

제거하지 않으면 계속 transition이 남아있어서 드래그 한대로 자연스럽게 이미지가 움직이지 않고 넘어가버린다.

 

 

이렇게해서 이미지 터치 슬라이드를 만들어봤다.

 

만들면서 하나씩 다 로그를 찍어보고 값을 확인하며 만든거라 수식이나 코드에 불필요한 부분 또는 잘못된 부분이 있을 수 있다.

 

참고할 부분만 하도록 하자.. 

 

이또한 시간을 너무 많이 소비했기에 이정도로 정리하고 마치도록 하겠다.