[react 30] #2 이미지 슬라이드

dev__bokyoung·2022년 8월 1일
0
post-thumbnail

프롤로그

이미지 슬라이드를 사용할 때에 실무에서는 주로 라이브러리를 사용하곤 했었다. 이번 강의에서는 자바스크립트 클래스문법으로 구현을 다시 해보는 작업을 가졌다. 역시 라이브러리는 편하지만 직접 작동 방식을 생각하면서 하는게 도움이 훨씬 더 많이 된다.

webpack 개발환경 세팅

지난 포스팅 slider 와 같은 개발환경으로 진행하기로 한다.
webpack 이용한 개발환경 세팅

추가적으로 세팅해야 할 몇몇 사항들이다.

1. 폰트 어썸 설치

npm install --save @fortawesome/fontawesome-free

style.css 상단에 불러와준다.

@import url('~@fortawesome/fontawesome-free/css/all.min.css');

2. webpack 에서 이미지를 처리하는 방식의 설정

웹 팩에서는 이미지를 처리하는 방식을 따로 설정해 주어야 한다고 한다.
webpack.config.js 파일로 들어간다.


module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      //새로 추가하는 부분 - 이미지 처리 방식
      {
        test: /\.jpeg$/, //jpeg파일을 만나면
        type: 'asset/inline', //asset/inline에 있는 파일들을 webpack 에 있는 내장된 로더로 읽어 들이겠다 라는 의미 
      },
    ],
  },

html & css 구조 세팅

코드를 냅다 보여주는 것보다 html 은 사실 박스 구조로 이해를 하는게 더 좋다고 생각하기 때문에 구조를 그려봤다.

웹퍼블리싱 공부를 처음 할때 강사님이 강조했던 것은 코드를 치기 전에 구조를 먼저 생각하라는 점이었다. 박스 구조 짜는 연습을 했던 것이 빠르게 레이아웃을 만드는데 도움이 많이 되었다.

가장 큰 아우터 랩이 slider-wrap 이라고 할 수 있다. 작게 그려놨지만 전체적인 구조를 보면 저렇게 div 로 감싸고 있는 큰 아우터가 존재하고, 그 안에 각각의 요소를 감싸주는 ul 랩과 각각의 요소들인 li 들이 존재한다.
그리고 화면에 보여지는 것은 파란색부분(가장 큰 아우터 랩) 이 될 것이다.

요소가 나열되어 있어야 슬라이더들이 옆으로 움직일때 자연스러운 움직임이 가능하기 때문이다. 슬라이더들이 나열되고, 그 위에 position 으로 화살표, play 버튼, indicator 들을 위치 시켰다.

위 이미지 처럼 박스구조를 이해하고 나서 코드를 보자!
훨씬 더 이해가 잘 될 것이다.


<!DOCTYPE html>
<html>
  <head>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <div class="slider-wrap" id="slider-wrap">
      <ul class="list slider" id="slider">
        <li>
          <img src="<%= require('./src/image/red.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/orange.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/yellow.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/green.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/blue.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/indigo.jpeg') %>" />
        </li>
        <li>
          <img src="<%= require('./src/image/violet.jpeg') %>" />
        </li>
      </ul>

      <div class="btn next" id="next"><i class="fa fa-arrow-right"></i></div>
      <div class="btn previous" id="previous">
        <i class="fa fa-arrow-left"></i>
      </div>

      <div class="indicator-wrap" id="indicator-wrap">
        <ul>
          <!-- <li class="active"></li>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
          <li></li>
          <li></li> -->
        </ul>
      </div>

      <div class="control-wrap play" id="control-wrap">
        <i class="fa fa-pause" id="pause" data-status="pause"></i>
        <i class="fa fa-play" id="play" data-status="play"></i>
      </div>
    </div>
  </body>
</html>

구현해야 할 기능들

기능단위로 나누어서 코드를 구현하였다. 하나하나 다시 살펴보자.

  1. next, prev 버튼 작동
  2. 인디케이터
  3. autoplay 기능

1. next, prev 버튼 구현

이미지 슬라이드의 원리는 간단하다.
next 버튼을 누르면 슬라이드요소들이 모두 한칸씩 왼쪽으로 위치 이동하는 것이다. 그렇게 되면 필요한게 전체 슬라이드의 갯수슬라이드 한개의 길이 이다.

  • assignElement()에 각각의 요소들을 잡아준다.
  • addEvent() 에는 이벤트를 실행 시켜 주는 함수를 만들어주고, this를 바인딩 시켜준다.
  • moveToRight(), moveToLeft() 를 만들어 각각 해당 길이 만큼 위치를 이동할 수 있도록 한다.
  • initSliderNumber(), initSliderWidth(), initSliderListWidth() 는 해당 슬라이드에 대한 전체 길이나 갯수 등을 초기화 하는 함수이다.
export default class ImageSlider {
  #currentPosition = 0;
  #slideNumber = 0;
  #slideWidth = 0;
  
  sliderWrapEl;
  sliderListEl;
  nextBtnEl;
  previousBtnEl;

  constructor() {
    this.assignElement();
    this.initSliderNumber();
    this.initSliderWidth();
    this.initSLiderListWidth();
    this.addEvent();
    
  }

  assignElement() {
    this.sliderWrapEl = document.getElementById('slider-wrap');
    this.sliderListEl = this.sliderWrapEl.querySelector('#slider');
    this.nextBtnEl = this.sliderWrapEl.querySelector('#next');
    this.previousBtnEl = this.sliderWrapEl.querySelector('#previous');
  }

//슬라이더 갯수 
  initSliderNumber() {
    this.#slideNumber = this.sliderListEl.querySelectorAll('li').length;
  }

//슬라이더 가로값
  initSliderWidth() {
    this.#slideWidth = this.sliderListEl.clientWidth;
  }

//전체 감싸고 있는 슬라이더 아우터 가로값
  initSLiderListWidth() {
    this.sliderListEl.style.width = `${this.#slideNumber * this.#slideWidth}px`;
  }

  addEvent() {
    this.nextBtnEl.addEventListener('click', this.moveToRight.bind(this));
    this.previousBtnEl.addEventListener('click', this.moveToLeft.bind(this));
  }
    
 
// 오른쪽 버튼 눌렀을 시, 왼쪽으로 한칸 씩 이동해 준다. 그리고 마지막 슬라이더라면 초기화 위치값을 초기화시켜준다.
  moveToRight() {
    this.#currentPosition += 1;
    if (this.#currentPosition === this.#slideNumber) {
      this.#currentPosition = 0;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }

// 왼쪽 버튼 눌렀을 시 오른쪽으로 한칸씩 이동해준다. 그리고 마지막 슬라이더라면 초기화 위치값을 초기화시켜준다.
  moveToLeft() {
    this.#currentPosition -= 1;
    if (this.#currentPosition === -1) {
      this.#currentPosition = this.#slideNumber - 1;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }



}

2. 인디케이터

인디케이터는 지표라고 생각하면 된다. 해당 번호를 누르면 그에 해당하는 슬라이더가 나타 날 수 있게 한다.

  • 인디케이터들을 슬라이더갯수에 따라 만들고
  • 활성화 되었을때 어떻게 될지 style을 구현한다.
  • 그리고 클릭 했을때의 이벤트 변화를 구현한다.

export default class ImageSlider {
  #currentPosition = 0;
  #slideNumber = 0;
  #slideWidth = 0;
  
  sliderWrapEl;
  sliderListEl;
  nextBtnEl;
  previousBtnEl;

  indeicaterWrapEl;
  controlWrapEl;

  constructor() {
    this.assignElement();
    this.initSliderNumber();
    this.initSliderWidth();
    this.initSLiderListWidth();
    this.addEvent();
    this.createIndicater();
    this.setIndicator();
    
  }

  assignElement() {
    this.sliderWrapEl = document.getElementById('slider-wrap');
    this.sliderListEl = this.sliderWrapEl.querySelector('#slider');
    this.nextBtnEl = this.sliderWrapEl.querySelector('#next');
    this.previousBtnEl = this.sliderWrapEl.querySelector('#previous');
    this.indeicaterWrapEl = this.sliderWrapEl.querySelector('#indicator-wrap');
    this.controlWrapEl = this.sliderWrapEl.querySelector('#control-wrap');
    
  }


  initSliderNumber() {
    this.#slideNumber = this.sliderListEl.querySelectorAll('li').length;
  }

  initSliderWidth() {
    this.#slideWidth = this.sliderListEl.clientWidth;
  }

  initSLiderListWidth() {
    this.sliderListEl.style.width = `${this.#slideNumber * this.#slideWidth}px`;
  }

  addEvent() {
    this.nextBtnEl.addEventListener('click', this.moveToRight.bind(this));
    this.previousBtnEl.addEventListener('click', this.moveToLeft.bind(this));
     this.indeicaterWrapEl.addEventListener(
      'click',
      this.onClickIndicator.bind(this),
    );
  }
    

// 인디케이터 클릭 시 작동 
onClickIndicator(event) {
    const indexPosition = parseInt(event.target.dataset.index, 10);
    if (Number.isInteger(indexPosition)) {
      this.#currentPosition = indexPosition;
      this.sliderListEl.style.left = `-${
        this.#slideWidth * this.#currentPosition
      }px`;
      this.setIndicator();
    }
    console.log(indexPosition);
  }
 
  moveToRight() {
    this.#currentPosition += 1;
    if (this.#currentPosition === this.#slideNumber) {
      this.#currentPosition = 0;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }

  moveToLeft() {
    this.#currentPosition -= 1;
    if (this.#currentPosition === -1) {
      this.#currentPosition = this.#slideNumber - 1;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;
  }


// 인디케이터 생성 - 슬라이더 갯수에 따라 만들어주기
createIndicater() {
    const docFragment = document.createDocumentFragment();
    for (let i = 0; i < this.#slideNumber; i += 1) {
      const li = document.createElement('li');
      li.dataset.index = i;
      docFragment.appendChild(li);
      this.indeicaterWrapEl.querySelector('ul').appendChild(docFragment);
    }
  }

//인디케이터 세팅 - 활성화 되었을 시 세팅하기
  setIndicator() {
    this.indeicaterWrapEl
      .querySelector('li.active')
      ?.classList.remove('active');

    this.indeicaterWrapEl
      .querySelector(`ul li:nth-child(${this.#currentPosition + 1})`)
      .classList.add('active');
  }



}


3. 자동플레이 버튼 구현

대부분의 슬라이더는 사용자가 건들이지 않아도 자동으로 슬라이드가 진행된다. 만약 사용자가 일시정지 버튼을 누르면 이미지가 멈추고, 다시 재생하면 슬라이드가 재생되야하는데 여기서 생각해야 할 것은

  • setInterval을 시켜줘 무한 재생이지만

  • 일시정지를 누르면 clearInterval 을 시켜준다.

  • 인디케이터도 그에 대응해 같이 따라와야하고,

  • 무한재생 되고 있을 때 인디케이터 버튼을 누르면 clear 를 시켜줘야 한다.

    아래는 전체 script이다.


export default class ImageSlider {
  #currentPosition = 0;
  #slideNumber = 0;
  #slideWidth = 0;
  #intervalId;
  #autoPlay = true;

  sliderWrapEl;
  sliderListEl;
  nextBtnEl;
  previousBtnEl;
  indeicaterWrapEl;
  controlWrapEl;

  constructor() {
    this.assignElement();
    this.initSliderNumber();
    this.initSliderWidth();
    this.initSLiderListWidth();
    this.addEvent();
    this.createIndicater();
    this.setIndicator();
    this.initAutoPlay();
  }

  assignElement() {
    this.sliderWrapEl = document.getElementById('slider-wrap');
    this.sliderListEl = this.sliderWrapEl.querySelector('#slider');
    this.nextBtnEl = this.sliderWrapEl.querySelector('#next');
    this.previousBtnEl = this.sliderWrapEl.querySelector('#previous');
    this.indeicaterWrapEl = this.sliderWrapEl.querySelector('#indicator-wrap');
    this.controlWrapEl = this.sliderWrapEl.querySelector('#control-wrap');
  }

  initAutoPlay() {
    this.#intervalId = setInterval(this.moveToRight.bind(this), 3000);
  }

  initSliderNumber() {
    this.#slideNumber = this.sliderListEl.querySelectorAll('li').length;
  }

  initSliderWidth() {
    this.#slideWidth = this.sliderListEl.clientWidth;
  }

  initSLiderListWidth() {
    this.sliderListEl.style.width = `${this.#slideNumber * this.#slideWidth}px`;
  }

  addEvent() {
    this.nextBtnEl.addEventListener('click', this.moveToRight.bind(this));
    this.previousBtnEl.addEventListener('click', this.moveToLeft.bind(this));
    this.indeicaterWrapEl.addEventListener(
      'click',
      this.onClickIndicator.bind(this),
    );
    this.controlWrapEl.addEventListener('click', this.togglePlay.bind(this));
  }

// 해당 버튼 플레이 & 일시정지 변경 
  togglePlay(event) {
    if (event.target.dataset.status === 'play') {
      this.#autoPlay = true;
      this.controlWrapEl.classList.add('play');
      this.controlWrapEl.classList.remove('pause');
      this.initAutoPlay();
    } else if (event.target.dataset.status === 'pause') {
      this.#autoPlay = false;
      this.controlWrapEl.classList.remove('play');
      this.controlWrapEl.classList.add('pause');
      clearInterval(this.#intervalId);
    }
  }

  onClickIndicator(event) {
    const indexPosition = parseInt(event.target.dataset.index, 10);
    if (Number.isInteger(indexPosition)) {
      this.#currentPosition = indexPosition;
      this.sliderListEl.style.left = `-${
        this.#slideWidth * this.#currentPosition
      }px`;
      this.setIndicator();
    }
    console.log(indexPosition);
  }

  moveToRight() {
    this.#currentPosition += 1;
    if (this.#currentPosition === this.#slideNumber) {
      this.#currentPosition = 0;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;

// 자동재생에 대한 조건문을 걸어준다.
    if (this.#autoPlay) {
      clearInterval(this.#intervalId);
      this.#intervalId = setInterval(this.moveToRight.bind(this), 3000);
    }

    this.setIndicator();
  }

  moveToLeft() {
    this.#currentPosition -= 1;
    if (this.#currentPosition === -1) {
      this.#currentPosition = this.#slideNumber - 1;
    }
    this.sliderListEl.style.left = `-${
      this.#slideWidth * this.#currentPosition
    }px`;

//자동재생에 대한 조건문을 걸어준다.
    if (this.#autoPlay) {
      clearInterval(this.#intervalId);
      this.#intervalId = setInterval(this.moveToRight.bind(this), 3000);
    }

    this.setIndicator();
  }

  createIndicater() {
    const docFragment = document.createDocumentFragment();
    for (let i = 0; i < this.#slideNumber; i += 1) {
      const li = document.createElement('li');
      li.dataset.index = i;
      docFragment.appendChild(li);
      this.indeicaterWrapEl.querySelector('ul').appendChild(docFragment);
    }
  }

  setIndicator() {
    this.indeicaterWrapEl
      .querySelector('li.active')
      ?.classList.remove('active');

    this.indeicaterWrapEl
      .querySelector(`ul li:nth-child(${this.#currentPosition + 1})`)
      .classList.add('active');
  }
}

에필로그

이미지 슬라이드는 퍼블리싱 공부할때 직접 손코딩도 해보고 기능도 나눠서 생각해보고 했던 기억이 있다. 그래서 그런지 로직이 머릿속에 남아 있다. 확실히 내가 직접 짜보고 생각해보면 그 과정이 머릿속에 더 오래 남는가 보다.

좋았던 부분은 라이브러리나 jqeury 사용을 안했다는 점과 자바스크립트 또한 클래스문법이라는 나에겐 새로운 방식으로 코드를 구현했다는 점이다.
이번에는 클래스 문법을 조금 더 자세하게 공부하며 분석했다. 클래스 문법이 조금더 직관적인 것 같아 조금 더 익숙해 진다면 실무에서도 적용해 보고 싶다.

아쉬웠던 부분은 이러하다.
슬라이드들이 이동하다가 오른쪽 끝에 다다랐을때 슬라이드들이 와다다다 하며 왼쪽으로 가도록 코드가 짜여있다. 하지만 그 부분은 자연스럽지 못하다고 생각한다. 이동될때 마다 반대쪽 맨 끝에 있는 슬라이드가 바로 바로 위치 이동을 해서 끝에 달라 붙었다면 훨씬 더 매끄러운 움직임이 나타났을 것이다.

profile
개발하며 얻은 인사이트들을 공유합니다.

0개의 댓글