nav에 offsetTop으로 스크롤 이동 붙이기 (차분일기)

잔나비·2025년 8월 21일

jannabee로그

목록 보기
7/14

기존에 만들었던 가상 브랜드 차분일기 홈페이지 nav에 클릭 시 섹션으로 스무스하게 이동을 붙였다.
작은 기능인데, 페이지가 확실히 ‘홈페이지답다’는 느낌이 난다. 🙂

offsetTop 핵심 요약

  • el.offsetTop = offsetParent(가장 가까운 position된 부모)의 위쪽부터 el까지의 Y거리
  • 뷰포트 기준이 필요하면 getBoundingClientRect().top + window.scrollY
  • 고정 헤더가 있으면 헤더 높이만큼 보정하거나 scroll-margin-top을 쓰면 편함

오늘의 스승은 유튜버 유노코딩:-)
나긋나긋한 목소리로 쉽고 유쾌하게 알려주신다 ㅎ


작업 일지

1) 구조

  • 클릭 대상: .btn-nav
  • 이동 대상: section들 (querySelectorAllNodeList 반환, 인덱스로 접근 가능)

  • 메뉴의 NodeList인 btnNavs의 두번째 인덱스값인 [1] 을 클릭했을 때 =>
    - 윈도우의 스크롤 지점의 맨 윗값이 변수 Top1 값을 가짐!
    • 이 때 변화는 스무쓰~ 하게 (auto로 지정하면 뚝뚝 끊기듯이 똑부러지게 이동함)

2) 구현 스니펫 (두 가지 방식)

A. 단순 인덱스 매핑(내가 처음 쓴 방식)

const btnNavs  = document.querySelectorAll('.btn-nav'); // NodeList
const sections = document.querySelectorAll('section');   // NodeList

btnNavs.forEach((btn, i) => {
  btn.addEventListener('click', (e) => {
    e.preventDefault();
    const top = sections[i].getBoundingClientRect().top + window.scrollY; 
    window.scrollTo({ top, behavior: 'smooth' });
  });
});

B. 더 견고한 방식: data-target / 앵커

<nav>
  <a class="btn-nav" data-target="#about">About</a>
  <a class="btn-nav" data-target="#menu">Menu</a>
  <a class="btn-nav" data-target="#contact">Contact</a>
</nav>
document.querySelectorAll('.btn-nav').forEach(a => {
  a.addEventListener('click', (e) => {
    e.preventDefault();
    const target = document.querySelector(a.dataset.target);
    target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
  });
});
  • 인덱스 의존성 제거 → 섹션 순서가 바뀌어도 안전
  • JS가 꺼진 상황을 대비해 앵커(#id)로 구성해두면 프로그레시브 인핸스먼트에 좋다.

3) 고정 헤더 보정

section { scroll-margin-top: 72px; } /* 헤더 높이에 맞게 조정 */

4) 접근성(모션 민감 사용자)

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

결과 & 느낀점

  • 드디어 nav 메뉴를 클릭하면 각 섹션으로 스무스하게 이동하는 페이지 완성 ✨
  • 단순히 offsetTop만 쓰는 게 아니라, 배열 인덱스로 각 section의 위치값을 가져오는 과정이 꽤 직관적이면서도 재밌었다.
  • 작은 기능 하나만 추가했을 뿐인데, 웹페이지가 확실히 '홈페이지답다' 는 느낌을 주는 게 신기하다.

추가 팁

  • 만약 고정 헤더(fixed header) 를 쓴다면, 그대로 offsetTop 값을 적용하면 섹션의 위쪽이 가려질 수 있음 → scroll-margin-top 속성을 section에 주면 간단하게 해결됨.

  • getBoundingClientRect().top + window.scrollY 조합을 쓰면 뷰포트 기준 좌표도 계산 가능 → 좀 더 정밀하게 제어할 수 있다.

  • scrollIntoView({ behavior: "smooth" }) 라는 내장 메서드도 있는데, 상황에 따라 offsetTop 보다 훨씬 간단히 구현할 수 있음.

앞으로의 활용

  • 이번엔 단순히 섹션 이동이었지만, 같은 방식으로 탭 메뉴인디케이터(스크롤 스파이) 같은 인터랙션에도 응용 가능할 듯하다.
  • 작은 성취지만, 직접 구현하고 나니 nav 메뉴에 대한 이해도가 확실히 올라간 것 같다.

덤: 아주 간단한 스크롤 스파이

const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    const id = e.target.id;
    const link = document.querySelector(`.btn-nav[data-target="#${id}"]`);
    if (e.isIntersecting) {
      document.querySelectorAll('.btn-nav').forEach(a => a.classList.remove('active'));
      link?.classList.add('active');
    }
  });
}, { rootMargin: '-50% 0px -50% 0px', threshold: 0 });

document.querySelectorAll('section[id]').forEach(s => io.observe(s));

프로젝트 링크

profile
“브랜드를 잇고, 감성을 설계하며, 기능을 실현하는 연결자”로 성장하고픈^.^

0개의 댓글