jQuery / Plugin / jQuery Smooth Scroll

jQuery Smooth Scroll은 href="#beta"처럼 같은 페이지의 특정 위치로 이동하는 링크를 부드럽게 스크롤하도록 만드는 플러그인입니다. 적용할 링크를 선택해 .smoothScroll()을 호출하고, speed로 이동 시간, offset으로 도착 위치를 조절합니다. 화면 이동뿐 아니라 목적지 포커스와 동작 줄이기 설정도 함께 처리하면 탐색을 이어가기 쉽습니다.

적용 범위와 파일 준비

이 글은 kswedberg의 jquery-smooth-scroll을 사용합니다. 비슷한 이름의 마우스 휠 제어 라이브러리와 다르며, 기본 스크롤 전체를 바꾸는 예제가 아닙니다. 제목의 id와 링크의 해시가 일치해야 합니다. 예를 들어 href="#beta"는 같은 문서의 id="beta"로 연결됩니다.

예제는 CDN의 플러그인 2.2.0과 jQuery 전체 파일 4.0.0을 사용합니다. jQuery → 플러그인 → 초기화 순서를 지키고, 애니메이션 기능이 빠진 slim 파일은 사용하지 않습니다. Windows Edge에서 같은 플러그인 파일을 jQuery 4.0.0·3.7.1 각각으로 실행해 아래 동작을 확인했습니다. 다른 플러그인이나 테마가 섞인 사이트의 호환성까지 보장하는 의미는 아닙니다.

모든 a에 일괄 적용하는 대신 메뉴의 .js-scroll 두 링크만 선택합니다. 같은 메뉴의 Native jump와 본문의 다른 링크는 기본 동작을 유지하므로 차이를 바로 비교할 수 있습니다. 실제 사이트에서도 문서 내 탐색 링크라는 의도가 분명한 범위에 적용합니다.

메뉴 여백과 포커스를 포함한 전체 예제

전체를 UTF-8의 smooth-scroll-demo.html로 저장해 엽니다. 별도 서버나 빌드 도구는 필요 없고, 처음에는 인터넷으로 두 JavaScript 파일을 읽습니다. 각 영역을 길게 만들고 마지막에도 공간을 두어 도착 위치를 비교할 수 있게 했습니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Same-page smooth scrolling</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-smooth-scroll/2.2.0/jquery.smooth-scroll.min.js"></script>
  <style>
    :root { --anchor-gap: 100px; }
    html { scroll-behavior: auto; }
    body { font: 16px/1.6 sans-serif; margin: 20px; padding-top: var(--anchor-gap); }
    #menu { position: fixed; top: 0; left: 0; right: 0; padding: 12px; background: #e6f4f1; z-index: 10; }
    #menu a, #menu button { display: inline-block; margin: 4px 8px; font: inherit; }
    section { min-height: 700px; max-width: 700px; }
    h2 { scroll-margin-top: var(--anchor-gap); }
    h2:focus { outline: 3px solid teal; }
    footer { min-height: 100vh; }
  </style>
</head>
<body>
  <nav id="menu" aria-label="Sections">
    <a class="js-scroll" href="#alpha">Alpha</a>
    <a class="js-scroll" href="#beta">Beta</a>
    <a href="#native">Native jump</a>
    <button type="button" id="reset">Reset</button>
    <span id="status" role="status">Native links available.</span>
  </nav>
  <h1>Explore the sections</h1>
  <section><h2 id="alpha" tabindex="-1">Alpha</h2><p>First destination. Text remains available without JavaScript.</p></section>
  <section><h2 id="beta" tabindex="-1">Beta</h2><p>Second destination. Tab can continue to <a href="#native">the next destination</a>.</p></section>
  <section><h2 id="native" tabindex="-1">Native destination</h2><p>This menu link is not enhanced by the plugin.</p></section>
  <footer>Enough space to align the last heading below the menu.</footer>
  <script>
    jQuery(function ($) {
      if (typeof $.fn.smoothScroll !== "function") return;
      const $menu = $("#menu"), $status = $("#status"), duration = 600;
      function gap() {
        const value = $menu.outerHeight() + 12;
        document.documentElement.style.setProperty("--anchor-gap", value + "px");
        return value;
      }
      gap(); $(window).on("resize.demo", gap);
      $("#menu a.js-scroll").smoothScroll({
        speed: duration, easing: "swing", autoFocus: true,
        beforeScroll: function (options) {
          options.offset = -gap();
          options.speed = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : duration;
          $status.text("Moving to " + options.scrollTarget);
        },
        afterScroll: function (options) {
          history.replaceState(null, "", options.scrollTarget);
          $status.text("Arrived at " + options.scrollTarget);
        }
      });
      $("#reset").on("click", function () {
        $("html, body").stop(true, false); window.scrollTo(0, 0);
        history.replaceState(null, "", location.pathname + location.search);
        $status.text("Ready; enhanced links: 2.");
      });
      $status.text("Ready; enhanced links: 2.");
    });
  </script>
</body>
</html>

플러그인이 정상 준비되면 메뉴에 다음 상태가 표시됩니다. JavaScript를 끄면 Native links available.이 남고 일반 해시 링크로 이동할 수 있습니다.

Ready; enhanced links: 2.
  1. Beta를 클릭합니다. 약 600ms에 걸쳐 내려가고 Arrived at #beta가 표시됩니다. 목표 제목 위에는 메뉴 높이와 12px을 합한 공간이 남습니다.
  2. Reset으로 문서 맨 위와 해시 없는 URL로 돌아갑니다. Alpha나 Beta에 키보드로 포커스를 놓고 Enter를 눌러 같은 이동을 확인합니다.
  3. 도착한 제목에는 포커스 윤곽선이 표시됩니다. Beta 도착 뒤 Tab을 누르면 그 아래의 링크로 탐색을 이어갈 수 있습니다.
  4. Native jump는 플러그인을 거치지 않고 이동합니다. 플러그인 상태 문구도 새 도착 안내로 바뀌지 않습니다.
  5. 동작 줄이기 설정을 reduce로 바꿔 Alpha·Beta를 누릅니다. 같은 목적지와 완료 처리를 사용하되 애니메이션 시간은 0이 됩니다.
  6. 창 폭을 바꾸어 메뉴가 줄바꿈되는 경우에도 제목이 가려지지 않는지 확인합니다. Reset 후 같은 링크로 다시 비교합니다.

이 예제의 tabindex="-1"은 제목을 일반 Tab 순서에 추가하지 않으면서 프로그램으로 포커스를 받을 수 있게 합니다. autoFocus=true가 이동 후 포커스를 옮깁니다. 화면만 이동하고 키보드 위치가 메뉴에 남는 상황을 구분하기 위한 설정입니다.

시간·오프셋·URL을 조정하는 기준

speed는 여기서 밀리초 단위의 이동 시간입니다. 값이 작을수록 같은 거리를 빨리 이동하며, 기본 easing인 swing은 진행 속도가 일정하지 않습니다. 먼저 duration 상수만 600에서 1200으로 바꾸어 같은 목적지에서 비교하고, 다시 600으로 되돌립니다. easing을 linear로 바꾸는 비교는 시간을 그대로 둡니다. 임의의 easing 이름은 별도 확장이 없으면 사용할 수 없습니다.

offset은 목표 좌표에 더하는 값입니다. 위쪽 여백이 필요하면 음수를 쓰므로, 메뉴가 차지하는 높이와 여유 12px의 합을 음수로 줍니다. beforeScroll에서 실제 높이를 다시 재기 때문에 창 크기 변화와 메뉴 줄바꿈을 반영합니다. 양수로 바꾸면 목표보다 더 아래로 이동하므로 고정 메뉴에 제목을 숨길 수 있습니다.

CSS의 scroll-margin-top도 지정했지만, 플러그인의 숫자 좌표 계산을 대신한다고 가정하지 않았습니다. 플러그인에는 offset을, 기본 링크에는 scroll-margin-top을 적용합니다. 두 방식의 실제 결과를 각각 확인해야 합니다.

플러그인의 기본 preventDefault는 링크의 기본 이동을 막습니다. 따라서 예제는 afterScroll에서 history.replaceState()로 주소의 해시를 바꿉니다. 현재 기록을 바꾸는 방식이므로 이동마다 뒤로 가기 기록을 추가하지 않으며, 별도의 hashchange 탐색도 구현하지 않았습니다. 뒤로·앞으로 이동이 요구되는 앱에서는 pushState·popstate 등의 정책을 따로 설계해야 합니다.

Reset은 진행 중인 효과와 대기열을 멈추고 스크롤·해시·상태를 되돌립니다. 목적지 이동 중 연속 클릭도 마지막 목적지에 도착하는지 확인했습니다. 이는 마우스 휠이나 모든 사용자 입력으로 애니메이션을 취소하는 기능을 구현했다는 뜻은 아닙니다.

CSS 방식과 문제 해결

단순한 문서 내 이동만 필요하다면 scroll-behavior로 충분할 수 있습니다. 다음 CSS는 플러그인 방식에 더해 사용하는 코드가 아닙니다. 위 HTML에서 두 외부 스크립트와 초기화 script, Reset 버튼과 상태 표시를 제거하고, 기존 html의 scroll-behavior:auto 규칙을 다음으로 교체합니다. 제목의 scroll-margin-top은 유지하되 고정 메뉴 높이에 맞는 값을 직접 정합니다.

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

이 방식은 브라우저의 기본 링크·해시 처리를 사용하며, 플러그인의 밀리초 시간·콜백 설정을 제공하지 않습니다. 동작 줄이기는 CSS 미디어 쿼리로 적용합니다. 반대로 플러그인 예제는 CSS 스무스 이동을 끈 상태에서 jQuery 애니메이션만 사용해 두 제어가 겹치지 않도록 했습니다.

smoothScroll is not a function 오류가 나면 Network에서 플러그인 경로와 응답, jQuery의 로딩 순서를 확인합니다. 플러그인을 읽은 뒤 jQuery를 다시 읽어 메서드가 사라진 경우도 살핍니다. 워드프레스에서는 테마의 기존 jQuery와 스크립트 등록 의존성을 먼저 확인합니다.

이동하지 않으면 id 중복·누락, 실제 href, 적용 선택자를 확인합니다. 도착 위치가 어긋나면 고정 메뉴 높이, 로딩 뒤 커지는 이미지·글꼴과 문서 끝의 남은 스크롤 공간을 점검합니다. 내부 스크롤 상자를 이동하려는 경우에는 문서 스크롤과 좌표 기준이 다르므로 별도의 scrollElement 설정과 해당 컨테이너 검증이 필요합니다.