https://github.com/ChangHyun2/programmers-dev-matching-2021
소스코드 참고 시 출처 표기 부탁드립니다!
무한 루프 지원
자동 재생 지원
슬라이드 width 자동 조절
레이지 로딩 지원
options
// Banner.js
// Banner 컴포넌트 render 함수
async render() {
// 랜덤 고양이 호출
let cats = await this.tryFetchData(api.getRandomCats, {
cb: ({ data }) => data,
cache: false,
errorTypes: ['api'],
showErrorMessage: false,
});
if (!cats) {
return this.render();
}
// cats 데이터를 이용해 HTML 마크업 template literal 생성
const slidesTemplate = cats
.map(
({ url, name, id }) => `
<li class="Carousel-slide" id-${id}>
<div class="img-wrapper lazy">
<img data-src=${url} alt=${name}/>
<div class="img-placeholder"/>
</div>
</li>`
)
.join('');
/*
Carousel 컴포넌트 생성 및 렌더링
1. 위 마크업을 이용해 slides를 렌더링하고
2. 5개씩 보여주며
3. 자동재생 옵션을 통해 2sec마다 다음 슬라이드로 이동
4. transition 속도는 0.3sec
*/
this.$carousel = new Carousel(this.$, {
slidesTemplate,
windowSlideSize: 5,
autoPlay: 2000,
duration: 300,
}).render();
}
이미지 슬라이드(slidesTemplate
) 10개 origin slides를
5개씩(windowSlideSize
) 보여준다고 가정하면
wrapper에서 origin slides의 앞 뒤 5개씩(fallback slides)을 복사해 새롭게 구성한 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 를 $slides로 저장하고
시작 위치가 0이 되게끔 slideTo
(transform3d)를 통해 이동시켜둔다.
prev 클릭 시, index -= 1
후 slideTo(this.index)
next 클릭 시, index += 1
후 slideTo(this.index)
를 이용해 슬라이드를 이동한다.
끝 지점에 도달할 경우, transitionend
를 통해 트랜지션이 끝나는 순간 슬라이드를 origin slides로 맞춰주고
끝 지점의 transitionend
가 아직 발생하지 않았을 떄 버튼이 클릭될 경우, transition
을 0ms
로 만들어 transitionend
를 발생시킨다.
resize발생 시, slide의 translate3d를 재설정한다.
이외 기능은 코드에서 후술
import Component from '../components/Component.js';
import { lazyLoad } from '../utils/index.js';
import { loadImage } from '../utils/lazyLoad.js';
export default class Carousel extends Component {
constructor(
$parent,
{ slidesTemplate, windowSlideSize = 5, autoPlay = 2000, duration = 300 }
) {
super($parent, 'div', {
className: 'Carousel',
styles: {
position: 'relative',
},
innerHTML: `
<button class="Carousel-left-arrow"> < </button>
<ul class="Carousel-slides">
</ul>
<button class="Carousel-right-arrow"> > </button>
`,
});
// 슬라이드를 감싸는 wrapper 요소
this.$slidesWrapper = this.$.querySelector('.Carousel-slides');
// options에서 설정한 기본 config
this.direction = 'next';
this.interval = autoPlay;
this.duration = duration;
this.slidesTemplate = slidesTemplate;
this.windowSlideSize = windowSlideSize;
this.slideWidth = 100 / this.windowSlideSize.toFixed(4) + '%';
// 시작 위치로 이동
this.index = 0;
this.slideTo(this.index);
// 윈도우 크기 리사이징 시 핸들러 실행
window.addEventListener('resize', this.handleWindowResize);
// autoplay 옵션 설정 시 autoplay 실행
autoPlay && this.autoPlay();
this.bindEvents();
}
// transition 효과 on/off
transitionOn() {
this.$slidesWrapper.style.transition = `transform ${this.duration}ms ease-in-out`;
}
transitionOff() {
this.$slidesWrapper.style.transition = '';
}
// transitionEnd 이벤트 유도 함수
forceTransitionEnd() {
this.$slidesWrapper.transition = '0ms';
}
// wrapper.children의 앞 쪽(front)에 슬라이드 개수에 해당되는 fallback 슬라이드가 포함되므로 이를 고려해 위치 이동
slideTo(index) {
const { width } = this.$slidesWrapper.getBoundingClientRect();
this.$slidesWrapper.style.transform = `translate3d(${
-(width / this.windowSlideSize) * (index + this.windowSlideSize)
}px, 0, 0)`;
}
// 이전 슬라이드로 이동
prevSlide() {
// 아직 transitionEnd가 동작하지 않았을 경우 이를 강제하기
if (this.index === -this.windowSlideSize) {
this.forceTransitionEnd();
return;
}
// transition 효과 적용 시킨 후
this.transitionOn();
// 페이지 이동
this.index--;
this.slideTo(this.index);
// wrapper의 맨 앞에 도달할 경우
// transition을 마치는 순간 origin slides의 뒤 쪽으로 이동하기
if (this.index === -this.windowSlideSize) {
this.$slidesWrapper.ontransitionend = () => {
// 자연스럽게 넘어가기 위해 transition 효과 off해두고
this.transitionOff();
// origin slides의 뒤 쪽으로 이동
this.index = this.$slides.length - this.windowSlideSize;
this.slideTo(this.index);
this.$slidesWrapper.ontransitionend = null;
};
}
}
// prev와 동일한 방식
nextSlide() {
if (this.index === this.$slides.length) {
this.forceTransitionEnd();
return;
}
this.transitionOn();
this.index++;
this.slideTo(this.index);
if (this.index === this.$slides.length) {
this.$slidesWrapper.ontransitionend = () => {
this.transitionOff();
this.index = 0;
this.slideTo(this.index);
this.$slidesWrapper.ontransitionend = null;
};
}
}
onClick = (e) => {
if (e.target.type !== 'submit') return;
/*
버튼 클릭에 의한 수동 페이지 이동과 autoplay에 의한 슬라이드 이동이 겹치면 안 됨
버튼 클릭 시, autoplay timeout을 clear하고 슬라이드 이동을 마칠 때 autoplay를 다시 실행
autoTimeout : 슬라이드 인터벌 이동 마킹
resetIntervalTimeout : autoplay를 실행하는 함수 마킹
*/
// autoplay일 경우(interval이 있을 경우)
if (this.interval) {
clearInterval(this.autoTimeout);
this.resetIntervalTimeout && clearInterval(this.resetIntervalTimeout);
}
switch (e.target.className) {
case 'Carousel-left-arrow':
this.direction = 'prev';
this.prevSlide();
break;
case 'Carousel-right-arrow':
this.direction = 'next';
this.nextSlide();
}
if (this.interval) {
this.resetIntervalTimeout = setTimeout(() => this.autoPlay(), this.duration);
}
};
// 리사이징 시 슬라이도 이동 위치 재설정
handleWindowResize = () => {
this.transitionOff();
this.slideTo(this.index);
this.transitionOn();
};
// autoplay 실행 시, interval 간격으로 방향에 따라 페이지 이동
autoPlay() {
this.autoTimeout = setInterval(() => {
this[this.direction + 'Slide']();
}, this.interval);
}
async render() {
this.$slidesWrapper.innerHTML = this.slidesTemplate;
this.$slides = Array.from(this.$slidesWrapper.children);
// fallback slides
const front = this.$slides.slice(0, this.windowSlideSize);
const back = this.$slides.slice(this.$slides.length - this.windowSlideSize);
// add front to end of slides
front.forEach((node) => {
const $slide = node.cloneNode(true);
this.$slidesWrapper.append($slide);
});
// add back to start of slides
back.reverse().forEach((node) => {
// 끝에 위치하는 fallback 슬라이드로 이동하는 순간에 lazyload가 실행되면 flickering이 발생하므로
// lazyload 없이 미리 image를 로딩시켜둠
loadImage(node);
const $slide = node.cloneNode(true);
this.$slidesWrapper.insertAdjacentElement('afterbegin', $slide);
});
lazyLoad({
root: this.$,
rootMargin: '100%',
threshold: 1,
});
}
}