본문 바로가기
개발/프로젝트

Project 2 : 이미지 슬라이더(1) - 개발 완료

by p_human 2022. 8. 24.

이전에 했던 가상 키보드 보다는 쉬웠다.

원래는 강의만 보고 따라하면 1시간도 걸리지 않는 프로젝트인데, 최대한 안 보고 기능만 똑같이 구현하도록 노력했다.

그래서 좀 시간이 걸렸다. 갑자기 든 생각이 내가 개발하는 방식은 먼저 어떻게 기능을 구현할지에 대해서 떠올릴 때까지 멍 때리면서 생각을 한다... 그리고 생각을 바탕으로 검색을 하거나 직접 구현을 해본다. 이게 맞는 것일까..?

 

그래도 이런식으로 반복하면 시간은 꽤 걸리지만 기분은 좋다. 내가 스스로 짠 코드고, 그에 맞는 코드를 찾아서 적용시켜서 부드럽게 돌아가는 프로그램을 만들었으니...

 

어쨌든 내가 생각했을 때는 최대한 깔끔하게 작성한 코드인 것 같다.

설명은 다 해놨으니 모르는게 있다면 댓글 달아주세요.

// src/js/ImageSlider.js
import ScheduleManager from './scheduleManager';

export default class ImageSlider {
  #schduler;
  #currentPosition;
  #sliderWrap;
  #slider;
  #autoBtn;
  #indicatorWrap;
  #nextBtn;
  #prevBtn;
  #slideWidth;
  #positionMax;

  constructor() {
    this.#currentPosition = 0;
    this.#schduler = new ScheduleManager(
      this.#intervalClickEvent.bind(this),
      2.5,
    );
    this.#autoSlider(true);
    this.assignElement();
    this.addEvent();
  }

  assignElement() {
    this.#sliderWrap = document.querySelector('.slider-wrap');
    this.#slider = document.getElementById('slider');
    this.#autoBtn = document.getElementById('control-wrap');
    this.#indicatorWrap = this.#sliderWrap.querySelector('.indicator-wrap ul');
    this.#nextBtn = this.#sliderWrap.querySelector('#next');
    this.#prevBtn = this.#sliderWrap.querySelector('#previous');
    this.#slideWidth = this.#slider.clientWidth;
    this.#positionMax = this.#slider.childElementCount;
  }

  // 이벤트를 연결해주는 함수
  addEvent() {
    this.#autoBtn.addEventListener('click', this.onClickAutoBtn.bind(this));
    this.#nextBtn.addEventListener('click', this.nextSlide.bind(this));
    this.#prevBtn.addEventListener('click', this.previousSlide.bind(this));
    [...this.#indicatorWrap.children].forEach(indicator =>
      indicator.addEventListener('click', this.#onClickIndicatorBtn.bind(this)),
    );
  }

  // 현재 위치에서 1을 더해주는 함수
  #addPosition() {
    this.#currentPosition++;
    if (this.#currentPosition >= this.#positionMax) {
      this.#currentPosition = 0;
      return;
    }
  }

  // 현재 위치에서 1을 빼주는 함수
  #subtractPosition() {
    this.#currentPosition--;
    if (this.#currentPosition < 0) {
      this.#currentPosition = this.#positionMax - 1;
      return;
    }
  }

  // 시작, 정지 버튼에 관련한 이벤트 함수
  onClickAutoBtn(event) {
    const controlWrap = event.target.closest('div');
    const isAuto = !controlWrap.classList.toggle('view');
    this.#autoSlider(isAuto);
  }

  // 이미지가 자동으로 슬라이딩 되는지에 대한 여부를 판별하는 함수
  // 해당 함수는 생성자 함수가 실행될 때 같이 실행이 되야 한다.
  // 그 이유는 페이지에 처음 들어갔을 때 자동으로 실행이 되어야 하기 때문이다.
  #autoSlider(isAuto) {
    if (isAuto) {
      this.#schduler.begin();
    } else {
      this.#schduler.end();
    }
  }

  // 일정 시간마다 클릭이벤트가 자동으로 호출되는 함수
  // 최대한 기능이 중복되지 않도록 따로 함수를 분리했고,
  #intervalClickEvent() {
    const mouseClick = new MouseEvent('click', {
      view: window,
    });
    this.#nextBtn.dispatchEvent(mouseClick);
  }

  // indicator 관련 함수
  // 클릭 및 자동으로 넘어가는 기능에 필요한 함수
  #updateIndicator() {
    // indicator의 전체를 구한 다음 active 효과를 삭제
    // 그 다음 현재 위치에 맞는 요소에 대한 active 효과를 적용
    const listNodes = this.#indicatorWrap.children;
    [...listNodes].forEach(list => list.classList.remove('active'));
    listNodes[this.#currentPosition].classList.add('active');
    // 스타일 설정
    this.#slider.style.left = `-${this.#currentPosition * this.#slideWidth}px`;
  }

  // indicator를 클릭했을 때 직접 발생되는 이벤트 함수
  #onClickIndicatorBtn(event) {
    // html에 설정된 data 속성의 값을 가져온 뒤 현재 위치값으로 덮어쓴다.
    const targetIndex = event.target.dataset.index;
    this.#currentPosition = targetIndex;
    this.#updateIndicator();
    // 이 함수를 실행한 이유는 자동으로 이미지가 넘어가고 있는 도중, 시간이
    // 별로 남지 않았을 때 누르게 되면 클릭한 순간에 이미지가 바로 넘어가기 때문에
    // 타이머를 리셋하고 다시 시작하는 방식으로 구현
    this.#schduler.reset();
  }

  // 다음 버튼을 누르면 다음으로 넘어가는 함수
  nextSlide() {
    this.#addPosition();
    this.#updateIndicator();
  }

  // 이전 버튼을 누르면 이전으로 넘어가는 함수
  previousSlide() {
    this.#subtractPosition();
    this.#updateIndicator();
  }
}

다음은 scheduleManager인데, ImageSlider에서 좀 더 편하게 사용할 수 있도록 따로 만들었다. 딱히 특별한 부분은 없다.

export default class ScheduleManager {
  #timerId;
  #func;
  #second;
  constructor(intervalFunc, second) {
    this.#timerId = 0;
    this.#func = intervalFunc;
    this.#second = second;
  }

  begin() {
    this.#timerId = setInterval(this.#func, this.#second * 1000);
  }

  end() {
    clearInterval(this.#timerId);
  }

  reset() {
    this.end();
    this.begin();
  }
}

 

다음은 HTML 파일이다. 강의에서 제공해준 것과 거의 비슷하지만 좀 더 편하게 구현하기 위해서 indicator 부분에 data 속성을 추가했다.

<!DOCTYPE html>
<html>
  <head> </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" data-index="0"></li>
          <li data-index="1"></li>
          <li data-index="2"></li>
          <li data-index="3"></li>
          <li data-index="4"></li>
          <li data-index="5"></li>
          <li data-index="6"></li>
        </ul>
      </div>

      <div class="control-wrap" 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>

다음은 CSS파일이다. 그냥 보면서 HTML요소를 어떻게 배치시켰는지에 대해서만 보면 끝난다.

CSS를 공부하면서 헷갈리는 것 중에 하나가 위치인데, 다음에 한 번 제대로 공부 해야겠다.

@import url('~@fortawesome/fontawesome-free/css/all.min.css');
* {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ! */
.slider-wrap {
  width: 1000px;
  height: 400px;
  margin: 50px auto;
  position: relative;
  overflow: hidden;
}
/* ! */
.slider-wrap ul.slider {
  left: 0;
  display: flex;
  width: 100%;
  height: 100%;
  position: absolute;
}
/* ! */
.slider-wrap ul.slider li {
  float: left;
  width: 1000px;
  height: 400px;
}
/* ! */
.btn {
  position: absolute;
  width: 50px;
  height: 60px;
  top: 50%;
  margin-top: -25px;
  line-height: 57px;
  text-align: center;
  cursor: pointer;
  background: rgba(0, 0, 0, 0.1);
  z-index: 100;
  user-select: none;
  transition: 0.1s;
}

.btn:hover {
  background: rgba(0, 0, 0, 0.3);
}
/* ! */
.next {
  right: -50px;
  border-radius: 7px 0px 0px 7px;
  color: white;
}
/* ! */
.previous {
  left: -50px;
  border-radius: 0px 7px 7px 7px;
  color: white;
}
/* ! */
.slider-wrap:hover .next {
  right: 0px;
}
/* ! */
.slider-wrap:hover .previous {
  left: 0px;
}

.indicator-wrap {
  height: 15px;
  position: relative;
  text-align: center;
  min-width: 20px;
  margin-top: 350px;
  margin-left: auto;
  margin-right: auto;
}

.indicator-wrap ul {
  width: 100%;
}

.indicator-wrap ul li {
  border-radius: 50%;
  background: #fff;
  opacity: 0.5;
  position: relative;
  top: 0;
  cursor: pointer;
  margin: 0 4px;
  display: inline-block;
  width: 15px;
  height: 15px;
}

.indicator-wrap ul li.active {
  width: 15px;
  height: 15px;
  opacity: 1;
}

.slider-wrap ul {
  transition: 0.4s;
}

.control-wrap {
  top: 350px;
  right: 35px;
  width: auto;
  position: absolute;
}

.control-wrap i {
  color: white;
  cursor: pointer;
}

.fa-play {
  display: none;
}

.fa-pause {
  display: block;
}

.view .fa-play {
  display: block;
}

.view .fa-pause {
  display: none;
}

코드는 여기까지이다.