본문 바로가기
  • 코딩, 허쌤이 떠먹여 줄게
FrontEnd/Javascript

이미지 슬라이더 무한 루프 - javascript

by 허쌤 2026. 2. 21.

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

📋 목차

  1. 개요
  2. 핵심 개념
  3. HTML 구조
  4. CSS 구조
  5. JavaScript 코드 상세 분석
  6. 실행 흐름
  7. 핵심 기술 포인트
  8. 개선 가능한 부분

[슬라이드 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">&gt;</p>
    <p class="prev">&lt;</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개의 슬라이드가 한 줄에 배치되도록 함
  • li14.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초)

동작 과정:

  1. Transition 설정
    • CSS transition을 설정하여 부드러운 애니메이션 효과
    • speed가 0이면 transition 없이 즉시 이동
  2. imgs.style.transition = `margin-left ${speed}s ease`;
  3. 위치 이동
    • 인덱스에 따라 margin-left 값을 계산
    • 예: 인덱스 2 → margin-left: -200% (2번 슬라이드 표시)
  4. imgs.style.marginLeft = `${-index * 100}%`;
  5. 페이저 업데이트
    • 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번 활성화
  6. let pagerIdx = (index - 1 + count) % count; pager.forEach((p, idx) => p.classList.toggle('on', idx === pagerIdx));
  7. 현재 인덱스 업데이트
    • 전역 변수 i를 업데이트하여 현재 위치 추적
  8. 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)와 혼동하지 마세요!

  • 페이저는 04까지만 있지만, 슬라이드 인덱스는 06까지 있습니다.

핵심 로직:

  1. transitionend 이벤트
    • CSS transition이 완료되면 자동으로 발생
    • 애니메이션이 끝난 후에만 실행되므로 타이밍이 정확함
  2. 슬라이드 인덱스 0 (마지막 복제본) 처리시나리오:
    • 현재 슬라이드 인덱스 1 (실제 1번 슬라이드)에 있음
    • 사용자가 이전 버튼 클릭 → move(0) 호출
    • 애니메이션으로 슬라이드 인덱스 0 (5_clone 복제본)으로 이동
    • 애니메이션 완료 후 transitionend 발생
    • i === 0 조건 만족 → 슬라이드 인덱스 5 (실제 5번 슬라이드)로 즉시 점프
    • 결과: 사용자는 1번 → 5번으로 자연스럽게 이동한 것처럼 보임
  3. if (i === 0) { // i는 슬라이드 인덱스 (0~6) imgs.style.transition = 'none'; // transition 제거 move(count, 0); // count = 5, 실제 5번 슬라이드로 점프 }
  4. 슬라이드 인덱스 6 (첫 번째 복제본) 처리시나리오:
    • 현재 슬라이드 인덱스 5 (실제 5번 슬라이드)에 있음
    • 사용자가 다음 버튼 클릭 → move(6) 호출
    • 애니메이션으로 슬라이드 인덱스 6 (1_clone 복제본)으로 이동
    • 애니메이션 완료 후 transitionend 발생
    • i === 6 조건 만족 → 슬라이드 인덱스 1 (실제 1번 슬라이드)로 즉시 점프
    • 결과: 사용자는 5번 → 1번으로 자연스럽게 이동한 것처럼 보임
  5. 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로 이동 (페이저는 04, 슬라이드는 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 이벤트를 활용하여 자연스러운 무한 루프를 구현했으며, 코드가 간결하고 이해하기 쉽습니다.

핵심 학습 포인트:

  1. 무한 루프 구현 방법 (복제본 활용)
  2. transitionend 이벤트의 활용
  3. CSS transition과 JavaScript의 조합
  4. 나머지 연산자를 이용한 인덱스 매핑
  5. 이벤트 기반 프로그래밍

이 코드를 이해하면 다양한 슬라이더 구현이 가능합니다!

'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