인터넷에서 맨날 섹션 별로 스크롤 되는 이벤트 구현할 때 이 티스토리 코드 사용해서 구현했었는데, jquery를 이용하여 구현하기 때문에 이번엔 javascript만을 이용해서 구현하고 싶었다.
html {
scroll-behavior: smooth; /* 스크롤 부드럽게 */
}
body {
overflow: hidden; /* scroll 막음 */
}
Next.js로 개발을 진행했기 때문에 useEffect 안에 넣어줬다. (window undefined 에러가 나기 때문)
const scrollUpByViewportHeight = () => {
const viewportHeight = window.innerHeight
const currentPosition = window.scrollY
if (currentPosition >= viewportHeight) {
window.scrollTo({
top: currentPosition - viewportHeight,
})
} else {
// 이미 제일 위에 있으면
window.scrollTo({
top: 0,
})
}
}
const scrollDownByViewportHeight = () => {
const viewportHeight = window.innerHeight
const currentPosition = window.scrollY
const documentHeight = document.body.scrollHeight
if (currentPosition + viewportHeight < documentHeight) {
window.scrollTo({
top: currentPosition + viewportHeight,
})
} else {
// 이미 제일 아래에 있으면
window.scrollTo({
top: documentHeight,
})
}
}
useEffect(() => {
let canTriggerScroll = true
const handleWheel = (e: WheelEvent) => {
if (!canTriggerScroll) return
canTriggerScroll = false
if (e.deltaY < 0) scrollUpByViewportHeight()
else scrollDownByViewportHeight()
setTimeout(function () {
canTriggerScroll = true
}, 1100) // 1초 대기
}
// mobile
let startTouchY: number
const handleTouchStart = (e: TouchEvent) => {
if (!canTriggerScroll) return
startTouchY = e.touches[0].clientY
}
const handleTouchEnd = (e: TouchEvent) => {
if (!canTriggerScroll) return
const endTouchY = e.changedTouches[0].clientY
const deltaY = startTouchY - endTouchY
if (Math.abs(deltaY) <= 20) return // delta 값이 작은 경우
canTriggerScroll = false
if (deltaY < 0) scrollUpByViewportHeight()
else scrollDownByViewportHeight()
setTimeout(function () {
canTriggerScroll = true
}, 1000) // 1초 대기
}
window.addEventListener('wheel', handleWheel, { passive: false })
window.addEventListener('touchstart', handleTouchStart)
window.addEventListener('touchend', handleTouchEnd)
return () => {
window.removeEventListener('wheel', handleWheel)
window.removeEventListener('touchstart', handleTouchStart)
window.removeEventListener('touchend', handleTouchEnd)
}
ScrollUpByViewportHeight, ScrollDownByViewportHeight 두 개의 함수가 있다.
이 함수는 챗지피티가 만들어줬다.
const scrollDownByViewportHeight = () => {
const viewportHeight = window.innerHeight
const currentPosition = window.scrollY
const documentHeight = document.body.scrollHeight
if (currentPosition + viewportHeight < documentHeight) {
window.scrollTo({
top: currentPosition + viewportHeight,
})
} else {
// 이미 제일 아래에 있으면
window.scrollTo({
top: documentHeight,
})
}
}
ScrollUp 함수는 현재 위치에서 화면 높이 만큼 빼준 위치로 scroll 해주므로 위의 섹션으로 이동한다.
반대로 Down은 더해준 위치로 scroll 하여 아래 섹션으로 이동한다.
먼저, wheel 이벤트를 이용해서 마우스 휠을 위로 올리면 scrollUp, 내리면 scrollDown 함수를 실행하였다.
const handleWheel = (e: WheelEvent) => {
if (e.deltaY < 0) scrollUpByViewportHeight()
else scrollDownByViewportHeight()
}
window.addEventListener('wheel', handleWheel, { passive: false })
근데 문제가 발생했다. scrollEvent가 발생하고 있는 와중에 또 wheelEvent가 발생하면 이상한 위치에서 스크롤이 멈췄다. 즉, wheel을 미세하게 딱 한번만 돌려야 정상작동했다..!
해결 방법을 열심히 찾다가 wheel 모션이 시작되면 canTriggerScroll 변수를 false로 바꾸고 setTimeout을 이용하여 약 1초 대기한 후 true로 바꿔주는 방법을 생각해냈다.
당연히 canTrigerScroll이 `false일 때는 return해서 wheel event가 실행되지 않도록 했다.
let canTriggerScroll = true
const handleWheel = (e: WheelEvent) => {
if (!canTriggerScroll) return
canTriggerScroll = false
if (e.deltaY < 0) scrollUpByViewportHeight()
else scrollDownByViewportHeight()
setTimeout(function () {
canTriggerScroll = true
}, 1100) // 1초 대기
}
window.addEventListener('wheel', handleWheel, { passive: false })
mobile 같은 경우에는 (TouchStart Event에서 첫번째 터치의 Y 값) - (TouchEnd Event에서 마지막 터치의 Y 값)으로 deltaY 값을 구했다.
여기서도 마찬가지로 canTriggerScroll 변수와 setTimeout을 이용하여 중복해서 이벤트가 발생하는 것을 막았다.
하지만 모바일에서는 또 다른 문제가 발생했다. 휠은 hover나 click과 분리되어 있지만, touch는 모바일에서의 클릭/hover와도 같으므로 header item을 클릭하는 등 스크롤 할 필요없는 경우에도 스크롤 이벤트가 실행이 됐다.
delta 값이 작은 경우 return 해줘서 문제를 해결했다.
// mobile
let startTouchY: number
const handleTouchStart = (e: TouchEvent) => {
if (!canTriggerScroll) return
startTouchY = e.touches[0].clientY
}
const handleTouchEnd = (e: TouchEvent) => {
if (!canTriggerScroll) return
const endTouchY = e.changedTouches[0].clientY
const deltaY = startTouchY - endTouchY
if (Math.abs(deltaY) <= 20) return // delta 값이 작은 경우
canTriggerScroll = false
if (deltaY < 0) scrollUpByViewportHeight()
else scrollDownByViewportHeight()
setTimeout(function () {
canTriggerScroll = true
}, 1000) // 1초 대기
}
window.addEventListener('touchstart', handleTouchStart)
window.addEventListener('touchend', handleTouchEnd)