[JavaScript, CSS, jQuery] SpringBoot : 캐러셀, 이미지 터치 슬라이드
목적
- 화면 터치를 감지하여 터치 드래그 시 이미지 슬라이드 전환 기능 구현
이전 글에서 한 화면에 꽉 차는 한 페이지 자체를 슬라이드 하는 기능을 만들어봤다.
페이지 슬라이드 기능에서는 터치를 감지해서 화면을 전환하지 않았지만, 상세 글 보기에 나오는 이미지 캐러셀은 인스타그램 게시물처럼 횡방향 터치 이동 시 전환되도록 만들고 싶었다.
아직 데이터가 없어서 이미지 소스나 개수 등을 동적으로 생성해서 처리하진 않은 상태지만, 슬라이드 기능이 메인이므로 정리해보도록 하겠다.
완성본은 아래와 같다.
[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 시키면 이미지가 왼쪽으로 이동하는 것처럼 보이게 되는 것이다.
여기까지 설명을 보면 각 이미지를 translate 시키는 것으로 생각할 수 있는데,
slide-cont-img에 left 100%를 주고 position을 absolute로 해둔 것이기 때문에 그 부모 요소인 slide_cont의 X값을 바꿔줘야 한다.
이렇게하면 드래그를 통해 이미지를 왔다갔다 움직일 수 있게된다.
[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이 남아있어서 드래그 한대로 자연스럽게 이미지가 움직이지 않고 넘어가버린다.
이렇게해서 이미지 터치 슬라이드를 만들어봤다.
만들면서 하나씩 다 로그를 찍어보고 값을 확인하며 만든거라 수식이나 코드에 불필요한 부분 또는 잘못된 부분이 있을 수 있다.
참고할 부분만 하도록 하자..
이또한 시간을 너무 많이 소비했기에 이정도로 정리하고 마치도록 하겠다.