ㅇㅇㅈ Blog

프론트엔드 수행중

0%

JS-ImageSlider

무한 루프 이미지 슬라이더

이미지 슬라이더에 대한 이해

  1. 이미지가 실제 보이는 div가 있다

  2. 그 안에 ul로 감싸진 실제 이미지들(li)이 일렬로 정렬되어 있다

  3. ul의 포지션이 움직이면서 다음이나 전의 이미지가 보여진다

HTML 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<div class="kind_wrap">
<div class="kind_slider">
<ul class="slider">
<li>
<img src="https://via.placeholder.com/800x200.png?text=A" alt="" />
</li>
<li>
<img src="https://via.placeholder.com/800x200.png?text=B" alt="" />
</li>
<li>
<img src="https://via.placeholder.com/800x200.png?text=C" alt="" />
</li>
<li>
<img src="https://via.placeholder.com/800x200.png?text=D" alt="" />
</li>
</ul>
</div>
<div class="arrow">
<a href="javascript:void(0)" class="prev">이전</a>
<a href="javascript:void(0)" class="next">다음</a>
</div>
</div>
</body>

style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
* {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
.kind_wrap {
border: 2px solid black;
width: 100%;
max-width: 800px;
margin: 0 auto;
position: relative;
}
.kind_wrap > .kind_slider {
overflow: hidden;
}
.kind_wrap > .kind_slider .slider {
position: relative;
}
.kind_wrap > .kind_slider .slider li {
float: left;
}
.kind_wrap > .kind_slider img {
vertical-align: top;
}
.kind_wrap .arrow > a.prev {
position: absolute;
left: -50px;
top: 100px;
}
.kind_wrap .arrow > a.next {
position: absolute;
right: -50px;
top: 100px;
}
  • 실제 보여지는 화면인 div.kind_wrap에만 최대 넓이 값이 들어있고
    ul 이나 li에는 사이즈가 없다
  • ul의 width 값은 이미지(li)의 갯수에 의해 정해진다 (ul의 width값이 변할수 있다)
  • 이미지(li)의 사이즈 역시 변할수 있다
  • 자바스크립트에서 li의 사이즈를 구하고 li의 갯수만큼 ul의 사이즈를 구해서 스타일에 넣어준다

JavaScript

  1. 각 노드들을 가져온다
  2. li의 width 값을 구해준다
  3. li의 갯수만큼 ul의 width값도 구해준다
  4. ul의 width 값을 설정해 준다

노드 준비

1
2
3
4
5
6
7
8
9
10
// 노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider') // ul
const slideLis = slider.querySelectorAll('li') //li를 배열로 받아온다
const arrow = kindWrap.querySelector('.arrow')

// CSSOM 셋업
const liWidth = slideLis[0].clientWidth // li 1개의 width
const sliderWidth = liWidth * slideLis.length // li의 갯수만큼 width를 곱해준다
slider.style.width = sliderWidth + 'px' // ul의 width 설정
  1. 버튼에 클릭 이벤트를 줘서 ul의 포지션을 li의 넓이값만큼 움직여 주면 된다.
  2. 버튼을 각각이 아닌 한번에 가져왔기 때문에 if문으로 구별을 해준다

클릭 이벤트 추가

1
2
3
4
5
6
7
// 클릭 이벤트
arrow.addEventListener('click', function (e) {
e.preventDefault
if (e.target.className === 'next') {
slider.style.left = `-${liWidth}px`
}
})
  • 이렇게까지 작성하고 나면 다음으로 가는 버튼이 작동하지만 한 번만 작동한다
    • slider의 left값이 계속 liWidth 값으로만 되기 때문이다.. 그래서 버튼을 누를때마다 liWidth 값을 누적시켜줄 필요가 있다
  • 그리고 현재 보고 있는 이미지(li)가 몇 번째 이미지 인지 인덱스 값도 설정 해준다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')

// CSSOM 셋업
const liWidth = slideLis[0].clientWidth
const sliderWidth = liWidth * slideLis.length
slider.style.width = sliderWidth + 'px'

// 변수를 추가해준다
let curIndex = 0 // 처음 이미지는 0번째 이미지이기 때문에 0으로 초기화
let moveDist = 0 // liWidth 값이 누적될 변수 처음에는 left 값이 0이므로 0

// 클릭 이벤트
arrow.addEventListener('click', function (e) {
e.preventDefault
if (e.target.className === 'next') {
// class가 next인것을 클릭하면
if (curIndex === slideLis.length - 1) {
// 만약 현재인덱스가 마지막 이미지라면
curIndex = 0 // 인덱스를 다시 0으로 초기화
moveDist = 0 // 이미지가 움직인 값 초기화
slider.style.transform = `translateX(${moveDist}px)` // slider에 transform 에 움직인 거리값을 초기화 해준다
} else {
// 현재 인덱스가 마지막 이미지가 아니면 (0~2 번째 이미지)
moveDist += -liWidth // 움직일 거리를 누적시켜준다 이때 이미지는 왼쪽으로 이동하므로 -값이 된다
slider.style.transform = `translateX(${moveDist}px)` // 누적거리를 transform에 적용
curIndex++ // 인덱스를 1 증가시켜준다
}
}
})
  • 넥스트 버튼을 눌렀을때 현재 인덱스가 몇 번째인지에 따라 동작을 추가해준다
  • curIndex는 0부터 시작해서 마지막 이미지에 가면 3가 된다
  • slideLis의 갯수는 4개 이므로 -1을 해줘야 마지막 숫자가 동일해진다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')

// CSSOM 셋업
const liWidth = slideLis[0].clientWidth
const sliderWidth = liWidth * slideLis.length
slider.style.width = sliderWidth + 'px'

// 변수 초기화
let curIndex = 0
let moveDist = 0
const speedTime = 500

// 클릭 이벤트
arrow.addEventListener('click', function (e) {
e.preventDefault

// next button
if (e.target.className === 'next') {
if (curIndex === slideLis.length - 1) {
curIndex = 0
moveDist = 0
slider.style.transform = `translateX(${moveDist}px)`
} else {
moveDist += -liWidth
slider.style.transform = `translateX(${moveDist}px)`
slider.style.transition = `all ${speedTime}ms ease` // 이미지가 옆으로 넘어가는 것을 보여주기 위해 transition 속성을 사용한다
curIndex++
}
}
// prev button
// class 가 next가 아닌것을 클릭하면
else {
if (curIndex === 0) {
// 현재 인덱스가 0이라면 마지막 이미지로 넘어가야한다
curIndex = slideLis.length - 1 // curIndex를 마지막 이미지의 index로 할당
moveDist = -(liWidth * curIndex) // 마지막 imgage의 left 값이므로 넓이*인덱스값
slider.style.transform = `translateX(${moveDist}px)` // transform 에 실제 적용해주고
slider.style.transition = `all ${speedTime}ms ease` // 넘어가는 효과를 위해 transition 설정
} else {
moveDist += liWidth // 이미지가 오른쪽으로 이동해야 하므로 -값이 아닌 +값을 누적시켜준다
slider.style.transform = `translateX(${moveDist}px)`
curIndex-- // 이전 이미지로 이동하므로 인덱스를 1씩 줄여준다
}
}
})

이렇게 작성하고 나면 문제점이 하나 생긴다 첫번째 이미지에서 이전을 클릭하거나 마지막 이미지에서 다음을 클릭하면
이미지가 역재생(?)되며 돌아간다는 것
이걸 자연스럽게 해주기 위해 트릭을 써야한다
첫 번째 이미지와 맨 마지막 이미지를 복제한 후
1번 앞에는 4번의 복제본을
4번 뒤에는 1번의 복제본을 추가해준다

이 상황에서 4번 이미지일때 다음을 클릭하게 되면 1번 복제본으로 넘어가는 애니메이션 (transition)을 500ms동안 수행한다.
그리고나서 수행이 끝나자마자 원본 이미지인 1번 이미지로 위치를 이동 시킨다.

이미지 클론

첫 번째 이미지는 slideLis[0]으로 직접 지정해 복제해도 상관 없지만,
마지막 이미지는 나중에 이미지가 추가 될 경우를 대비해 배열의 길이에서 -1만큼을 뺀 값으로 지정해 복제해준다

inserBefore(‘추가할노드’,’추가 할 위치’)는 ‘추가 할 위치’의 앞 쪽으로 추가하는 메소드
appendChild(‘추가할노드’)는 그냥 맨 뒤쪽으로 추가된다.

1
2
3
4
5
6
7
8
9
10
11
// 노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')
// 1번과 4번을 복제(clone)해준다
const cloneA = slideLis[0].cloneNode(true)
const cloneD = slideLis[slideLis.length - 1].cloneNode(true)
// 복제한 노드를 원래의 li 앞,뒤로 추가해준다
slider.insertBefore(cloneD, slideLis[0])
slider.appendChild(cloneA)

복제한 이미지를 추가하고 나면 화면이 이렇게 이상해진다..
그 이유는 복제해서 넣기전의 넓이 값을 먼저 가지기 때문이다
그래서 복제한후의 넓이값을 다시 구해줘야한다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')
const cloneA = slideLis[0].cloneNode(true)
const cloneC = slideLis[slideLis.length - 1].cloneNode(true)
slider.insertBefore(cloneC, slideLis[0])
slider.appendChild(cloneA)

// 변수 초기화
let curIndex = 1 // 복제를 해 넣고나면 처음으로 보여줄 이미지의 인덱스는 1이 된다
let moveDist = 0
const speedTime = 500

// CSSOM 셋업
const slideCloneLis = slider.querySelectorAll('li') // 클론 복재후 li들을 다시 변수에 저장한다
const liWidth = slideLis[0].clientWidth
const sliderWidth = liWidth * slideCloneLis.length // 복제한 길이로 ul의 넓이 값을 다시 지정한다
slider.style.width = sliderWidth + 'px'
moveDist = -liWidth // 첫 번째 이미지가 보여야하므로 -liWidth 값으로 재할당 해준다

setTimeout()을 이용해 마지막 이미지에 가서 버튼을 클릭했을때 transition 효과를 0초로 설정하여
넘어가는 이미지효과를 제거한후 이동하는 방식이다

setTimeout

  1. 처음으로 보여줄 이미지가 사실상 두 번째에 있으므로 포지션 초기값을 다시 설정해준다
  2. 버튼을 눌렀을 경우 이미지가 계속 넘어가도록 해준다
  3. setTimeout()함수로 이미지의 위치를 자연스럽게 교체해준다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 노드셋업
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')

const cloneA = slideLis[0].cloneNode(true)
const cloneC = slideLis[slideLis.length - 1].cloneNode(true)
slider.insertBefore(cloneC, slideLis[0])
slider.appendChild(cloneA)

// 변수 초기화
let curIndex = 1
let moveDist = 0
const speedTime = 500

// CSSOM 셋업
const slideCloneLis = slider.querySelectorAll('li')
const liWidth = slideLis[0].clientWidth
const sliderWidth = liWidth * slideCloneLis.length
slider.style.width = sliderWidth + 'px'
slider.style.transform = `translateX(-${liWidth}px)`
moveDist = -liWidth

arrow.addEventListener('click', function (e) {
e.preventDefault
if (e.target.className === 'next') {
// if 실행문이 변경됐다 마지막인덱스에 갔을때 멈추는게 아니라 계속 넘어가도록 실행문을 작성한다
moveDist += -liWidth
curIndex += 1
slider.style.transform = `translateX(${moveDist}px)`
slider.style.transition = `all ${speedTime}ms ease-in-out`
if (curIndex === slideCloneLis.length - 1) {
// 마지막 인덱스에 갔을때 setTimeout()함수를 실행한다
setTimeout(() => {
slider.style.transition = 'all 0ms' // transition 효과를 없애주고
moveDist = -liWidth // 처음의 포지션값으로 돌려준다
curIndex = 1 // index값도 1로 재할당
slider.style.transform = `translateX(${moveDist}px)` // css에 적용해준다
}, speedTime)
}
} else {
moveDist += liWidth
curIndex += -1
slider.style.transform = `translateX(${moveDist}px)`
slider.style.transition = `all ${speedTime}ms ease-in-out`
if (curIndex === 0) {
setTimeout(() => {
slider.style.transition = 'all 0ms' // transition 효과를 없애준다
moveDist = -liWidth * (slideCloneLis.length - 2) // 포지션을 값을 정해준다
curIndex = slideCloneLis.length - 2 // index 값 재할당
slider.style.transform = `translateX(${moveDist}px)` // css에 적용
}, speedTime)
}
}
})
  • prev 버튼을 눌렀을시 헷갈린점
    1. moveDist 값이 원래의 4번 위치로 가야한다
    2. curIndex도 4번이어야한다
  • 현재 이미지의 갯수는 6개이고 원래의 4번이미지의 curIndex 값은 4번이므로 배열의 길이에서 클론한 li의 갯수만큼 빼준다

코드 정리

코드가 길어지고 반복되는 요소가 있으므로 함수로 만들어 코드를 정리해준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//  노드 준비
const kindWrap = document.querySelector('.kind_wrap')
const slider = kindWrap.querySelector('.slider')
const slideLis = slider.querySelectorAll('li')
const arrow = kindWrap.querySelector('.arrow')
const cloneA = slideLis[0].cloneNode(true)
const cloneC = slideLis[slideLis.length - 1].cloneNode(true)
slider.insertBefore(cloneC, slideLis[0])
slider.appendChild(cloneA)

// 변수 초기화
let curIndex = 1
let moveDist = 0
const speedTime = 500

// CSSOM 셋업
const slideCloneLis = slider.querySelectorAll('li')
const liWidth = slideLis[0].clientWidth
const sliderWidth = liWidth * slideCloneLis.length
slider.style.width = sliderWidth + 'px'
moveDist = -liWidth
slider.style.transform = `translateX(-${liWidth}px)`

arrow.addEventListener('click', function (e) {
e.preventDefault
if (e.target.className === 'next') {
move(-1)
if (curIndex === slideCloneLis.length - 1) moveTimeOut(1)
} else {
move(1)
if (curIndex === 0) moveTimeOut(slideCloneLis.length - 2)
}
})

function move(direction) {
moveDist += liWidth * direction
curIndex += -1 * direction
slider.style.transform = `translateX(${moveDist}px)`
slider.style.transition = `all ${speedTime}ms ease-in-out`
}

function moveTimeOut(index) {
setTimeout(() => {
slider.style.transition = 'all 0ms'
moveDist = -liWidth * index
curIndex = index
slider.style.transform = `translateX(${moveDist}px)`
}, speedTime)
}