1. 분석
이전 슬라이드 드래그 구현 포스팅에 이어서 무한 슬라이드 기능을 구현하려 한다.
외부 라이브러리를 사용하지 않고 순수 자바스크립트만을 이용해 무한 슬라이드를 구현한다.
무한 슬라이드를 구현하기 위해선 처음과 마지막 슬라이드에서 이전, 이후로 이동했을 때 자연스럽게 슬라이드가 진행돼야 한다. 예를 들자면 1, 2, 3, 4, 5 슬라이드가 존재할 때 5번에서 다음 버튼을 눌렀을 때 1번 슬라이드를 보여주는 것이다.
따라서 시각적으로 자연스럽게 무한 슬라이드를 구현하기 위해 처음과 마지막 슬라이드를 각각 복제 해서 슬라이드를 보여줄 것이다.
먼저 엘리먼트를 복제한다. 복제하는 단계에서 엘리먼트 태그, 클래스, 앨리먼트 안의 내용을 모두 복사해준다.
// 무한 슬라이드를 위해 start, end 슬라이드 복사하기
const startSlide = slideItems[0];
const endSlide = slideItems[slideItems.length - 1];
// 엘리먼트 생성
const startElem = document.createElement(startSlide.tagName);
const endElem = document.createElement(endSlide.tagName);
// 엘리먼트에 클래스 적용, 내용 복사 동일하게 하기
endSlide.classList.forEach((c) => endElem.classList.add(c));
endElem.innerHTML = endSlide.innerHTML;
startSlide.classList.forEach((c) => startElem.classList.add(c));
startElem.innerHTML = startSlide.innerHTML;
// 각 복제한 엘리먼트를 각 위치에 추가하기
slideItems[0].before(endElem);
slideItems[slideItems.length - 1].after(startElem);
// 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
slideItems = document.querySelectorAll(".slide_item");
let offset = slideWidth * currSlide;
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
엘리먼트를 복제했다면 슬라이드를 이동하는 함수에서 처음과 마지막 슬라이드에서 무한 슬라이드 이동을 위한 부분을 추가해준다.
function nextMove() {
currSlide++;
if (currSlide <= maxSlide) {
// ... 정상적인 흐름으로 슬라이드를 넘기는 경우 (생략)
} else {
// 마지막 슬라이드에서 첫 슬라이드로 넘어가는 경우
// 무한 슬라이드 기능 - currSlide 값만 변경해줘도 되지만 시각적으로 자연스럽게 하기 위해 아래 코드 작성
currSlide = 0;
let offset = slideWidth * currSlide;
slideItems.forEach((i) => {
i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
});
currSlide++;
offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
// setTimeout을 사용하는 이유는 비동기 처리를 이용해 transition이 제대로 적용되게 하기 위함
setTimeout(() => {
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `transition: ${0.15}s; left: ${-offset}px`);
});
}, 0);
// // 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
}
}
추가적으로 복제로 인해 늘어난 슬라이드로 페이지네이션을 클릭했을 때 적용될 오프셋값이 변경됐으므로 수정해준다.
// 각 페이지네이션 클릭 시 해당 슬라이드로 이동하기
for (let i = 0; i < maxSlide; i++) {
// 각 페이지네이션마다 클릭 이벤트 추가하기
paginationItems[i].addEventListener("click", () => {
// 클릭한 페이지네이션에 따라 현재 슬라이드 변경해주기(currSlide는 시작 위치가 1이기 때문에 + 1)
currSlide = i + 1;
// 슬라이드를 이동시키기 위한 offset 계산
const offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
// 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
});
}
2. 구현 결과 미리보기
3. 구현
3.1. HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla js infinite slide</title>
<link rel="stylesheet" href="./slide.css" />
</head>
<body>
<div class="slide slide_wrap">
<div class="slide_item item1">1</div>
<div class="slide_item item2">2</div>
<div class="slide_item item3">3</div>
<div class="slide_item item4">4</div>
<div class="slide_item item5">5</div>
<div class="slide_prev_button slide_button">◀</div>
<div class="slide_next_button slide_button">▶</div>
<ul class="slide_pagination"></ul>
</div>
<script src="./slide.js"></script>
</body>
</html>
3.2. CSS
.slide {
/* layout */
display: flex;
flex-wrap: nowrap;
/* 컨테이너의 내용물이 컨테이너 크기(width, height)를 넘어설 때 보이지 않도록 하기 위해 hidden을 준다. */
overflow: hidden;
/* position */
/* slide_button의 position absolute가 컨테이너 안쪽에서 top, left, right offset이 적용될 수 있도록 relative를 준다. (기본값이 static인데, static인 경우 그 상위 컨테이너로 나가면서 현재 코드에선 html을 기준으로 offset을 적용시키기 때문) */
position: relative;
/* size */
width: 100%;
/* slide drag를 위해 DOM요소가 드래그로 선택되는것을 방지 */
user-select: none;
}
.slide_item {
/* layout */
display: flex;
align-items: center;
justify-content: center;
/* position - 버튼 클릭시 left offset값을 적용시키기 위해 */
position: relative;
left: 0px;
/* size */
width: 100%;
height: 300px;
/* flex item의 flex-shrink는 기본값이 1이므로 컨테이너 크기에 맞게 줄어드는데, 슬라이드를 구현할 것이므로 줄어들지 않도록 0을 준다. */
flex-shrink: 0;
/* transition */
transition: left 0.15s;
}
.slide_button {
/* layout */
display: flex;
justify-content: center;
align-items: center;
/* position */
position: absolute;
/* 버튼이 중앙에 위치하게 하기위해 계산 */
top: calc(50% - 16px);
/* size */
width: 32px;
height: 32px;
/* style */
border-radius: 100%;
background-color: #cccc;
cursor: pointer;
}
.slide_prev_button {
left: 10px;
}
.slide_next_button {
right: 10px;
}
/* 각 슬라이드가 변경되는 것을 시각적으로 확인하기 쉽도록 각 슬라이드별 색상 적용 */
.slide_item.item1 {
background-color: darkgoldenrod;
}
.slide_item.item2 {
background-color: aqua;
}
.slide_item.item3 {
background-color: blueviolet;
}
.slide_item.item4 {
background-color: burlywood;
}
.slide_item.item5 {
background-color: cornflowerblue;
}
/* 페이지네이션 스타일 */
ul,
li {
list-style-type: none;
padding: 0;
margin: 0;
}
.slide_pagination {
/* layout */
display: flex;
gap: 5px;
/* position */
position: absolute;
bottom: 0;
/* left:50%, translateX(-50%)를 하면 가로 가운데로 위치시킬 수 있다. */
left: 50%;
transform: translateX(-50%);
}
.slide_pagination > li {
/* 현재 슬라이드가 아닌 것은 투명도 부여 */
color: #7fb5ff88;
cursor: pointer;
font-size: 25px;
}
.slide_pagination > li.active {
/* 현재 슬라이드 색상은 투명도 없이 */
color: #7fb5ff;
}
.slide_item_duplicate {
display: flex;
align-items: center;
justify-content: center;
position: relative;
left: 0px;
width: 100%;
height: 300px;
flex-shrink: 0;
transition: left 0.15s;
}
3.3. JS
// 슬라이크 전체 크기(width 구하기)
const slide = document.querySelector(".slide");
let slideWidth = slide.clientWidth;
// 버튼 엘리먼트 선택하기
const prevBtn = document.querySelector(".slide_prev_button");
const nextBtn = document.querySelector(".slide_next_button");
// 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
let slideItems = document.querySelectorAll(".slide_item");
// 현재 슬라이드 위치가 슬라이드 개수를 넘기지 않게 하기 위한 변수
const maxSlide = slideItems.length;
// 버튼 클릭할 때 마다 현재 슬라이드가 어디인지 알려주기 위한 변수
let currSlide = 1;
// 페이지네이션 생성
const pagination = document.querySelector(".slide_pagination");
for (let i = 0; i < maxSlide; i++) {
if (i === 0) pagination.innerHTML += `<li class="active">•</li>`;
else pagination.innerHTML += `<li>•</li>`;
}
const paginationItems = document.querySelectorAll(".slide_pagination > li");
// 무한 슬라이드를 위해 start, end 슬라이드 복사하기
const startSlide = slideItems[0];
const endSlide = slideItems[slideItems.length - 1];
// 엘리먼트 생성
const startElem = document.createElement(startSlide.tagName);
const endElem = document.createElement(endSlide.tagName);
// 엘리먼트에 클래스 적용 동일하게 하기
endSlide.classList.forEach((c) => endElem.classList.add(c));
endElem.innerHTML = endSlide.innerHTML;
startSlide.classList.forEach((c) => startElem.classList.add(c));
startElem.innerHTML = startSlide.innerHTML;
// 각 복제한 엘리먼트를 각 위치에 추가하기
slideItems[0].before(endElem);
slideItems[slideItems.length - 1].after(startElem);
// 슬라이드 전체를 선택해 값을 변경해주기 위해 슬라이드 전체 선택하기
slideItems = document.querySelectorAll(".slide_item");
let offset = slideWidth * currSlide;
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
function nextMove() {
currSlide++;
// 마지막 슬라이드 이상으로 넘어가지 않게 하기 위해서
if (currSlide <= maxSlide) {
// 슬라이드를 이동시키기 위한 offset 계산
const offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
// 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
} else {
// 무한 슬라이드 기능 - currSlide 값만 변경해줘도 되지만 시각적으로 자연스럽게 하기 위해 아래 코드 작성
currSlide = 0;
let offset = slideWidth * currSlide;
slideItems.forEach((i) => {
i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
});
currSlide++;
offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
// setTimeout을 사용하는 이유는 비동기 처리를 이용해 transition이 제대로 적용되게 하기 위함
setTimeout(() => {
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `transition: ${0.15}s; left: ${-offset}px`);
});
}, 0);
// // 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
}
}
function prevMove() {
currSlide--;
// 1번째 슬라이드 이하로 넘어가지 않게 하기 위해서
if (currSlide > 0) {
// 슬라이드를 이동시키기 위한 offset 계산
const offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
// 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
} else {
// 무한 슬라이드 기능 - currSlide 값만 변경해줘도 되지만 시각적으로 자연스럽게 하기 위해 아래 코드 작성
currSlide = maxSlide + 1;
let offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
});
currSlide--;
offset = slideWidth * currSlide;
setTimeout(() => {
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
// i.setAttribute("style", `transition: ${0}s; left: ${-offset}px`);
i.setAttribute("style", `transition: ${0.15}s; left: ${-offset}px`);
});
}, 0);
// 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
}
}
// 버튼 엘리먼트에 클릭 이벤트 추가하기
nextBtn.addEventListener("click", () => {
// 이후 버튼 누를 경우 현재 슬라이드를 변경
nextMove();
});
// 버튼 엘리먼트에 클릭 이벤트 추가하기
prevBtn.addEventListener("click", () => {
// 이전 버튼 누를 경우 현재 슬라이드를 변경
prevMove();
});
// 브라우저 화면이 조정될 때 마다 slideWidth를 변경하기 위해
window.addEventListener("resize", () => {
slideWidth = slide.clientWidth;
});
// 각 페이지네이션 클릭 시 해당 슬라이드로 이동하기
for (let i = 0; i < maxSlide; i++) {
// 각 페이지네이션마다 클릭 이벤트 추가하기
paginationItems[i].addEventListener("click", () => {
// 클릭한 페이지네이션에 따라 현재 슬라이드 변경해주기(currSlide는 시작 위치가 1이기 때문에 + 1)
currSlide = i + 1;
// 슬라이드를 이동시키기 위한 offset 계산
const offset = slideWidth * currSlide;
// 각 슬라이드 아이템의 left에 offset 적용
slideItems.forEach((i) => {
i.setAttribute("style", `left: ${-offset}px`);
});
// 슬라이드 이동 시 현재 활성화된 pagination 변경
paginationItems.forEach((i) => i.classList.remove("active"));
paginationItems[currSlide - 1].classList.add("active");
});
}
// 드래그(스와이프) 이벤트를 위한 변수 초기화
let startPoint = 0;
let endPoint = 0;
// PC 클릭 이벤트 (드래그)
slide.addEventListener("mousedown", (e) => {
startPoint = e.pageX; // 마우스 드래그 시작 위치 저장
});
slide.addEventListener("mouseup", (e) => {
endPoint = e.pageX; // 마우스 드래그 끝 위치 저장
if (startPoint < endPoint) {
// 마우스가 오른쪽으로 드래그 된 경우
prevMove();
} else if (startPoint > endPoint) {
// 마우스가 왼쪽으로 드래그 된 경우
nextMove();
}
});
// 모바일 터치 이벤트 (스와이프)
slide.addEventListener("touchstart", (e) => {
startPoint = e.touches[0].pageX; // 터치가 시작되는 위치 저장
});
slide.addEventListener("touchend", (e) => {
endPoint = e.changedTouches[0].pageX; // 터치가 끝나는 위치 저장
if (startPoint < endPoint) {
// 오른쪽으로 스와이프 된 경우
prevMove();
} else if (startPoint > endPoint) {
// 왼쪽으로 스와이프 된 경우
nextMove();
}
});