jQuery / Method / .animate() - 애니메이션 효과 만드는 메서드

jQuery의 .animate()는 CSS 속성의 현재 값에서 목표 값까지 일정 시간에 걸쳐 바꾸는 메서드입니다. 예를 들어 .animate({ left: 100, width: 160, opacity: 0.4 })는 위치·폭·불투명도를 함께 변경합니다. 실행 시간과 easing을 지정할 수 있으며, 완료 콜백은 선택된 요소마다 호출됩니다.

변경할 속성과 시간 정하기

.animate()의 첫 인자는 목표 속성 객체입니다. left와 width에 숫자를 주면 이 예제에서는 px 단위로 처리하고, opacity는 단위 없는 0~1 값을 사용합니다. 같은 객체에 넣은 속성들은 한 효과 안에서 동시에 바뀝니다. 서로 다른 효과를 연달아 호출하면 기본 효과 대기열에 들어간다는 점과 구분합니다.

left가 이동으로 보이려면 position이 static이 아닌 배치여야 합니다. 예제는 relative로 원래 자리를 기준으로 움직이며, left=100은 문서 전체의 절대 x좌표가 아닙니다. width는 콘텐츠 폭이고 border·padding 조건에 따라 실제 바깥 폭이 달라질 수 있어, 예제는 간단한 content-box로 범위를 고정했습니다.

duration은 밀리초이며 기본값은 400입니다. easing은 진행 속도 곡선으로, 기본 swing과 linear를 비교할 수 있습니다. 목표 값이 같아도 중간 속도가 달라집니다. 기본 jQuery만으로 임의의 색상이나 transform 문자열까지 모두 같은 방식으로 보간할 수 있다고 가정하지 않습니다. 숫자로 바꿀 수 있는 지원 속성인지 확인합니다.

여러 속성과 두 요소를 함께 움직이는 전체 예제

전체 코드를 UTF-8의 animate-demo.html로 저장하고 브라우저로 엽니다. 인터넷으로 공식 jQuery 전체 파일 4.0.0을 읽습니다. 시간 지정 애니메이션에는 effects 모듈이 필요하므로 slim을 사용하지 않습니다. 별도 서버나 설치는 필요하지 않습니다. Windows Edge에서 3.7.1·4.0.0으로 중간값, 목표값과 정지·재실행을 확인했습니다.

두 상자는 같은 목표로 이동합니다. Start는 매번 알려진 초기값으로 복원한 뒤 실행하며, Reset은 시간·easing 선택은 유지하고 상자와 완료 카운터만 초기화합니다. Start 실행 중에는 중복 재생을 막습니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Animate CSS properties and observe completion</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 20px; }
    label { display: block; margin: 8px 0; }
    select, button { font: inherit; margin: 4px; }
    #stage { background: #eef4f4; padding: 10px; overflow-x: auto; }
    .box { position: relative; left: 0; width: 80px; height: 45px; margin: 12px 0; background: teal; color: white; box-sizing: content-box; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <label>Duration <select id="duration"><option value="600">600ms</option><option value="1200">1200ms</option><option value="0">0ms</option></select></label>
  <label>Easing <select id="easing"><option value="swing">swing</option><option value="linear">linear</option></select></label>
  <button type="button" id="start">Start</button>
  <button type="button" id="stop">Stop here</button>
  <button type="button" id="reset">Reset</button>
  <div id="stage"><div class="box" id="alpha">Alpha</div><div class="box" id="beta">Beta</div></div>
  <pre id="result" aria-live="polite"></pre>
  <script>
    jQuery(function ($) {
      const $boxes = $("#stage > .box");
      let generation = 0, busy = false, state = "ready", completed = 0, batches = 0;
      function report() {
        const values = $boxes.map(function () {
          const $box = $(this);
          return this.id + ": left=" + Math.round(parseFloat($box.css("left"))) + "; width=" + Math.round($box.width()) + "; opacity=" + Number(parseFloat($box.css("opacity")).toFixed(2));
        }).get();
        $("#result").text(["state=" + state, "completed=" + completed + "; batches=" + batches, ...values].join("\n"));
      }
      function reset() {
        generation++; $boxes.stop(true, false).css({ left: 0, width: 80, opacity: 1 });
        busy = false; state = "ready"; completed = batches = 0;
        $("#start").prop("disabled", false); report();
      }
      $("#start").on("click", function () {
        if (busy) return;
        reset(); busy = true; state = "running"; $(this).prop("disabled", true);
        const run = generation;
        const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
        const duration = reduce ? 0 : Number($("#duration").val());
        $boxes.animate({ left: 100, width: 160, opacity: 0.4 }, {
          duration: duration, easing: $("#easing").val(),
          complete: function () { if (run === generation) { completed++; report(); } }
        });
        $boxes.promise("fx").done(function () {
          if (run !== generation) return;
          batches++; busy = false; state = "done";
          $("#start").prop("disabled", false); report();
        });
        report();
      });
      $("#stop").on("click", function () {
        if (!busy) return;
        generation++; $boxes.stop(true, false);
        busy = false; state = "stopped"; $("#start").prop("disabled", false); report();
      });
      $("#reset").on("click", reset);
      reset();
    });
  </script>
</body>
</html>

초기 출력입니다. 폭은 80, 상대 left는 0, 불투명도는 1입니다. 표시를 읽기 쉽게 반올림했지만 실제 애니메이션을 정수로 제한한 것은 아닙니다.

state=ready
completed=0; batches=0
alpha: left=0; width=80; opacity=1
beta: left=0; width=80; opacity=1
  1. 기본 600ms·swing에서 Start를 누릅니다. 두 상자의 위치·폭·불투명도가 함께 바뀝니다.
  2. 완료 후 state=done, completed=2; batches=1입니다. 두 상자 모두 left=100; width=160; opacity=0.4입니다.
  3. Reset 후 easing만 linear로 바꾸어 같은 조건을 재생합니다. 목표값은 같고 진행 속도의 느낌이 달라집니다. 다시 swing으로 되돌려 비교할 수 있습니다.
  4. Duration을 1200ms로 정해 실행한 뒤 Stop here를 누릅니다. 중간 상태에서 멈추고 state=stopped입니다. 기다려도 목표로 계속 움직이거나 done으로 바뀌지 않아야 합니다.
  5. Reset으로 초기값을 확인한 뒤 다시 Start합니다. 0ms를 고르면 즉시 목표값과 완료 카운터가 표시됩니다.
  6. 동작 줄이기 설정이 reduce라면 선택한 시간이 1200ms여도 즉시 최종 상태로 갑니다.

요소별 완료와 전체 완료 구분하기

complete 함수는 선택한 요소별로 한 번씩 호출되므로 이 예제에서는 2회입니다. 같은 함수 안에서 this는 해당 DOM 요소입니다. 첫 번째 complete가 실행됐다고 두 번째 요소까지 끝났다고 판단하지 않습니다. .animate() 호출 자체는 효과 종료를 기다려 JavaScript를 멈추지 않습니다.

.promise("fx")는 해당 집합의 효과 작업이 끝나는 시점을 관찰합니다. 여기서는 모든 상자의 효과가 끝난 후 batches를 1회 증가시키고 Start를 다시 활성화합니다. 뒤에 기본 대기열 작업을 추가했다면 그 작업까지 관찰 범위에 들어갈 수 있습니다.

효과 대기열이 비워지는 것은 사용자가 끝까지 보았다는 뜻과 항상 같지 않습니다. .stop(true, false)로 현재 위치에 멈추고 대기열을 비워도 fx promise가 해소될 수 있습니다. 그래서 Stop과 Reset은 먼저 generation을 바꾸어 이전 실행의 콜백을 무효로 합니다. 취소된 실행의 promise가 화면을 done으로 덮지 않게 한 것입니다.

기본 대기열에서 같은 버튼을 연속 실행하면 효과가 줄줄이 쌓일 수 있습니다. 예제는 버튼 비활성화와 busy 검사로 중복을 막습니다. 현재 위치에서 새 목표로 바로 방향을 바꾸는 UI라면 이 정책 대신 기존 효과 정지·새 목표 적용을 의도에 맞게 설계해야 합니다.

정지·복원과 실제 화면 적용

Stop here는 jumpToEnd=false이므로 끝값으로 강제 이동하지 않습니다. Reset은 정지 뒤 left·width·opacity를 명시해 초기 상태를 만듭니다. 이전 중간값을 그대로 두고 Start만 반복하면 비교 조건이 달라지므로, 초기화가 무엇을 되돌리는지 분명히 합니다.

prefers-reduced-motion이 reduce면 duration만 이 실행에서 0으로 정합니다. 같은 목표와 완료 경로를 사용하므로 제어 상태를 별도로 방치하지 않습니다. 페이지 전체의 jQuery 효과를 끄는 전역 설정은 바꾸지 않습니다.

폭과 위치 변화는 주변 배치나 겹침에 영향을 줄 수 있습니다. 좁은 화면, 확대, 긴 텍스트와 스크롤 컨테이너에서 확인하고 콘텐츠나 조작 버튼이 가려지지 않게 합니다. opacity=0으로 만들더라도 자동으로 display:none이나 접근성 숨김 상태가 되는 것은 아니므로 목적에 맞는 상태를 별도로 다룹니다.

CSS transition 등 다른 방식이 같은 속성을 동시에 제어하면 결과를 판단하기 어렵습니다. 해당 요소와 속성의 제어 주체를 정하고, 플러그인이 추가한 효과나 대기열과 섞이지 않는지 확인합니다. 예제를 실제 사이트에 넣을 때는 선택자와 이벤트 범위도 그 기능 안으로 한정합니다.