[javascript] Auto Animation Plugin 만들기

javascript auto animation plugin

이번에는 자동으로 다양한 Animation이 실행되는 플러그인을 만들어보도록 하겠습니다. 들어가기 이전에 먼저 결과물을 확인해보겠습니다.

이렇게 애니메이션을 자동화 할 수 있습니다. 포스트 순서는 다음과 같습니다.

  1. 뼈대 만들기
  2. 수도 코드 정의하기
  3. 추상화
  4. 구현

그리고 현재 포스트를 읽기 이전에 다음 포스트를 읽으시면 큰 도움이 될 것입니다.
[javascript] Slide Animation Plugin 만들기

뼈대 만들기

일단 html로 뼈대를 정의하도록 하겠습니다

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation</title>
<script src="js/junil.animation.js"></script>
</head>
<body>
<a href="#" onclick="new junil.Animation({playTarget: 'body'})">애니메이션 실행</a>
<a href="#" onclick="new junil.Animation({playTarget: 'body', reverse: true})">애니메이션 역실행</a>
<a href="#" onclick="new junil.Animation({playTarget: 'body', callback: () => { new junil.Animation({playTarget: 'body', reverse: true}) }})">애니메이션 실행 후 역실행</a>
<section>
  <h1 class="animation left2right">제목1</h1>
  <p class="animation big2small">내용1</p>
</section>
<section>
  <h2 class="animation right2left">제목2</h2>
  <p class="animation top2btm">내용2</p>
</section>
<section>
  <h3 class="animation left2right">제목3</h3>
  <p class="animation btm2top">내용3</p>
</section>
<section>
  <h4 class="animation right2left">제목4</h4>
  <p class="animation small2big">내용4</p>
</section>
<section>
  <h5 class="animation left2right">제목5</h5>
  <p class="animation default">내용5</p>
</section>
<section>
  <h6 class="animation right2left">제목6</h6>
  <p class="animation big2small">내용6</p>
</section>
</body>
</html>

플러그인의 원리는 다음과 같습니다.

  1. 애니메이션을 실행시킬 태그에 'animation' 이라는 이름의 class를 지정한다
  2. 애니메이션이 실행되는 형태(left2right, right2left, ... )를 정의한다.
  3. 플러그인 실행 시 animation class를 가진 모든 태그는 애니메이션을 실행한다.




수도 코드 정의하기

프로그램을 전체적으로 작성하기 이전에, 항상 수도코드로 정의해보면 좋습니다.

  1. 애니메이션 클래스 작성
    1. 애니메이션 생성자 작성
    2. 애니메이션 스타일 세팅 메소드 작성
    3. 애니메이션 준비 메소드 작성
    4. 애니메이션 초기화 메소드 작성
    5. 애니메이션 실행 메소드 작성
      • 모든 태그 반복
        • 애니메이션 활성화
  2. 애니메이션 클래스의 인스턴스 생성

이를 토대로 Animation Class를 추상화를 해보겠습니다.

추상화

직전에 수도 코드를 정의하였습니다. 수도 코드를 기반으로 추상화한 코드는 다음과 같습니다.

// 1. 애니메이션 클래스 정의
class Animation {
  // 1-1. 생성자
  constructor () { }

  // 1-2. 스타일 세팅
  static styleSet () { }

  // 1-3. 준비
  static init () { }

  // 1-4. 초기화
  clear () { }

  // 1-5. 실행
  play () {
    // 1-5-1. 모든 태그 반복
    this.target.forEach(ele => {
        /* 애니메이션 활성화 */
    })
  }
}
// 2. 애니메이션 클래스의 인스턴스 생성
new Animation()

대충 감이 오시나요? 이렇게 코드에 대한 추상화를 한 다음, 이를 채워 나가는 방식으로 코드를 구현하면 쉽습니다.

구현

모듈화 준비

먼저 다른 코드와 겹치지 않도록 모듈화를 합니다.

var junil = junil || {};
(function () {
  class Animation {
    constructor () { }
    static styleSet () { }
    static init () { }
    clear () { }
    play () {}
  }
  junil.Animation = Animation
})();

DOM 함수 작성

animation plugin을 작성하기 이전에, 기본적인 DOM function을 정의해보겠습니다.

var junil = junil || {};
(function () {
  // 전체 태그 선택
  const all = ele => document.querySelectorAll(ele)

  // 단일 태그 선택
  const one = ele => document.querySelector(ele)

  // class 추가
  const addClass = (ele, addClassName) => {
    const classList = ele.className.split(' ')
    const index = classList.indexOf(addClassName)
    if (index === -1) {
      classList.push(addClassName)
      ele.className = classList.join(' ')
    }
  }

  // class 삭제
  const removeClass = (ele, removeClassName) => {
    const classList = ele.className.split(' ')
    const index = classList.indexOf(removeClassName)
    if (index !== -1) {
      classList.splice(index, 1)
      ele.className = classList.join(' ')
    }
  }

  // Animatino plugin
  const timerList = [] // timer의 stack
  class Animation {
    constructor () { }
    static styleSet () { }
    static init () { }
    clear () { }
    play () {}
  }

  // 사이트 로드시 실행
  window.onload = function () {
    // 애니메이션 플러그인 준비
    Animation.init()
  }

  // animation plugin export
  junil.Animation = Animation
})();

이제 본격적으로 plugin의 구현 코드를 작성하겠습니다.

1) 애니메이션 준비 : static init()

static init () {
  // 모든 animation class가 있는 element를 선택합니다
  // animationBefore 라는 class를 추가합니다.
  all('.animation').forEach(ele => addClass(ele, 'animationBefore'))
  timerList = [] // timer stack을 초기화 합니다.
  // animation 관련 style을 추가합니다.
  Animation.styleSet()
}

2) 스타일 초기화 : static styleSet()

static styleSet () {
  const style = document.createElement('style')
  style.innerHTML = `
    .animation{opacity:1;transform:inherit;transition:1s}
    .animation.animationBefore{opacity:0;transform:scale(0);transition:0s}
    .animation.animationBefore.top2btm{transform:translateY(-100px)}
    .animation.animationBefore.btm2top{transform:translateY(100px)}
    .animation.animationBefore.left2right{transform:translateX(-100px)}
    .animation.animationBefore.right2left{transform:translateX(100px)}
    .animation.animationBefore.big2small{transform:scale(2, 2)}
    .animation.animationBefore.type2{transition:1s}
  `
  one('head').appendChild(style)
}

사실 제일 핵심이 되는 부분입니다. 이 부분의 역할은 이렇습니다.

  • .animation: 나타나있는 상태(실행된 상태)
  • .animation.animaionBefore: 나타나기 전의 상태(실행 되기 전)
  • 나머지: 나타나기 전의 위치 혹은 형태

init 부분에서 .animation에다가 .animationBefore를 추가합니다. 즉, .animation들은 나타나기 전의 상태로 초기화 한 다음에 .animationBefore를 차례대로 삭제하면, 애니메이션이 실행되는 것입니다.

3) 생성자 : constructor ()

constructor (option) {
  /* set variable */
  this.delay      = 30
  this.lastTimer  = 0
  this.playTarget = one(option.playTarget)
  this.reverse    = option.reverse || false
  this.callback   = option.callback || function () {}

  /* start play */
  this.play()
}

Animation Instance를 생성 되는 시점에 변수 설정을 하고, animation을 play 시킵니다.

4) 범위 내 선택 : find ()

find (ele) { return this.playTarget.querySelectorAll(ele) }

이 부분은 중복 되는 코드를 없애고자 추가했습니다. playTarget 태그 내부에서 animation을 실행하게 되는데, 이 때 정확히 playTarget 내부의 태그만 선택할 때 find method를 사용합니다.

5) 초기화 : clear ()

clear () {
  timerList.forEach(element => { clearTimeout(element) })
  timerList = []
}

애니메이션 실행을 초기화 하는 코드입니다. 애니메이션이 순차적으로 실행되게 하려면 setTimeout 을 이용해야 하며, 해당 정보를 timerList Array에 push 합니다. 그리고 초기화 할 때 timerList에 있는 setTimeout을 전부 clear한 다음, timerList 또한 초기화 합니다.

6) 실행 : play ()

play () {
  this.clear()
  let timer = 0
  const seq = this.reverse ?
              this.find('.target-reverse') :
              this.find('.animation')
  const len = seq.length
  seq.forEach((ele, index) => {
    timerList.push(setTimeout(_ => {
      if(this.reverse){
        const target = seq[len - index - 1]
        addClass(target, 'animationBefore')
        addClass(target, 'type2')
        removeClass(target, 'target-reverse')
      } else {
        removeClass(ele, 'animationBefore')
        removeClass(ele, 'type2')
        addClass(ele, 'target-reverse')
      }
    }, timer))
    timer += this.delay
  })
  this.lastTimer = timer
  setTimeout(this.callback, this.lastTimer + 1000)
}

애니메이션이 실행되는 형태는 2가지이며, reverse를 통해 판별합니다.

  1. reverse = false: 위에서 부터 차례대로 나타남
  2. reverse = true: 아래에서 부터 차례대로 사라짐

reverse가 true일 경우, 마지막 태그 부터 선택해야 하고(len - index -1)
reverse가 false일 경우, 첫번째 태그 부터 선택해야 합니다.

animation + animationBefore 상태에서 animationBefore가 사라지면 tag가 나타나게 되고
animation만 있는 상태에서 animationBefore가 추가 되면, tag가 사라집니다.

그리고 이러한 과정을 setTimeout으로 지연 실행 함으로써 자연스러운 애니메이션이 연출됩니다.

마지막으로 callback 함수가 있을 경우 이를 실행합니다. callback 함수가 실행 되는 시점은 모든 setTimeout이 진행 된 다음입니다.

최종 코드

전부 작성했으니, 최종본을 확인해봅시다.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animation</title>
<script src="js/junil.animation.js"></script>
</head>
<body>
<a href="#" onclick="new junil.Animation({playTarget: 'body'})">애니메이션 실행</a>
<a href="#" onclick="new junil.Animation({playTarget: 'body', reverse: true})">애니메이션 역실행</a>
<a href="#" onclick="new junil.Animation({playTarget: 'body', callback: () => { new junil.Animation({playTarget: 'body', reverse: true}) }})">애니메이션 실행 후 역실행</a>
<section>
  <h1 class="animation left2right">제목1</h1>
  <p class="animation big2small">내용1</p>
</section>
<section>
  <h2 class="animation right2left">제목2</h2>
  <p class="animation top2btm">내용2</p>
</section>
<section>
  <h3 class="animation left2right">제목3</h3>
  <p class="animation btm2top">내용3</p>
</section>
<section>
  <h4 class="animation right2left">제목4</h4>
  <p class="animation small2big">내용4</p>
</section>
<section>
  <h5 class="animation left2right">제목5</h5>
  <p class="animation default">내용5</p>
</section>
<section>
  <h6 class="animation right2left">제목6</h6>
  <p class="animation big2small">내용6</p>
</section>
</body>
</html>

junil.animation.js

// 모듈화
var junil = junil || {};
(function () {
  let timerList = [] 
  // DOM 함수
  const all = ele => document.querySelectorAll(ele)
  const one = ele => document.querySelector(ele)
  const addClass = (ele, addClassName) => {
    const classList = ele.className.split(' ')
    const index = classList.indexOf(addClassName)
    if (index === -1) {
      classList.push(addClassName)
      ele.className = classList.join(' ')
    }
  }
  const removeClass = (ele, removeClassName) => {
    const classList = ele.className.split(' ')
    const index = classList.indexOf(removeClassName)
    if (index !== -1) {
      classList.splice(index, 1)
      ele.className = classList.join(' ')
    }
  }

  // animation plugin
  class Animation {
    constructor (option) {
      /* set variable */
      this.delay      = 30
      this.lastTimer  = 0
      this.playTarget = one(option.playTarget)
      this.reverse    = option.reverse || false
      this.callback   = option.callback || function () {}

      /* start play */
      this.play()
    }

    find (ele) { return this.playTarget.querySelectorAll(ele) }

    play () {
      this.clear()
      let timer = 0
      const seq = this.reverse ? this.find('.target-reverse') : this.find('.animation')
      const len = seq.length
      seq.forEach((ele, index) => {
        timerList.push(setTimeout(_ => {
          if(this.reverse){
            const target = seq[len - index - 1]
            addClass(target, 'animationBefore')
            addClass(target, 'type2')
            removeClass(target, 'target-reverse')
          } else {
            removeClass(ele, 'animationBefore')
            removeClass(ele, 'type2')
            addClass(ele, 'target-reverse')
          }
        }, timer))
        timer += this.delay
      })
      this.lastTimer = timer
      setTimeout(this.callback, this.lastTimer + 1000)
    }

    clear () {
      timerList.forEach(element => { clearTimeout(element) })
      timerList = []
    }

    static init () {
      all('.animation').forEach(ele => addClass(ele, 'animationBefore'))
      timerList = []
      Animation.styleSet()
    }

    static styleSet () {
      const style = document.createElement('style')
      style.innerHTML = `
        .animation{opacity:1;transform:inherit;transition:1s}
        .animation.animationBefore{opacity:0;transform:scale(0);transition:0s}
        .animation.animationBefore.top2btm{transform:translateY(-100px)}
        .animation.animationBefore.btm2top{transform:translateY(100px)}
        .animation.animationBefore.left2right{transform:translateX(-100px)}
        .animation.animationBefore.right2left{transform:translateX(100px)}
        .animation.animationBefore.big2small{transform:scale(2, 2)}
        .animation.animationBefore.type2{transition:1s}
      `
      one('head').appendChild(style)
    }
  }

  window.onload = _ => {
    Animation.init()
  }
  junil.Animation = Animation
})();


참고 자료