ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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이 남아있어서 드래그 한대로 자연스럽게 이미지가 움직이지 않고 넘어가버린다.

     

     

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

     

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

     

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

     

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

Designed by Tistory.