https://github.com/ChangHyun2/programmers-dev-matching-2021

소스코드 참고 시 출처 표기 부탁드립니다!

무한 캐러셀

무한 루프 지원
자동 재생 지원

  • 자동 재생과 슬라이드 이동 버튼 간의 간섭 X
  • 버튼으로 슬라이드 이동 시 이동 방향으로 자동 재생 방향 전환

슬라이드 width 자동 조절

  • carousel wrapper 크기와 보여줄 슬라이드 개수에 기반해 슬라이드 크기 자동 조절
  • 윈도우 리사이징 지원

레이지 로딩 지원

  • wrapper의 너비에 해당되는 prev/next slides 까지만 미리 로딩

options

  • autoPlay : 몇 ms 마다 자동 재생할 것인지
  • duration : 몇 ms 동안 transition을 발생시킬 것인진
  • windowSlideSize : 슬라이드를 몇 개씩 보여줄 것인지
  • slidesTemplate : 보여줄 슬라이드 HTML 마크업

캐러셀 사용 예시


// 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 -= 1slideTo(this.index)
next 클릭 시, index += 1slideTo(this.index)를 이용해 슬라이드를 이동한다.

끝 지점에 도달할 경우, transitionend를 통해 트랜지션이 끝나는 순간 슬라이드를 origin slides로 맞춰주고
끝 지점의 transitionend가 아직 발생하지 않았을 떄 버튼이 클릭될 경우, transition0ms로 만들어 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,
    });
  }
}

참고 자료 : https://im-developer.tistory.com/97

profile
띵가띵가

0개의 댓글