jQuery / 요소가 순서대로 나타나게 하는 방법

요소를 일정 간격으로 나타내려면 각 요소에 .delay(index * gap).fadeIn(duration)을 적용합니다. 이 방식은 시작 시간을 나누므로 효과 시간이 간격보다 길면 서로 겹칩니다. 앞 요소가 완전히 나타난 다음 요소를 시작하려면 앞 효과의 완료 콜백에서 다음 요소를 실행합니다.

시작 간격과 완료 순서 구분하기

.delay()는 각 요소의 효과 대기열에서 다음 작업을 기다리게 합니다. 집합 전체가 하나의 대기열을 공유하는 것은 아닙니다. 각 요소에 같은 delay를 주면 비슷한 시점에 시작하므로 index를 곱해 시작 시간을 다르게 만듭니다. 이 대기 중에도 JavaScript 전체가 멈추지는 않습니다.

예제의 간격은 200ms, .fadeIn() 시간은 400ms입니다. 시작 간격 방식에서는 Alpha가 끝나기 전에 Beta가 시작할 수 있습니다. 완료 대기 방식은 Alpha 완료 뒤 200ms를 기다려 Beta를 시작하므로 겹치지 않습니다. 브라우저의 실제 실행 간격은 작업 부하와 화면 갱신에 따라 달라질 수 있으며, 이 수치는 예약 기준입니다.

.each()의 순서는 선택 집합 순서입니다. 예제는 #items 바로 아래 .item을 문서 순서대로 선택하므로 Alpha·Beta·Gamma입니다. 별도의 정렬 집합을 만들면 시각적 배치 순서와 달라질 수 있습니다. CSS로 위치만 바꾼 화면에서는 독자가 읽는 순서와 일치하는지도 확인합니다.

두 방식을 비교하는 전체 예제

아래 전체 코드를 UTF-8의 appearance-demo.html로 저장해 브라우저로 엽니다. 인터넷으로 공식 jQuery 전체 파일 4.0.0을 읽습니다. 시간 지정 효과를 사용하므로 effects가 빠진 slim으로 바꾸지 않습니다. 서버나 설치는 필요하지 않습니다. Windows Edge에서 3.7.1·4.0.0으로 순서와 초기화·재실행을 확인했습니다.

HTML 자체에는 세 요소가 있고, 스크립트가 준비된 뒤 숨겨 실습을 시작합니다. JavaScript 로딩 실패만으로 본문을 영구히 숨기는 CSS는 두지 않았습니다. 아래 출력은 자바스크립트 초기화가 끝난 상태입니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Staggered versus strictly sequential appearance</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; }
    button, select { font: inherit; margin: 4px; }
    .item { border: 2px solid teal; padding: 12px; margin: 12px 0; background: #e7f5f1; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <label>Mode <select id="mode"><option value="stagger">Staggered starts</option><option value="serial">Wait for completion</option></select></label>
  <button type="button" id="start">Start</button>
  <button type="button" id="reset">Reset</button>
  <div id="items"><div class="item" id="alpha">Alpha</div><div class="item" id="beta">Beta</div><div class="item" id="gamma">Gamma</div></div>
  <pre id="result" aria-live="polite"></pre>
  <pre id="events"></pre>
  <script>
    jQuery(function ($) {
      const $items = $("#items > .item"), gap = 200, duration = 400;
      let generation = 0, busy = false, state = "ready", completed = [], events = [];
      function report() {
        $("#result").text(`mode=${$("#mode").val()}\nstate=${state}\nvisible=${$items.filter(":visible").length}/${$items.length}\ncompleted=${completed.join(",") || "(none)"}`);
        $("#events").text(events.join("\n") || "(none)");
      }
      function reset() {
        generation++; // Invalidate callbacks before settling old effects.
        $items.stop(true, true).hide();
        busy = false; state = "ready"; completed = []; events = [];
        $("#start").prop("disabled", false); report();
      }
      function start() {
        if (busy) return;
        reset(); busy = true; state = "running"; $("#start").prop("disabled", true);
        const run = generation, mode = $("#mode").val();
        const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
        if (reduce) {
          $items.show(); completed = $items.map(function () { return this.id; }).get();
          events = ["Reduced motion: shown immediately"];
          busy = false; state = "done"; $("#start").prop("disabled", false); report(); return;
        }
        function reveal(index, wait) {
          const $item = $items.eq(index), id = $item[0].id;
          $item.delay(wait).queue(function (next) {
            if (run !== generation) return;
            events.push("start " + id); next(); report();
          }).fadeIn(duration, function () {
            if (run !== generation) return;
            completed.push(id); events.push("done " + id);
            if (completed.length === $items.length) {
              busy = false; state = "done"; $("#start").prop("disabled", false);
            } else if (mode === "serial") reveal(index + 1, gap);
            report();
          });
        }
        if (!$items.length) { busy = false; state = "done"; $("#start").prop("disabled", false); report(); return; }
        if (mode === "stagger") $items.each(function (index) { reveal(index, index * gap); });
        else reveal(0, 0);
        report();
      }
      $("#start").on("click", start);
      $("#reset").on("click", reset);
      $("#mode").on("change", reset);
      reset();
    });
  </script>
</body>
</html>

처음에는 요소 0개가 보이고, 완료된 요소도 없습니다.

mode=stagger
state=ready
visible=0/3
completed=(none)
  1. Staggered starts에서 Start를 누릅니다. 로그에서 start beta가 done alpha보다 앞에 나올 수 있으며, 세 효과의 시작이 200ms씩 나뉩니다.
  2. 완료되면 state=done, visible=3/3, completed=alpha,beta,gamma입니다. Start는 다시 누를 수 있습니다.
  3. Mode를 Wait for completion으로 바꾸면 초기화됩니다. Start 후 로그는 start alpha → done alpha → start beta → done beta → start gamma → done gamma 순서입니다.
  4. 실행 중 Reset을 누르면 숨김 초기 상태로 돌아갑니다. 잠시 기다려도 과거 대기 효과가 다시 나타나지 않아야 합니다. 이후 Start로 같은 동작을 반복합니다.
  5. 운영체제나 개발자 도구에서 동작 줄이기를 켠 뒤 Start를 누르면 대기 없이 모두 표시하고 Reduced motion: shown immediately를 기록합니다.

반복 클릭과 초기화 처리

Start는 실행 중 비활성화하고 busy 값으로도 중복 실행을 막습니다. 버튼 비활성화만이 아니라 함수 진입 시 확인하므로 코드에서 다시 호출하는 경우에도 효과가 쌓이지 않습니다. 전체 완료는 각 요소의 완료 콜백을 모아 판단합니다. 한 요소의 콜백이 실행됐다고 집합 전체가 끝난 것으로 처리하지 않습니다.

.stop(true, true)는 기다리는 효과를 지우고 현재 효과를 끝 상태로 정리할 수 있습니다. Reset은 먼저 generation을 바꿔 과거 콜백을 무효로 만든 뒤 효과를 정리하고 .hide()로 알려진 초기 상태를 만듭니다. true인 jumpToEnd가 완료 콜백을 실행할 수 있으므로 토큰 변경보다 먼저 호출하지 않습니다.

이 순서는 재생 후 중간에 멈췄다가 다시 실행할 때 남은 효과 상태와 대기열이 섞이지 않도록 한 것입니다. 단순 clearQueue만으로 현재 진행 중인 효과까지 멈추는 것은 아니며, 대기열 밖의 setTimeout을 별도로 사용했다면 그 타이머는 별도 취소해야 합니다. 예제는 기본 fx 대기열의 delay를 사용합니다.

로그용 .queue()는 시작 시점을 기록한 뒤 next()를 호출해 뒤의 fadeIn으로 진행합니다. next()를 빼면 해당 효과 대기열이 그 작업에서 멈춥니다. 엄격한 순차 방식은 완료 콜백에서 다음 인덱스를 호출하므로, 완료 전에 한꺼번에 모든 다음 작업을 시작하지 않습니다.

사용자 설정과 실제 화면 적용

prefers-reduced-motion이 reduce면 페이드와 대기를 모두 생략하고 최종 상태를 즉시 만듭니다. 이 예제의 처리이며 사이트 전체 jQuery 효과 설정을 바꾸지 않습니다. 순서를 보여 주는 장식 효과 때문에 중요한 정보에 접근하는 시간이 길어지지 않게 합니다.

display:none에서 나타나는 항목은 레이아웃 공간이 생기면서 아래 내용이 움직일 수 있습니다. 고정된 자리가 필요한 카드·이미지는 별도 컨테이너 높이를 확보하거나 다른 표시 방식을 검토합니다. 숨기는 항목에 포커스 가능한 입력이 있다면 키보드 접근과 현재 포커스도 함께 설계해야 합니다.

동적으로 추가한 요소까지 대상에 넣으려면 실행 시점의 선택 집합과 완료 개수를 다시 정합니다. 예제의 $items는 초기 세 요소를 보관한 집합이므로 자동으로 새 요소를 포함하지 않습니다. 처음부터 전부 다시 재생할지 새 요소만 나타낼지 정한 뒤 초기화와 대기열 소유 범위를 맞춥니다.