[javascript] Slide Animation Plugin 만들기

javascript slide animation plugin

이번에는 javascript slide animation plugin을 제작해보겠습니다.

  1. 추상화 과정
  2. 설계 과정
  3. 구현
  4. 모듈화

해당 포스트는 [javascript] slide animation 만들기에서 이어지는 포스트입니다.


추상화 과정

플러그인을 만들 때는 먼저 추상화 과정이 필요합니다. 일단 slide 제작은 총 3 part로 구성됩니다.

  1. 기본 변수 설정
  2. 슬라이드에 맞는 스타일(css) 설정
  3. 슬라이드 Play 설정

실제 코드를 보고 part 를 확인해보겠습니다.

// 기본 library function
const all = ele => document.querySelectorAll(ele)
const one = ele => document.querySelector(ele)

// slide animation
const slide = _ => {
  // part 1) variable setting
  const wrap = one('.slide')
  const target = wrap.children[0]
  const len = target.children.length

  // part 2) style setting
  const liStyle = `position:absolute;left:0;right:0;top:0;bottom:0;transition:1s;opacity:0`
  target.style.cssText = `position:relative;`
  Array.from(target.children).forEach(ele => ele.style.cssText = liStyle)
  target.children[0].style.opacity = 1

  let pos = 0
  setInterval(_ => {
    // part 3) slide play
    target.children[pos].style.opacity = 0
    pos = (pos + 1) % len 
    target.children[pos].style.opacity = 1
  }, 1500)
}

// slide play
window.onload = function () {
  slide()
}


이것을 다시 function으로 간단하게 추상화 하면 이렇습니다.

const slide = _ => {
  init()       // part 1) variable setting
  styleSet()   // part 2) style setting
  play()       // part 3) slide play
}

이제 추상화 한 코드를 작성하면 됩니다.


설계 과정

기본적으로 설계는 도메인 모델(알고리즘)과 네이티브 모델(렌더링) 으로 구분합니다. 도메인 모델은 어느 프로그램에서나 사용할 수 있는 것을 말하며, 네이티브 모델은 언어/플랫폼/OS 등의 환경 마다 달라지는 부분을 말합니다.


객체지향의 설계에 대해 자세히 나와있는 동영상 강의가 있습니다. 꼭 시간 내서 공부해보세요

링크주소 : https://www.youtube.com/watch?v=_tmIikzjvOk


우리도 한 번 slide에 대한 설계를 해봅시다.

// 슬라이드 타임 마다 실행할 클래스 목록
const slideType = {}

// 슬라이드 클래스
const Slide = class {

  // 생성자
  constructor () {
    this.variableSet()
    this.styleSet()
    this.play()
  }

  // 변수 세팅
  variableSet () {}

  // 스타일 세팅
  styleSet () {}

  // 슬라이드 플레이
  slidePlay () { }


  static init () {
    /* 슬라이드 객체 마다 기본 설정을 지정합니다. */
  }
}

const SlideWidth = class extends Slide {
  constructor () { super() }
  variableSet () {}
  styleSet () {}
  slidePlay () {}
}

const SlideHeight = class extends Slide {
  constructor () { super() }
  variableSet () {}
  styleSet () {}
  slidePlay () {}
}

const SlideFade = class extends Slide {
  constructor () { super() }
  variableSet () {}
  styleSet () {}
  slidePlay () {}
}

이렇게 하는 것이 100% 정확한 설계라고 할 순 없습니다. 이 포스트를 따라서 해보고, 가능 하다면 스스로 한 번 만들어 보는 것을 추천합니다.

구현

먼저 뼈대 (HTML + CSS)를 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Slide</title>
<style>
  *{margin:0;padding:0;}
  ul,li{list-style:none;}
  .slide{height:150px;overflow:hidden;margin:20px;}
  .slide ul{height:100%;}
  .slide li{height:100%;}
  .slide li:nth-child(1){background:#faa;}
  .slide li:nth-child(2){background:#afa;}
  .slide li:nth-child(3){background:#aaf;}
  .slide li:nth-child(4){background:#faf;}
</style>
<script src="js/junil.slide.js"></script>
<script src="js/junil.common.js"></script>
</head>
<body>
<div class="slide" data-type="SlideWidth" data-playTime="500" data-setTime="1500">
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>
<div class="slide" data-type="SlideHeight" data-playTime="1000" data-setTime="2000">
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>
<div class="slide" data-type="SlideFade" data-playTime="500" data-setTime="1700">
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>
</body>
</html>

총 3개의 slide class가 존재하며, 이것들이 모두 Slide가 실행되도록 만들 것입니다.

slideType Object

전역 변수인 const slideType = {} 부분 입니다.

// 슬라이드 타임 마다 실행할 클래스 목록
const slideType = { SlideWidth, SlideHeight, SlideFade }

const로 변수를 할당할 경우 window object에 할당 되지 않습니다. 따라서 이처럼 별도의 변수를 만들어 사용해야 합니다.


Slide Class

먼저 const Slide = class {} 부분의 구현 코드입니다.

staic init()

  static init () {
    // .slide 의 갯수 만큼 Slide Type에 맞는 Instance를 생성합니다.
    // dataset은 data-type 같이 data-로 시작하는 속성을 가져옵니다.
    all('.slide').forEach(ele => new slideType[ele.dataset.type](ele))
  }

constructor()

  // 생성자
  constructor (ele) {
    // 변수 세팅
    this.variableSet(ele) // ele를 매개변수로 넘겨 사용합니다.
    // 스타일 세팅
    this.styleSet()
    // 실행
    this.play()
  }

styleSet(), playBefore(), playAfter()

// 빈 메소드로 만듭니다.
// Slide Class를 상속하여 사용할 때
// 아래의 method들이 없더라도 오류다 발생하지 않도록 하는 역할입니다
styleSet () {}
playBefore () {}
playAfter () {}




SlideWidth, SlideHeight, SlideFade > constructor()

Slide를 상속하여 사용하는 모든 Class의 contructor()에 공통적으로 적용하는 내용입니다.

// extends하여 사용할 경우
// 항상 부모의 constructor(=super)를 실행해줘야 합니다.
constructor (ele) { super(ele) }




SlideWidth

여기서부터는 class SlideWidth = class extends Slide {} 부분입니다.

styleSet()

styleSet () {
  // ul의 가로 너비가 100% * 슬라이드 장면 갯수
  this.target.style.cssText = `width:calc(100% * ${this.len});display:flex;transition:${this.playTime}s`

  // li의 가로 너비가 100% / 슬라이드 장면 갯수 = 슬라이드의 가로 너비
  Array.from(this.target.children).forEach(ele => ele.style.cssText = `width:calc(100% / ${this.len})`)
}

playAfter()

playAfter () {
  // 장면 이동
  this.target.style.marginLeft = `${-this.pos * 100}%`
}



SlideHeight

여기서부터는 class SlideHeight = class extends Slide {} 부분입니다.

styleSet()

styleSet () {
  // ul의 세로 너비가 100% * 슬라이드 장면 갯수
  this.target.style.cssText = `height:calc(100% * ${this.len});transition:${this.playTime}s`

  // li의 세로 너비가 100% / 슬라이드 장면 갯수 = 슬라이드의 가로 너비
  Array.from(this.target.children).forEach(ele => ele.style.cssText = `height:calc(100% / ${this.len})`)
}

playAfter()

playAfter () {
  // 장면 이동
  this.target.style.marginTop = `${-this.pos * this.h}px`
}




SlideFade

여기서부터는 class SlideFade = class extends Slide {} 부분입니다.

styleSet()

styleSet () {
  this.target.style.cssText = `position:relative;`
  const liStyle = `position:absolute;left:0;right:0;top:0;bottom:0;transition:${this.playTime}s;opacity:0;`
  Array.from(this.target.children).forEach(ele => ele.style.cssText = liStyle)
  this.target.children[0].style.opacity = 1
}

playBefore(), playAfter()

fade의 경우 before와 after가 필요합니다.

playBefore () { this.target.children[this.pos].style.opacity = 0 }
playAfter () { this.target.children[this.pos].style.opacity = 1 }




Slide Plugin 최종 코드입니다.

junil.common.js

// 기본 함수
const all = ele => document.querySelectorAll(ele)
const one = ele => document.querySelector(ele)

// 사이트 로드시 실행
window.onload = function () {
  Slide.init()
}

junil.slide.js

// 슬라이드 클래스
const Slide = class {

  // 생성자
  constructor (ele) {
    this.variableSet(ele)
    this.styleSet()
    this.play()
  }

  // 변수 세팅
  variableSet (ele) {
    this.pos = 0
    this.target = ele.children[0] // .slide ul 선택
    this.len = this.target.children.length // .slide li의 갯수
    this.playTime = parseFloat(ele.dataset.playtime) / 1000 // transition time
    this.setTime = ~~ele.dataset.settime // setInterval 주기, ~~ 연산은 parseInt와 같음
    this.h = ele.clientHeight // 세로 너비
  }

  // 빈 메소드를 구현하여, 자식 Class에서 실행하더라도 오류가 발생하지 않도록 한다.
  styleSet () {}
  playBefore () {}
  playAfter () {}

  // 슬라이드 플레이
  play () {
    setInterval(_ => {
      // 장면 전환 전에 실행 할 구문
      this.playBefore()

      // 장면 선택
      this.pos = (this.pos + 1) % this.len

      // 장면 전환
      this.playAfter()
    }, this.setTime)
  }


  static init () {
    // .slide 의 갯수 만큼 Slide Type에 맞는 Instance를 생성합니다.
    all('.slide').forEach(ele => new slideType[ele.dataset.type](ele))
  }
}

// 가로 슬라이드
const SlideWidth = class extends Slide {
  // extends하여 사용할 경우 항상 부모의 constructor(=super)를 실행해줘야 한다.
  constructor (ele) { super(ele) }
  styleSet () {
    // ul의 가로 너비가 100% * 슬라이드 장면 갯수
    this.target.style.cssText = `width:calc(100% * ${this.len});display:flex;transition:${this.playTime}s`

    // li의 가로 너비가 100% / 슬라이드 장면 갯수 = 슬라이드의 가로 너비
    Array.from(this.target.children).forEach(ele => ele.style.cssText = `width:calc(100% / ${this.len})`)
  }
  playAfter () {
    // 장면 이동
    this.target.style.marginLeft = `${-this.pos * 100}%`
  }
}

// 세로 슬라이드
const SlideHeight = class extends Slide {
  constructor (ele) { super(ele) }
  styleSet () {
    // ul의 세로 너비가 100% * 슬라이드 장면 갯수
    this.target.style.cssText = `height:calc(100% * ${this.len});transition:${this.playTime}s`

    // li의 세로 너비가 100% / 슬라이드 장면 갯수 = 슬라이드의 가로 너비
    Array.from(this.target.children).forEach(ele => ele.style.cssText = `height:calc(100% / ${this.len})`)
  }
  playAfter () {
    // 장면 이동
    this.target.style.marginTop = `${-this.pos * this.h}px`
  }
}

// 페이드 슬라이드
const SlideFade = class extends Slide {
  constructor (ele) { super(ele) }
styleSet () {
  this.target.style.cssText = `position:relative;`
  const liStyle = `position:absolute;left:0;right:0;top:0;bottom:0;transition:${this.playTime}s;opacity:0;`
  Array.from(this.target.children).forEach(ele => ele.style.cssText = liStyle)
  this.target.children[0].style.opacity = 1
}
playBefore () { this.target.children[this.pos].style.opacity = 0 }
playAfter () { this.target.children[this.pos].style.opacity = 1 }
}

// 슬라이드 타임 마다 실행할 클래스 목록
const slideType = { SlideWidth, SlideHeight, SlideFade }

하지만 아직 끝나지 않았습니다. 플러그인은 모듈화를 해야 완성됩니다. 다른 plugin에서 접근할 수 없도록 해야 합니다.


모듈화

모듈화는 간단합니다. scope와 namespace를 이용하면 됩니다.
scope와 namespace에 대한 기초 내용은 다음 링크를 참고해주세요
[javascript] hoising, scope, closure

junil.Slide.js

먼저 플러그인 파일 부터 수정합니다.

// junil plugin에 붙일 것입니다.
// 아래 처럼 or 구문을 통해 변수를 할당할 때는 var를 사용해야 합니다.
// 그 이유는 hoisting이 발생해야 아래 구문을 사용할 수 있기 때문입니다.
// 하지만 const, let은 es6구문이며, hoisting이 발생하지 않습니다.
var junil = junil || {};

// 즉시 실행 함수는 괄호'()' 구문을 사용하기 때문에, 구문 선언 이전에 세미콜론이 있어야 합니다.
// 만약에 위에 구문에서 세미콜론이 없다면 다음과 같이 됩니다.
// var junil = junil || {}(function () {})()
// 즉, 구문이 이어지게 됩니다. 그래서 세미콜론으로 이를 방지합니다.
// 즉시 실행 함수를 통해 scope를 설정합니다.
// 외부 파일에서는 (function () {})() 내부의 코드에 접근할 수 없습니다.
(function () {
  const Slide = class { /* ... */ }
  const SlideWidth = class extends Slide { /* ... */ }
  const SlideHeight = class extends Slide { /* ... */ }
  const SlideFade = class extends Slide { /* ... */ }
  const slideType = { SlideWidth, SlideHeight, SlideFade }

  // Namespace를 이용하여 Export 합니다.
  junil.Slide = Slide
})();
// 끝나는 지점에도 마찬가지로 세미콜론을 해줘야 안전합니다.

그 다음 엔트리 파일(전역 실행 파일)에서 namespace를 변경합니다.

// 기본 함수
const all = ele => document.querySelectorAll(ele)
const one = ele => document.querySelector(ele)

// 사이트 로드시 실행
window.onload = function () {
  junil.Slide.init()
}

이렇게 모듈화를 완성하였습니다. 최종 소스 첨부하도록 하겠습니다.



참고 자료

  1. ES6+ 문법 http://junil-hwang.com/blog/javascript-es6-spec/
  2. hoisitng, scope, closure http://junil-hwang.com/blog/hoistring-scope-closure/
  3. javascript slide animation http://junil-hwang.com/blog/javascript-slide-animation/
  4. css slide animation http://junil-hwang.com/blog/javascript-slide-animation/