Skip to content

스크롤 인터랙션 ​

스크롤과 관련된 기능 세 가지(부드러운 스크롤, 헤더 스타일 변경, 맨 위로 버튼)를 모았습니다.

부드러운 스크롤 ​

이 프로젝트는 부드러운 스크롤을 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