29. slide

이 글은 여러 이미지를 한 자리에서 차례로 보여 주는 슬라이드 인터페이스를 만든다. jQuery(제이쿼리)의 페이드 메서드와 위치 애니메이션, CSS 클래스 전환, 순수 JavaScript(자바스크립트) 구현을 코드별로 비교한다. 각 예제에서는 현재 슬라이드의 상태를 어떻게 저장하고 마지막 이미지 다음에 첫 이미지로 돌아가는지 확인한다.

슬라이드 전환 방식 비교 — 요소를 겹쳐 배치하는 페이드 방식과 나열하여 위치를 이동하는 방식의 차이를 비교한다.

1. 기본 슬라이드

1.1. fadeInOut

1.1.1. 메서드 활용

페이드 인·아웃(fade in·out)은 한 요소의 투명도를 바꾸어 화면에 나타내거나 사라지게 하는 전환 방식이다. 두 슬라이드를 같은 위치에 겹친 뒤 현재 항목은 숨기고 다음 항목은 보이면 이미지가 부드럽게 교체된다. 미리보기에서는 한 이미지가 사라지는 동안 다음 이미지가 나타나는지, 번호 문구도 함께 바뀌는지 확인한다.

예제

미리보기

리소스 이미지

http://qwerew.cafe24.com/images/1.jpg
http://qwerew.cafe24.com/images/2.jpg
http://qwerew.cafe24.com/images/3.jpg
http://qwerew.cafe24.com/images/4.jpg
http://qwerew.cafe24.com/images/5.jpg
http://qwerew.cafe24.com/images/6.jpg
http://qwerew.cafe24.com/images/7.jpg
http://qwerew.cafe24.com/images/8.jpg
http://qwerew.cafe24.com/images/9.jpg
http://qwerew.cafe24.com/images/10.jpg

리소스 목록은 예제에서 사용할 이미지 주소를 제공한다. 주소를 직접 사용할 때는 네트워크에서 파일을 불러오므로 연결 상태와 원본 서버의 응답에 따라 이미지가 늦게 나타나거나 표시되지 않을 수 있다.

HTML

<body>
  <div id="content">
    <h3>Simple Crossfade Waterfalls Slideshow</h3>
    <ul id="crossfade">
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/1.jpg" alt="" /></a>
        <p>1번</p>
      </li>
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/2.jpg" alt="" /></a>
        <p>2번</p>
      </li>
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/3.jpg" alt="" /></a>
        <p>3번</p>
      </li>
    </ul>
  </div>
</body>

HTML은 슬라이드 한 장을 #crossfade 안의 li 하나로 구성한다. 각 항목에는 이미지와 번호 문구가 함께 들어간다. JavaScript 선택자가 이 구조를 기준으로 항목을 찾으므로 idli 계층을 바꾸면 선택자도 같은 구조에 맞춰 바꿔야 한다.

CSS

#crossfade {
  position: relative;
  margin: auto;
  padding: 0;
  list-style-type: none;
  width: 600px;
  height: 400px;
  overflow: hidden;
}

#crossfade li {
  position: absolute;
  width: 600px;
  height: 400px;
}

#crossfade p {
  position: absolute;
  bottom: 0;
  padding: 20px;
  color: #fff;
  background: #000;
  background-color: rgba(0, 0, 0, 0.6);
  margin: 0;
  left: 0;
  right: 0;
}

CSS는 #crossfadeposition: relative로 두고 각 liposition: absolute로 배치한다. 따라서 여러 항목이 600×400 영역의 같은 좌표에 겹친다. overflow: hidden은 컨테이너 밖으로 나간 부분을 가리며, 너비나 높이를 바꾸려면 컨테이너와 슬라이드 항목의 크기를 함께 맞춰야 한다. 문구의 rgba(0, 0, 0, 0.6)에서 마지막 값은 불투명도이므로 0에 가까울수록 배경이 투명해진다.

JS

방법1

$(() => {
  $('#crossfade li').hide().filter(':first').show();
  setInterval(slideshow, 3000);
  function slideshow() {
    $('#crossfade li:first').fadeOut('slow').next().fadeIn('slow').end().appendTo('#crossfade');
  }
});

방법 1은 처음에 첫 항목만 보인 뒤 3초마다 slideshow 함수를 실행한다. fadeOut()으로 첫 항목을 숨기고, next()로 다음 형제를 찾아 fadeIn()으로 보인다. 마지막의 appendTo()는 방금 숨긴 첫 항목을 목록 끝으로 옮기므로 같은 처리 흐름을 계속 반복할 수 있다. setInterval의 3000을 줄이면 전환 간격이 짧아지고, 늘리면 각 이미지가 더 오래 머문다.

방법2

$(() => {
  let idx = 0;
  $('#crossfade li').hide();
  $('#crossfade li').eq(0).show();
  setInterval(slideshow, 3000);
  function slideshow() {
    $('#crossfade li').eq(idx).fadeOut('slow');
    idx++;
    if (idx > $('#crossfade li').length - 1) {
      idx = 0;
    }
    $('#crossfade li').eq(idx).fadeIn('slow');
  }
});

방법 2는 현재 순서를 idx에 저장한다. 현재 항목을 숨긴 뒤 값을 1 늘리고, 항목 수의 마지막 인덱스를 넘으면 0으로 되돌린다. length - 1을 쓰는 이유는 항목이 세 개일 때 인덱스가 0, 1, 2처럼 0부터 시작하기 때문이다. jQuery를 불러오지 않았거나 코드가 HTML 요소보다 먼저 실행되면 $ 또는 대상 요소를 찾지 못할 수 있으므로 실행 시점도 확인한다.

1.1.2. 클래스 활용

미리보기

이 예제는 JavaScript가 매번 투명도를 계산하는 대신, CSS에 전환 규칙을 두고 클래스의 유무로 상태를 바꾼다. 클래스는 여러 요소에 같은 상태 이름을 붙이는 표식이며, JavaScript는 현재 항목에서 표식을 떼고 다음 항목에 붙이는 방식으로 화면을 전환한다. 미리보기에서는 활성 항목만 보이고 전환 시간이 일정한지 확인한다.

HTML

<body>
  <div id="content">
    <h3>Simple Crossfade Waterfalls Slideshow</h3>
    <ul id="crossfade">
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/1.jpg" alt="" /></a>
        <p>1번</p>
      </li>
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/2.jpg" alt="" /></a>
        <p>2번</p>
      </li>
      <li>
        <a href="#"><img src="http://qwerew.cafe24.com/images/3.jpg" alt="" /></a>
        <p>3번</p>
      </li>
    </ul>
  </div>
</body>

CSS

#crossfade {
  position: relative;
  margin: auto;
  padding: 0;
  list-style-type: none;
  width: 600px;
  height: 400px;
  overflow: hidden;
}

#crossfade li {
  position: absolute;
  width: 600px;
  height: 400px;
  opacity: 0;
  transition: opacity 0.5s;
}

#crossfade p {
  position: absolute;
  bottom: 0;
  padding: 20px;
  color: #fff;
  background: #000;
  background-color: rgba(0, 0, 0, 0.6);
  margin: 0;
  left: 0;
  right: 0;
}

각 슬라이드는 기본값 opacity: 0으로 숨겨지고, transition: opacity 0.5s가 투명도 변화에 0.5초를 사용한다. 전환 시간을 늘리면 변화가 느려지고 줄이면 빨라진다. 활성 클래스에 투명도를 1로 만드는 규칙이 별도로 있어야 화면에 항목이 나타나므로, 클래스 이름과 CSS 선택자가 일치하는지 확인한다.

JS

$(() => {
  $('#crossfade li').hide().filter(':first').show();
  setInterval(slideshow, 3000);
  function slideshow() {
    $('#crossfade li:first').fadeOut('slow').next().fadeIn('slow').end().appendTo('#crossfade');
  }
});

JavaScript는 첫 항목을 숨겨 목록 끝으로 보내는 방식으로 순서를 순환시킨다. 이 방식은 항목 자체의 배열 순서를 바꾸므로 별도의 인덱스 없이도 항상 첫 번째와 다음 항목을 기준으로 처리할 수 있다.

1.2. 이동

이동 슬라이드는 항목의 left 값을 바꾸어 가로 방향으로 밀어 내고 다음 항목을 화면 안으로 가져온다. 페이드 방식과 달리 이전 화면과 다음 화면의 이동 방향이 보여야 하므로 모든 항목의 시작 위치를 먼저 정렬해야 한다.

jQuery1 미리보기

jQuery2 미리보기

두 미리보기는 같은 HTML·CSS 구조에 서로 다른 jQuery 이동 로직을 적용한다. HTML의 각 li는 한 장의 슬라이드이며, CSS에서 절대 배치되므로 JavaScript가 지정하는 left 값에 따라 보이는 영역으로 들어오거나 밖으로 나간다.

HTML
<div id="content">
	<h3>Slideshow</h3>
	<ul id="crossfade">
		<li>
			<a href="#"><img src="http://qwerew.cafe24.com/images/1.jpg" alt="" /></a>
			
1번

		</li>
		<li>
			<a href="#"><img src="http://qwerew.cafe24.com/images/2.jpg" alt="" /></a>
			
2번

		</li>
		<li>
			<a href="#"><img src="http://qwerew.cafe24.com/images/3.jpg" alt="" /></a>
			
3번

		</li>
	</ul>
</div>
CSS
#crossfade {
	position: relative;
	margin: auto;
	padding: 0;
	list-style-type: none;
	width: 600px;
	height: 400px;
	overflow: hidden;
}
#crossfade li {
	position: absolute;
	width: 600px;
	height: 400px;
}

#crossfade p {
	position: absolute;
	bottom: 0;
	padding: 20px;
	color: #fff;
	background: #000;
	background-color: rgba(0, 0, 0, 0.6);
	margin: 0;
	left: 0;
	right: 0;
}

컨테이너의 고정 크기와 overflow: hidden은 화면에 한 장만 보이게 한다. 항목 너비를 600px에서 바꾸면 이동 계산이 백분율 기준인지 픽셀 기준인지 함께 살펴야 한다. 이 예제는 항목 너비의 100%를 한 장의 이동 거리로 사용한다.

JQurey1
const slide = $('#crossfade li');

// 정렬

slide.each((i, o) => {

$(o).css('left', i * 100 + '%');

});

let idx = 0;

// 무한루프를 위한 setInterval

setInterval(() => {

slide

.eq(idx)

.stop()

.animate({ left: -100 + '%' }, 1000, function () {

$(this).css('left', (slide.length - 1) * 100 + '%');

});

idx++;

if (idx > slide.length - 1) {

idx = 0;

}

slide

.eq(idx)

.stop()

.animate({ left: 0 + '%' }, 1000);

}, 3000); // 3초마다 반복

첫 번째 jQuery 코드는 each()로 항목을 0%, 100%, 200% 위치에 차례로 놓는다. 3초마다 현재 항목을 -100%로 내보내고 다음 항목을 0%로 가져온다. stop()은 이전 애니메이션 대기열을 멈춰 반복 실행이 겹치는 현상을 줄인다. idx가 항목 수를 넘으면 0으로 돌아가므로 마지막 뒤에 첫 장이 이어진다.

JQurey2
		const slide = $('#crossfade li');
		let current = 0;
		// 정렬
		slide.each((i, o) => {
			$(o).css('left', i * 100 + '%');
		});	setInterval(function () {
		var prev = slide.eq(current);
		move(prev, 0, '-100%');
		current++;
		if (current == slide.length) {
			current = 0;
		}
		var next = slide.eq(current);
		move(next, '100%', '0%');
	}, 3000);

	function move(tg, start, end) {
		tg.css('left', start).stop().animate({ left: end }, 1000);
	}

두 번째 jQuery 코드는 이동 처리를 move(tg, start, end) 함수로 분리한다. 대상, 시작 위치, 끝 위치만 넘기면 같은 애니메이션을 재사용할 수 있다. 현재 항목은 0%에서 -100%로 이동하고 다음 항목은 100%에서 0%로 이동한다. 함수 호출의 시작·끝 값을 반대로 주면 이동 방향도 달라진다.

2. 인디케이터가 있는 슬라이드

인디케이터(indicator, 인디케이터)는 전체 슬라이드 중 현재 위치를 표시하고 특정 항목으로 이동하게 하는 조작 요소이다. 아래 예제는 좌우 화살표, 원형 버튼, 자동 재생을 하나의 현재 인덱스와 연결한다. 완성 화면에서는 이미지가 바뀔 때 활성 버튼의 on 클래스도 같은 순서로 이동하는지 확인한다.

  #### 코드

[제이쿼리슬라이드](https://qwerewqwerew.github.io/source/jq/12/12-jq.html)
[자바스크립트슬라이드](https://qwerewqwerew.github.io/source/jq/12/12-js.html)

[완성화면](12/final/img.zip)

  
  #### HTML


<div class="slide_wrap">
  <div class="brand_visual">
    <ul>
      <li class="visual_0"><a href="#">배너이미지1</a></li>
      <li class="visual_1"><a href="#">배너이미지2</a></li>
      <li class="visual_2"><a href="#">배너이미지3</a></li>
    </ul>
  </div>
  <div class="btns">
    <img src="img/left.png" class="prev" width="30" height="50" alt="" />
    <img src="img/right.png" class="next" width="30" height="50" alt="" />
  </div>
  <ul class="buttons">
    <li class="on"><a href="#">배너1</a></li>
    <li><a href="#">배너2</a></li>
    <li><a href="#">배너3</a></li>
  </ul>
</div>

HTML은 배너 목록, 이전·다음 화살표, 인디케이터 목록을 나눈다. 첫 인디케이터의 on 클래스는 초기 활성 상태를 뜻한다. 이미지 경로는 현재 문서 위치를 기준으로 해석되는 상대 경로이므로 img 폴더와 파일명이 실제 구조와 맞아야 배경과 화살표가 나타난다.

CSS

/* common */
html {
  width: 100%;
  height: 100%;
  overflow-y: scroll;
}
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0px;
  padding: 0px;
  font: inherit;
  vertical-align: baseline;
}
body {
  font-size: 12px;
  font-family: Dotum, Arial;
  color: #74767a;
  line-height: 120%;
}
a,
a:link,
a:visited {
  color: #74767a;
  text-decoration: none;
}
ul,
ol {
  list-style: none;
}
table,
fieldset,
th,
td,
img {
  border: none;
}
img,
input,
select {
  vertical-align: middle;
}
.slide_wrap{position:relative;}
.brand_visual ul {
  overflow: hidden;
  position: relative;
  width: 100%;
  height: clamp(50vh,500px,20vh);
}
.brand_visual ul li {
  position: absolute;
  width: 100%;
  height: 100%;
}
 /* 글씨 제거 */
.brand_visual ul li a {
  display: block;
  text-indent: -9999px;
}

/* visual_0, visual_1, visual_2 위치정렬 */
.brand_visual .visual_0 {
  left: 0;
  background: url(img/0.png) 50% 0 no-repeat;
}
.brand_visual .visual_1 {
  left: 100%;
  background: url(img/1.png) 50% 0 no-repeat;
}
.brand_visual .visual_2 {
  left: 200%;
  background: url(img/2.png) 50% 0 no-repeat;
}

/* 화살표 */
.btns {
  position: relative;
  top: -250px;
  width: 100%;
}
.btns .prev {
  position: absolute;
  left: 100px;
}
.btns .next {
  position: absolute;
  right: 100px;
}

/* 버튼 위치 지정 */
.buttons {
  display: flex;
  position: absolute;
  left: 50%;
  top: -135px;
  gap:10px;
  transform:translateX(-50%);
}
.buttons li {
  background: url(img/btnVisual.png) 0 -16px no-repeat;
  width: 14px;
  height: 15px;
  overflow: hidden;
  cursor: pointer;
}
/* 글씨 제거 */
.buttons li a {
  display: block;
  text-indent: -9999px;
}
/* 활성화된 버튼 */
.buttons li.on {
  background-position: 0 0;
}

첫 CSS 블록은 기본 여백과 목록 표시를 초기화한다. 두 번째 블록은 슬라이드를 가로로 정렬하고, visual_0부터 visual_2까지 각각 0%, 100%, 200% 위치에 둔다. .buttons li.on은 배경 이미지 위치를 바꾸어 활성 인디케이터를 표시한다. 화살표와 버튼은 절대 위치를 사용하므로 슬라이드 높이를 바꾸면 top 값도 화면에 맞게 조정해야 한다.

JQ

$(() => {
  const visual = $('.brand_visual>ul>li');
  const button = $('.buttons>li');
  const leftBtn = $('.btns .prev');
  const rightBtn = $('.btns .next');
  let setIntervalId;
  const counter = $('.counter');

  let current = 0;

  function timer() {
    setIntervalId = setInterval(function () {
      changeSlide((current + 1) % visual.length); // 0%3=0/1%3=1/2%3=2/3%3=0
    }, 3000);
  }

  function move(tg, start, end) {
    tg.css('left', start).stop().animate({ left: end }, { duration: 500, ease: 'easeOutCubic' });
  }

  function cnt(num) {
    counter.html(`${num + 1}`);
  }

  button.on('click', function () {
    const i = $(this).index();
    changeSlide(i);
  });

  function changeSlide(i, direction = 'right') {
    if (current == i) return;
    const currentEl = visual.eq(current);
    const nextEl = visual.eq(i);
    if (direction === 'right') {
      move(currentEl, 0, '-100%');
      move(nextEl, '100%', 0);
    } else {
      move(currentEl, 0, '100%');
      move(nextEl, '-100%', 0);
    }
    button.eq(current).removeClass('on');
    button.eq(i).addClass('on');
    cnt(i);
    current = i;
  }

  $('.slide_wrap').on({
    mouseover: function () {
      clearInterval(setIntervalId);
    },
    mouseout: timer,
  });

  rightBtn.click(function () {
    changeSlide((current + 1) % visual.length);
    return false;
  });

  leftBtn.click(function () {
    changeSlide((current - 1 + visual.length) % visual.length, 'left');
    return false;
  });

  timer(); // timer를 마지막에 호출하여 초기 슬라이드 쇼를 시작
});

jQuery 구현은 current에 현재 번호를 저장하고 changeSlide()에서 화면, 인디케이터, 카운터를 함께 갱신한다. 나머지 연산자 %는 다음 번호가 항목 수와 같아질 때 0으로 순환하게 한다. 마우스가 슬라이드 위에 있으면 clearInterval()로 자동 재생을 멈추고, 벗어나면 타이머를 다시 시작한다. easeOutCubic을 사용하려면 해당 완화 함수를 제공하는 환경이 준비돼 있어야 하며, 준비되지 않으면 애니메이션 옵션을 확인한다.

JS

const visual = document.querySelectorAll('#brandVisual>ul>li');
const button = document.querySelectorAll('#buttonList>li');
const leftBtn = document.querySelector('.btnImg .prev');
const rightBtn = document.querySelector('.btnImg .next');
let current = 0;
let setIntervalId;
let isMove = false;

// 슬라이드 이동
function moveSlide(prevIndex, nextIndex) {
  if (isMove) return;
  isMove = true;
  move(visual[prevIndex], 0, '-100%');
  button[prevIndex].classList.remove('on');
  move(visual[nextIndex], '100%', 0);
  button[nextIndex].classList.add('on');
  current = nextIndex;
}

// 애니메이션
function move(tg, start, end) {
  let keyframes = [{ left: start }, { left: end }];
  let options = { duration: 1000, fill: 'forwards' };
  let animation = tg.animate(keyframes, options);
  animation.onfinish = () => {
    isMove = false;
  };
}

// 타이머
function timer() {
  setIntervalId = setInterval(() => {
    let nextIndex = (current + 1) % visual.length;
    moveSlide(current, nextIndex);
  }, 3000);
}

setTimeout(timer, 1000);

// 이벤트 리스너들
document.querySelector('#wrap').addEventListener('mouseover', () => clearInterval(setIntervalId));
document.querySelector('#wrap').addEventListener('mouseout', timer);

button.forEach((btn, i) => {
  btn.addEventListener('click', () => moveSlide(current, i));
});

rightBtn.addEventListener('click', (e) => {
  e.preventDefault();
  let nextIndex = (current + 1) % visual.length;
  moveSlide(current, nextIndex);
});

leftBtn.addEventListener('click', (e) => {
  e.preventDefault();
  let nextIndex = (current - 1 + visual.length) % visual.length;
  moveSlide(current, nextIndex);
});

순수 JavaScript 구현도 현재 번호와 자동 재생 타이머를 관리하지만, 요소 선택에는 querySelectorquerySelectorAll을 사용한다. move()는 Web Animations API(웹 애니메이션 API)로 left 값을 바꾸며, isMove는 이전 이동이 끝나기 전에 새 이동이 겹치지 않게 막는다. 이 코드의 선택자 #brandVisual, #buttonList, .btnImg, #wrap은 앞의 HTML 예제 이름과 다르므로 실제 적용 문서에서는 대상 구조와 선택자가 일치하는지 먼저 확인해야 한다.

댓글 남기기