스크롤 인터랙션
스크롤과 관련된 기능 세 가지(부드러운 스크롤, 헤더 스타일 변경, 맨 위로 버튼)를 모았습니다.
부드러운 스크롤
이 프로젝트는 부드러운 스크롤을 JS의 scrollIntoView()가 아니라 CSS 한 줄로 처리합니다.
css
html {
scroll-behavior: smooth;
}메뉴 링크가 href="#about" 같은 앵커이기만 하면 브라우저가 자동으로 부드럽게 스크롤합니다. 결과는 JS 구현과 동일하지만, 체크리스트에 "JS로 구현"이 명시돼 있다면 방식이 다르다는 점을 밝혀두는 게 좋습니다.
헤더 스타일 변경 & 맨 위로 버튼
스크롤 이벤트 하나(updateScrollUi)에서 두 가지를 동시에 처리합니다.
js
selectors.header.classList.toggle('is-scrolled', scrollY >= 60);
selectors.topButton.classList.toggle('is-visible', scrollY >= 300);- 60px 이상 스크롤되면 헤더에 배경/그림자가 생깁니다.
- 300px 이상이면 맨 위로 이동 버튼이 나타납니다.
- 버튼 클릭 시
window.scrollTo({ top: 0, behavior: 'smooth' })로 최상단 이동을 처리합니다.
스크롤 이벤트와 렌더링 성능
스크롤 이벤트는 손가락을 한 번 쓸어내기만 해도 수십 번씩 발생합니다. 그래서 updateScrollUi 안에서 하는 일이 무거우면 화면이 뚝뚝 끊기기 쉽습니다.
브라우저는 화면을 바꿀 때 대체로 아래 순서를 다시 밟습니다. 앞쪽 단계부터 다시 할수록 비용이 큽니다.
스타일 계산 → 레이아웃(리플로우) → 페인트(리페인트) → 합성
크기·위치가 바뀌면 색·그림자만 바뀌면 transform·opacity만 바뀌면
여기부터 다시 여기부터 다시 여기만 다시이 프로젝트의 코드는 이 관점에서 비교적 가벼운 편입니다.
scrollY를 한 번 읽고, 클래스를 붙였다 떼기만 합니다.classList.toggle은 값이 이미 같으면 실제로 바뀌는 것이 없으므로, 60px·300px 경계를 넘나드는 순간에만 화면이 다시 그려집니다.- 헤더의
is-scrolled는 배경색·그림자만 바꾸므로 레이아웃까지 다시 하지 않고 페인트만 다시 합니다. - 리스너를
addEventListener('scroll', updateScrollUi, { passive: true })로 등록합니다.passive: true는 "이 리스너는preventDefault()로 스크롤을 막지 않는다"고 브라우저에 미리 알려 주는 옵션이라, 브라우저가 리스너가 끝나기를 기다리지 않고 바로 스크롤할 수 있습니다.
다만 기능을 더 붙일 때는 다음을 조심하는 것이 좋습니다.
| 조심할 점 | 이유 | 대안 |
|---|---|---|
스크롤마다 offsetTop, getBoundingClientRect()를 여러 번 읽고 쓰기를 반복 | 읽을 때마다 브라우저가 레이아웃을 즉시 다시 계산함 | 읽기를 먼저 모아서 한 번에 하고, 쓰기는 그다음에 |
스크롤 위치에 따라 top, height를 직접 바꿈 | 매번 리플로우 발생 | transform으로 움직이기 |
| "요소가 보이는지"를 스크롤 이벤트로 계산 | 계산이 자주, 오래 일어남 | IntersectionObserver (애니메이션 효과에서 사용) |
| 핸들러 안에서 무거운 계산 | 스크롤 동안 화면이 끊김 | requestAnimationFrame으로 한 프레임에 한 번만 처리 |
리플로우와 리페인트가 무엇인지는 브라우저 동작 원리 글에서 자세히 다룹니다.
정리
| 기능 | 방식 |
|---|---|
| 부드러운 스크롤 | CSS scroll-behavior: smooth |
| 헤더 스타일 변경 | 스크롤 60px 이상 → is-scrolled |
| 맨 위로 버튼 | 스크롤 300px 이상 → is-visible, 클릭 시 scrollTo |
| 성능 | 클래스 토글만 해서 가벼움. 기능을 늘릴 때는 레이아웃 읽기/쓰기 반복을 피하기 |
출처: Codyssey-B1/B1-1
