이미지 슬라이더 무한 루프 - 완전 정복
📋 목차

[슬라이드 3 바로가기](https://bhher.github.io/webdesign/slide_3/)
개요
이 이미지 슬라이더는 무한 루프를 구현한 슬라이더입니다. 마지막 슬라이드에서 다음 버튼을 누르면 첫 번째 슬라이드로 자연스럽게 돌아가고, 첫 번째 슬라이드에서 이전 버튼을 누르면 마지막 슬라이드로 이동합니다.
주요 기능:
- 무한 루프 슬라이드 (끝없이 순환)
- 자동 재생 (3초 간격)
- 이전/다음 버튼 제어
- 페이저(점) 클릭으로 직접 이동
- 마우스 오버 시 자동 재생 일시 정지
핵심 개념
1. 무한 루프의 원리
무한 루프를 구현하는 핵심은 복제본(Clone)을 사용하는 것입니다.
원본 구조: [1, 2, 3, 4, 5]
복제 후: [5_clone, 1, 2, 3, 4, 5, 1_clone]
- 마지막 슬라이드(5번)를 앞에 복제: 첫 번째에서 이전 버튼 클릭 시 자연스럽게 마지막으로 이동
- 첫 번째 슬라이드(1번)를 뒤에 복제: 마지막에서 다음 버튼 클릭 시 자연스럽게 첫 번째로 이동
2. 인덱스 체계
복제본 구조: [5_clone, 1, 2, 3, 4, 5, 1_clone]
인덱스: 0 1 2 3 4 5 6
- 인덱스 0: 마지막 슬라이드의 복제본 (5_clone)
- 인덱스 1~5: 실제 슬라이드 (1, 2, 3, 4, 5)
- 인덱스 6: 첫 번째 슬라이드의 복제본 (1_clone)
3. transitionend 이벤트
CSS transition이 완료되면 transitionend 이벤트가 발생합니다. 이 이벤트를 활용하여 복제본에 도달했을 때 실제 슬라이드 위치로 "점프"합니다.
HTML 구조
<div id="wrap">
<div class="slide">
<ul class="imgs cf">
<li><img src="./images/photo1.jpg" alt=""/></li>
<li><img src="./images/photo2.jpg" alt=""/></li>
<li><img src="./images/photo3.jpg" alt=""/></li>
<li><img src="./images/photo4.jpg" alt=""/></li>
<li><img src="./images/photo5.jpg" alt=""/></li>
</ul>
</div>
<ul class="pager">
<li class="on"><a href="#"></a></li>
<li><a href="#"></a></li>
<li><a href="#"></a></li>
<li><a href="#"></a></li>
<li><a href="#"></a></li>
</ul>
<p class="next">></p>
<p class="prev"><</p>
</div>
구조 설명:
.imgs: 모든 슬라이드 이미지를 담는 컨테이너.pager: 슬라이드 위치를 나타내는 점(페이저).next: 다음 버튼.prev: 이전 버튼
CSS 구조
핵심 CSS 속성
.slide .imgs {
width: 700%; /* 7배 너비 (5개 실제 + 2개 복제본) */
}
.slide .imgs li {
width: 14.2857%; /* 100% / 7 = 14.2857% */
float: left;
}
설명:
.imgs의 너비를700%로 설정하여 7개의 슬라이드가 한 줄에 배치되도록 함- 각
li는14.2857%너비를 가져 7개가 정확히 한 줄에 배치됨 margin-left를 조절하여 보이는 슬라이드를 변경
JavaScript 코드 상세 분석
전체 코드
document.addEventListener('DOMContentLoaded', () => {
const imgs = document.querySelector('.imgs');
const items = document.querySelectorAll('.imgs li');
const pager = document.querySelectorAll('.pager li');
const count = items.length;
let i = 1, timer;
// 1. 앞뒤 복제본 추가 (무한 루프 핵심)
const firstClone = items[0].cloneNode(true);
const lastClone = items[count - 1].cloneNode(true);
imgs.appendChild(firstClone);
imgs.insertBefore(lastClone, items[0]);
// 2. 초기 위치 설정 (1번 이미지)
imgs.style.marginLeft = '-100%';
// 3. 슬라이드 이동 함수
function move(index, speed = 0.6) {
imgs.style.transition = `margin-left ${speed}s ease`;
imgs.style.marginLeft = `${-index * 100}%`;
// 페이저 업데이트 (복제본일 때 인덱스 보정)
let pagerIdx = (index - 1 + count) % count;
pager.forEach((p, idx) => p.classList.toggle('on', idx === pagerIdx));
i = index;
}
// 4. 무한 루프를 위한 위치 초기화 (트랜지션 종료 후 실행)
imgs.addEventListener('transitionend', () => {
if (i === 0) { // 마지막 복제본 -> 실제 마지막으로 점프
imgs.style.transition = 'none';
move(count, 0);
} else if (i === count + 1) { // 첫번째 복제본 -> 실제 첫번째로 점프
imgs.style.transition = 'none';
move(1, 0);
}
});
// 5. 이벤트 바인딩
document.querySelector('.next').onclick = () => move(i + 1);
document.querySelector('.prev').onclick = () => move(i - 1);
pager.forEach((p, idx) => p.onclick = () => move(idx + 1));
// 6. 자동 재생 및 마우스 제어
const play = () => timer = setInterval(() => move(i + 1), 3000);
const stop = () => clearInterval(timer);
document.getElementById('wrap').onmouseenter = stop;
document.getElementById('wrap').onmouseleave = play;
play();
});
라인별 상세 설명
1. 초기화 및 변수 선언
document.addEventListener('DOMContentLoaded', () => {
const imgs = document.querySelector('.imgs');
const items = document.querySelectorAll('.imgs li');
const pager = document.querySelectorAll('.pager li');
const count = items.length;
let i = 1, timer;
설명:
DOMContentLoaded: DOM이 완전히 로드된 후 실행imgs: 슬라이드 컨테이너 요소items: 모든 슬라이드li요소들 (복제 전, 5개)pager: 페이저(점) 요소들count: 실제 슬라이드 개수 (5)i: 현재 슬라이드 인덱스 (1부터 시작, 실제 1번 슬라이드)timer: 자동 재생 타이머 저장 변수
2. 복제본 생성 및 추가
// 1. 앞뒤 복제본 추가 (무한 루프 핵심)
const firstClone = items[0].cloneNode(true);
const lastClone = items[count - 1].cloneNode(true);
imgs.appendChild(firstClone);
imgs.insertBefore(lastClone, items[0]);
설명:
firstClone: 첫 번째 슬라이드(1번)의 복제본lastClone: 마지막 슬라이드(5번)의 복제본appendChild(firstClone): 복제본을 맨 뒤에 추가 →[1, 2, 3, 4, 5, 1_clone]insertBefore(lastClone, items[0]): 복제본을 맨 앞에 추가 →[5_clone, 1, 2, 3, 4, 5, 1_clone]
최종 구조:
인덱스: 0 1 2 3 4 5 6
슬라이드: [5_clone, 1, 2, 3, 4, 5, 1_clone]
3. 초기 위치 설정
// 2. 초기 위치 설정 (1번 이미지)
imgs.style.marginLeft = '-100%';
설명:
margin-left: -100%는 한 슬라이드 너비만큼 왼쪽으로 이동- 복제본(인덱스 0)을 건너뛰고 실제 1번 슬라이드(인덱스 1)가 보이도록 설정
- 각 슬라이드가
14.2857%너비이므로-100%는 정확히 한 슬라이드 너비
시각적 설명:
초기 상태:
[5_clone, 1, 2, 3, 4, 5, 1_clone]
↑
margin-left: -100%로 1번이 보임
4. 슬라이드 이동 함수
// 3. 슬라이드 이동 함수
function move(index, speed = 0.6) {
imgs.style.transition = `margin-left ${speed}s ease`;
imgs.style.marginLeft = `${-index * 100}%`;
// 페이저 업데이트 (복제본일 때 인덱스 보정)
let pagerIdx = (index - 1 + count) % count;
pager.forEach((p, idx) => p.classList.toggle('on', idx === pagerIdx));
i = index;
}
매개변수:
index: 이동할 슬라이드 인덱스 (0~6)speed: 애니메이션 속도 (기본값 0.6초)
동작 과정:
- Transition 설정
- CSS transition을 설정하여 부드러운 애니메이션 효과
speed가 0이면 transition 없이 즉시 이동
imgs.style.transition = `margin-left ${speed}s ease`;- 위치 이동
- 인덱스에 따라
margin-left값을 계산 - 예: 인덱스 2 →
margin-left: -200%(2번 슬라이드 표시)
- 인덱스에 따라
imgs.style.marginLeft = `${-index * 100}%`;- 페이저 업데이트
pagerIdx: 페이저 인덱스 계산 (0~4)- 복제본(인덱스 0, 6)일 때도 올바른 페이저를 활성화
(index - 1 + count) % count공식:- 인덱스 0 (5_clone) →
(0-1+5) % 5 = 4→ 페이저 4번 활성화 - 인덱스 1 (실제 1번) →
(1-1+5) % 5 = 0→ 페이저 0번 활성화 - 인덱스 6 (1_clone) →
(6-1+5) % 5 = 0→ 페이저 0번 활성화
- 인덱스 0 (5_clone) →
let pagerIdx = (index - 1 + count) % count; pager.forEach((p, idx) => p.classList.toggle('on', idx === pagerIdx));- 현재 인덱스 업데이트
- 전역 변수
i를 업데이트하여 현재 위치 추적
- 전역 변수
i = index;
5. 무한 루프 구현 (transitionend 이벤트)
// 4. 무한 루프를 위한 위치 초기화 (트랜지션 종료 후 실행)
imgs.addEventListener('transitionend', () => {
if (i === 0) { // 마지막 복제본 -> 실제 마지막으로 점프
imgs.style.transition = 'none';
move(count, 0);
} else if (i === count + 1) { // 첫번째 복제본 -> 실제 첫번째로 점프
imgs.style.transition = 'none';
move(1, 0);
}
});
⚠️ 중요: 인덱스 체계 이해하기
이 코드에서 사용하는 인덱스는 슬라이드 인덱스입니다 (페이저 인덱스와 다릅니다):
슬라이드 구조: [5_clone, 1, 2, 3, 4, 5, 1_clone]
슬라이드 인덱스: 0 1 2 3 4 5 6
실제 슬라이드 번호: 복제본 1 2 3 4 5 복제본
- 슬라이드 인덱스 0: 마지막 슬라이드의 복제본 (5_clone) ← 복제본
- 슬라이드 인덱스 1~5: 실제 슬라이드 (1번, 2번, 3번, 4번, 5번) ← 실제 슬라이드
- 슬라이드 인덱스 6: 첫 번째 슬라이드의 복제본 (1_clone) ← 복제본
페이저 인덱스 (0~4)와 혼동하지 마세요!
- 페이저는 0
4까지만 있지만, 슬라이드 인덱스는 06까지 있습니다.
핵심 로직:
- transitionend 이벤트
- CSS transition이 완료되면 자동으로 발생
- 애니메이션이 끝난 후에만 실행되므로 타이밍이 정확함
- 슬라이드 인덱스 0 (마지막 복제본) 처리시나리오:
- 현재 슬라이드 인덱스 1 (실제 1번 슬라이드)에 있음
- 사용자가 이전 버튼 클릭 →
move(0)호출 - 애니메이션으로 슬라이드 인덱스 0 (5_clone 복제본)으로 이동
- 애니메이션 완료 후
transitionend발생 i === 0조건 만족 → 슬라이드 인덱스 5 (실제 5번 슬라이드)로 즉시 점프- 결과: 사용자는 1번 → 5번으로 자연스럽게 이동한 것처럼 보임
if (i === 0) { // i는 슬라이드 인덱스 (0~6) imgs.style.transition = 'none'; // transition 제거 move(count, 0); // count = 5, 실제 5번 슬라이드로 점프 }- 슬라이드 인덱스 6 (첫 번째 복제본) 처리시나리오:
- 현재 슬라이드 인덱스 5 (실제 5번 슬라이드)에 있음
- 사용자가 다음 버튼 클릭 →
move(6)호출 - 애니메이션으로 슬라이드 인덱스 6 (1_clone 복제본)으로 이동
- 애니메이션 완료 후
transitionend발생 i === 6조건 만족 → 슬라이드 인덱스 1 (실제 1번 슬라이드)로 즉시 점프- 결과: 사용자는 5번 → 1번으로 자연스럽게 이동한 것처럼 보임
else if (i === count + 1) { // count + 1 = 6 imgs.style.transition = 'none'; move(1, 0); // 실제 1번 슬라이드로 점프 }
시각적 흐름:
마지막(5번) → 다음 버튼 클릭
[5_clone, 1, 2, 3, 4, 5, 1_clone]
↑
애니메이션으로 이동
↓
transitionend 발생
↓
[5_clone, 1, 2, 3, 4, 5, 1_clone]
↑
transition 없이 즉시 점프
6. 이벤트 바인딩
// 5. 이벤트 바인딩
document.querySelector('.next').onclick = () => move(i + 1);
document.querySelector('.prev').onclick = () => move(i - 1);
pager.forEach((p, idx) => p.onclick = () => move(idx + 1));
설명:
- 다음 버튼: 현재 인덱스 + 1로 이동
- 이전 버튼: 현재 인덱스 - 1로 이동
- 페이저 클릭: 클릭한 페이저 인덱스 + 1로 이동 (페이저는 0
4, 슬라이드는 15)
7. 자동 재생 및 마우스 제어
// 6. 자동 재생 및 마우스 제어
const play = () => timer = setInterval(() => move(i + 1), 3000);
const stop = () => clearInterval(timer);
document.getElementById('wrap').onmouseenter = stop;
document.getElementById('wrap').onmouseleave = play;
play();
설명:
play(): 3초마다 다음 슬라이드로 자동 이동stop(): 자동 재생 중지mouseenter: 마우스가 슬라이더 위에 올라가면 자동 재생 중지mouseleave: 마우스가 벗어나면 자동 재생 재개play(): 페이지 로드 시 자동 재생 시작
실행 흐름
시나리오 1: 다음 버튼 클릭 (1번 → 2번)
1. 사용자가 다음 버튼 클릭
2. move(2) 호출 (i + 1 = 2)
3. transition 설정: 'margin-left 0.6s ease'
4. margin-left: -200% 설정
5. 애니메이션 시작 (1번 → 2번)
6. 페이저 업데이트 (0번 → 1번)
7. i = 2 업데이트
8. 애니메이션 완료 (transitionend 발생)
9. i === 2이므로 점프하지 않음
시나리오 2: 마지막에서 다음 버튼 클릭 (5번 → 1번)
1. 현재 i = 5 (마지막 슬라이드)
2. 사용자가 다음 버튼 클릭
3. move(6) 호출 (i + 1 = 6)
4. transition 설정: 'margin-left 0.6s ease'
5. margin-left: -700% 설정 (1_clone 위치)
6. 애니메이션 시작 (5번 → 1_clone)
7. 페이저 업데이트 (4번 → 0번, 1_clone이므로)
8. i = 6 업데이트
9. 애니메이션 완료 (transitionend 발생)
10. i === 6 (count + 1)이므로 조건 만족
11. transition = 'none' 설정
12. move(1, 0) 호출 (실제 1번으로 점프)
13. margin-left: -100% 즉시 설정 (애니메이션 없음)
14. 페이저 업데이트 (0번 유지)
15. i = 1 업데이트
시나리오 3: 첫 번째에서 이전 버튼 클릭 (1번 → 5번)
1. 현재 i = 1 (첫 번째 슬라이드)
2. 사용자가 이전 버튼 클릭
3. move(0) 호출 (i - 1 = 0)
4. transition 설정: 'margin-left 0.6s ease'
5. margin-left: 0% 설정 (5_clone 위치)
6. 애니메이션 시작 (1번 → 5_clone)
7. 페이저 업데이트 (0번 → 4번, 5_clone이므로)
8. i = 0 업데이트
9. 애니메이션 완료 (transitionend 발생)
10. i === 0이므로 조건 만족
11. transition = 'none' 설정
12. move(5, 0) 호출 (실제 5번으로 점프)
13. margin-left: -500% 즉시 설정 (애니메이션 없음)
14. 페이저 업데이트 (4번 유지)
15. i = 5 업데이트
핵심 기술 포인트
1. cloneNode(true)
const firstClone = items[0].cloneNode(true);
cloneNode(true): 깊은 복사 (자식 요소까지 모두 복사)cloneNode(false): 얕은 복사 (요소만 복사, 자식 제외)- 이미지까지 포함하여 완전히 동일한 요소 생성
2. 나머지 연산자(%)를 이용한 페이저 인덱스 계산
let pagerIdx = (index - 1 + count) % count;
예시:
- 인덱스 0:
(0 - 1 + 5) % 5 = 4→ 페이저 4번 - 인덱스 1:
(1 - 1 + 5) % 5 = 0→ 페이저 0번 - 인덱스 6:
(6 - 1 + 5) % 5 = 0→ 페이저 0번
복제본과 실제 슬라이드를 동일한 페이저로 매핑하는 핵심 공식입니다.
3. transitionend 이벤트의 활용
imgs.addEventListener('transitionend', () => {
// transition 완료 후 실행
});
- CSS transition이 완료되면 자동 발생
setTimeout보다 정확한 타이밍 보장- 여러 transition이 동시에 발생해도 각각에 대해 이벤트 발생
4. transition 제거를 통한 즉시 점프
imgs.style.transition = 'none';
move(count, 0);
- transition을 제거하면 애니메이션 없이 즉시 이동
- 사용자는 점프를 느끼지 못함 (복제본과 실제 슬라이드가 동일하므로)
5. 화살표 함수와 이벤트 핸들러
document.querySelector('.next').onclick = () => move(i + 1);
- 화살표 함수로 간결한 코드 작성
- 클로저를 통해
i변수에 접근
개선 가능한 부분
1. transitionend 이벤트 중복 방지
현재 코드는 모든 transitionend에 대해 이벤트가 발생하므로, 다른 transition이 있을 경우 문제가 될 수 있습니다.
개선안:
imgs.addEventListener('transitionend', (e) => {
// margin-left transition만 처리
if (e.propertyName !== 'margin-left') return;
if (i === 0) {
imgs.style.transition = 'none';
move(count, 0);
} else if (i === count + 1) {
imgs.style.transition = 'none';
move(1, 0);
}
});
2. 자동 재생 중지 시 타이머 정리
마우스가 빠르게 들어갔다 나갔다 할 때 타이머가 중복 생성될 수 있습니다.
개선안:
const play = () => {
stop(); // 기존 타이머 정리
timer = setInterval(() => move(i + 1), 3000);
};
3. 페이저 업데이트 최적화
현재는 모든 페이저를 순회하며 업데이트합니다.
개선안:
// 현재 활성화된 페이저 찾기
const currentActive = document.querySelector('.pager li.on');
if (currentActive) currentActive.classList.remove('on');
pager[pagerIdx].classList.add('on');
4. 터치 이벤트 지원
모바일 환경을 위한 스와이프 제스처 추가 가능합니다.
5. 키보드 네비게이션
방향키로 슬라이드 이동 기능 추가 가능합니다.
마무리
이 이미지 슬라이더는 복제본을 활용한 무한 루프 구현의 완벽한 예제입니다. transitionend 이벤트를 활용하여 자연스러운 무한 루프를 구현했으며, 코드가 간결하고 이해하기 쉽습니다.
핵심 학습 포인트:
- 무한 루프 구현 방법 (복제본 활용)
transitionend이벤트의 활용- CSS transition과 JavaScript의 조합
- 나머지 연산자를 이용한 인덱스 매핑
- 이벤트 기반 프로그래밍
이 코드를 이해하면 다양한 슬라이더 구현이 가능합니다!
'FrontEnd > Javascript' 카테고리의 다른 글
| jQuery 높이 관련 메서드 (0) | 2026.03.03 |
|---|---|
| Slick Slider 공부하기 (0) | 2026.02.26 |
| 세로메뉴 - 아코디언 메뉴 (1) | 2026.02.16 |
| 아코디언 메뉴 (Width 방식) (0) | 2026.02.13 |
| 아코디언 메뉴 (Height + CSS Transition 방식) (0) | 2026.02.13 |