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

el.offsetTop = offsetParent(가장 가까운 position된 부모)의 위쪽부터 el까지의 Y거리getBoundingClientRect().top + window.scrollYscroll-margin-top을 쓰면 편함오늘의 스승은 유튜버 유노코딩:-)
나긋나긋한 목소리로 쉽고 유쾌하게 알려주신다 ㅎ

.btn-navsection들 (querySelectorAll → NodeList 반환, 인덱스로 접근 가능)
btnNavs의 두번째 인덱스값인 [1] 을 클릭했을 때 =>Top1 값을 가짐!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' });
});
});
<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' });
});
});
section { scroll-margin-top: 72px; } /* 헤더 높이에 맞게 조정 */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
만약 고정 헤더(fixed header) 를 쓴다면, 그대로 offsetTop 값을 적용하면 섹션의 위쪽이 가려질 수 있음 → scroll-margin-top 속성을 section에 주면 간단하게 해결됨.
getBoundingClientRect().top + window.scrollY 조합을 쓰면 뷰포트 기준 좌표도 계산 가능 → 좀 더 정밀하게 제어할 수 있다.
scrollIntoView({ behavior: "smooth" }) 라는 내장 메서드도 있는데, 상황에 따라 offsetTop 보다 훨씬 간단히 구현할 수 있음.
덤: 아주 간단한 스크롤 스파이
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));
