๋ฌดํ ์คํฌ๋กค ์ ๋ค์ํ ์น์ฌ์ดํธ, ์ดํ๋ฆฌ์ผ์ด์ ์์ ๋ณผ ์ ์๋ค. ์ผ๋ฐ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๊ณ ์ถ์ ๋ฐ์ดํฐ๊ฐ ๋ง์ ๊ฒฝ์ฐ ์ด๋ฅผ ํ๋ฒ์ ๋ชจ๋ ๋ ๋๋ง ํ๋ ๊ฒ์ ๋นํจ์จ์ ์ด๋ค. ์๋ํ๋ฉด ์ฌ์ฉ์๊ฐ ๊ทธ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ณผ ํ๋ฅ ๋ ๋ฎ๊ณ ์ฌ์ฉ์๋ ๊ทธ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ฐ๋ ๋คํธ์ํฌ ๋ฆฌ์์ค ๋ญ๋น๊ฐ ๋ฐ์ํ๋ฉด์ ๋๊ธฐ์ ์ด๊ธฐ ๋ก๋ฉ ์๋๊ฐ ๋๋ ค์ง๊ธฐ ๋๋ฌธ์ด๋ค. ๋ง์ฝ ์ฐ๋ฆฌ ์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ๋ ์ฌ์ฉ์๋ค์ด ๋ฐฐํฐ๋ฆฌ๋ ๋นจ๋ฆฌ ๋ณ๊ณ ๋ฐ์ดํฐ ์๋ชจ๋๋ ๋์ด๋๋๊ฑธ ๋ฐ๊ฒฌํ๋ค๋ฉด ๋ค์ ๋ฐฉ๋ฌธํ์ง ์์ ๊ฐ๋ฅ์ฑ์ด ๋์ ์ง ์๋ ์๊ฒ ๋ค.
๋ฐ์ดํฐ๊ฐ ๋ง๋ค -> ๋๋ ์ ๋ณด์ฌ์ฃผ์ -> ํ์ด์ง๋ค์ด์ or ๋ฌดํ ์คํฌ๋กค ๋ฑ์ผ๋ก ์๊ฐ์ ์ฎ๊ฒจ ๋ณผ ์ ์๋๋ฐ ํ์ด์ง๋ค์ด์ ๊ณผ ๋ฌดํ์คํฌ๋กค์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํด์ฃผ๋ ์ฑ๊ฒฉ์ด ์กฐ๊ธ ๋ค๋ฅธ ๊ฒ ๊ฐ๋ค. ํ์ด์ง๋ค์ด์ ๊ฐ์ ๊ฒฝ์ฐ ์ฌ๋ฌ๋ฐ์ดํฐ๋ฅผ ํ์ด์ง๋ก ์ชผ๊ฐ์ ๋๋๋๋ฐ ์ฒซ๋ฒ์งธ ์๋ฏธ๊ฐ ์๊ณ ๋๋ฒ์งธ ์๋ฏธ๋ ์ฌ์ฉ์์๊ฒ ๋ฐ์ดํฐ์ ์์น๋ฅผ ํน์ ํ ์ ์๋๋ก ๋์์ฃผ๋ ๊ฒ์ด๋ค. ๊ทธ๋์ 1ํ์ด์ง์ 3๋ฒ์งธ๊ธ, 3ํ์ด์ง์ 9๋ฒ์งธ ๊ธ ์ฒ๋ผ ์์น๋ฅผ ๊ธฐ์ตํด์ ๋ค์ ํด๋น ๊ฒ์๋ฌผ์ ์ ๊ทผ ํ ์ ์๋ค.
๋ฐ๋ฉด์ ๋ฌดํ ์คํฌ๋กค์ ์์น๋ฅผ ํน์ ํ ์ ์์ง๋ง ํ ํ์ด์ง ๋ด์์ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ ๋๋ง ํ ์ ์๋ค. ๊ทธ๋์ ์ข ๋ ๋์ UX ๋ฅผ ๋ณด์ฌ์ค ์ ์๋ค.
์ธ์คํ๊ทธ๋จ์ด๋ ํ์ด์ค๋ถ ๊ฐ์ ๊ฒฝ์ฐ๊ฐ ๊ทธ๋ฐํ๋ฐ ๋ฐฉ๊ธ ์ ์ ๋ดค์๋ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ณด๊ณ ์ถ์ ๋ ํ์ด์ง๋ฅผ ์๋ฌด๋ฆฌ ๋ด๋ ค๋ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ์ํ ์๋ ์๋ค. ๊ธฐ์ ๋ค์ ์ฌ๊ธฐ์ ๋ฌด์จ ์ธ๊ณต์ง๋ฅ์ ์ด์ฉํด์ ๊ฐ์ธํ ๋ ์ถ์ฒ ์์คํ ์ ๋์ ํ๊ณ ์ฌ์ฉ์๊ฐ ์ข์ํ ๋งํ ๋ฐ์ดํฐ๋ฅผ ๋ง๊ตฌ์ก์ด๋ก ๋ผ์ ๋ฃ์ด์ฃผ๋ ๊ฒ ๊ฐ๋ค.
์ผ๋จ ์นดํค์คํก์์๋ ๊ทธ๋ฅ ์ฑํ ๋ฐ์ดํฐ๊ฐ ๋๋ฌด ๋ง์ผ๋ฉด ํ๋ฒ์ ๋ค ๋ ๋๋ง ํ ์ ์๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ๋๋ ์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด์ ๋ฌดํ ์คํฌ๋กค์ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค. ๋๋ถ๋ถ์ ์ฑํ ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ค ์ด๋ฐ๋ฐฉ์์ผ ๊ฑฐ๋ค.
์์๋ณธ ๊ฒฐ๊ณผ ๋ฌดํ ์คํฌ๋กค์ scroll
์ด๋ฒคํธ, intersection observer API
๋๊ฐ์ง ๋ฐฉ์์ผ๋ก ๊ตฌํ ๊ฐ๋ฅํ๋ค.
window.addEventListener('scroll', () => {
const y = window.scrollY
const clientHeight = document.element.clientHeight
const scrollHeight = document.element.scrollHeight
if(y + clientHeight > scrollHeight) {
// ๋ฐ์ดํฐ ๋ ๋ถ๋ฌ์ค๊ธฐ
}
})
์์ ์ฝ๋์ฒ๋ผ scroll
์ด๋ฒคํธ์ ๋ฆฌ์ค๋์์ ํ์ฌ ์คํฌ๋กค์์น๋ฅผ ํ์
ํด์ ๋ง์ฝ ์ ์ฒด ์คํฌ๋กค ๊ธธ์ด๋ณด๋ค ๊ธธ๊ฒ ๋ด๋ ค ๊ฐ๋ค๋ฉด ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ฉด ๋๋ค.
์คํฌ๋กค ํฌ์ง์
์ ์ฌ๋ฌ๊ฐ์ง ์์๊ฐ ์๋๋ฐ ์์ ๊ทธ๋ฆผ์ ๋ณด๋ฉด ๋์ถฉ ์ ์ ์๋ค.
๊ตฌํํ๊ธฐ๋ ๋งค์ฐ ๊ฐ๋จํ์ง๋ง ์กฐ๊ธ๋ ๋ง์ ธ์ค์ผํ ๋ถ๋ถ์ด ์๋ค. scroll
์ด๋ฒคํธ๋ ๋งค์ฐ ๋น๋ฒํ๊ฒ ์ผ์ด๋๊ธฐ ๋๋ฌธ์ ๊ทธ๋๋ง๋ค ์ ์ฝ๋ฐฑํจ์๊ฐ ๋์ํ๋ค๋ฉด ์น ์ดํ๋ฆฌ์ผ์ด์
์ฑ๋ฅ์ ์์ข์ ์ํฅ์ ๋ผ์น๋ค. ์๋ํ๋ฉด scroll
์ ํ๋ฉด์ ํ์ฌ ์์น๋ฅผ ์์๋ด๊ธฐ ์ํด clientHeight
๊ฐ์ ๊ณ์ฐ๋ ํ๋กํผํฐ ๊ฐ์ ์๊ตฌํ๋๋ฐ ์ด๋ ๋ธ๋ผ์ฐ์ ์ reflow
๋ฅผ ์ ๋ฐํ๊ธฐ ๋๋ฌธ์ด๋ค. - reflow ์ ๋ฐํ๋ ๊ฒ๋ค
- Throttle : ๋ง์ง๋ง ํจ์๊ฐ ํธ์ถ๋ ํ ์ผ์ ์๊ฐ์ด ์ง๋๊ธฐ ์ ์ ๋ค์ ํธ์ถ๋์ง ์๋๋ก ํ๋ ๊ฒ
- Debounce : ์ฐ์ด์ด ํธ์ถ๋๋ ํจ์๋ค ์ค ๋ง์ง๋ง ํจ์(๋๋ ์ ์ผ ์ฒ์)๋ง ํธ์ถํ๋๋ก ํ๋ ๊ฒ
๊ทธ๋์ throttle
์ด๋ debounce
๋ฅผ ๊ฑธ์ด์ ๋ชจ๋ ์์ฒญ์ ๊ฑธ์ง ์๊ณ ์ฑ๋ฅ์ ๋์ฌ์ฃผ๋ ๋ฑ์ ์ถ๊ฐ์์
์ ํด์ค์ผ ํ๋ค.
Intersection Observer API๋ ํ๊ฒ ์์์ ์์ ์์ ๋๋ ์ต์์ document ์ viewport ์ฌ์ด์ intersection ๋ด์ ๋ณํ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ๊ด์ฐฐํ๋ ๋ฐฉ๋ฒ์ ๋๋ค. - MDN
์ฌ์ค ์ด ๋ง์ ์์ง๋ ์ดํดํ์ง ๋ชปํ๊ฒ ๋ค. ์ด์จ๋ element ๊ฐ ํ๋ฉด์ ๋ณด์ด๋ฉด ๊ทธ๊ฑธ ์์์ฑ ์ ์๋๋ก ํด์ค๋ค๋ ๊ฑฐ๊ณ ์ฌ๋ฌ๊ฐ์ง ์ฉ๋๋ก ์ฌ์ฉ๋ ์ ์๋ค. ์นดํค์คํก์์๋ 2๋ฒ์ ์ด์ ๋ก ์ฌ์ฉํ๋ค.
- ํ์ด์ง๊ฐ ์คํฌ๋กค ๋๋ ๋์ค์ ๋ฐ์ํ๋ ์ด๋ฏธ์ง๋ ๋ค๋ฅธ ์ปจํ ์ธ ์ ์ง์ฐ ๋ก๋ฉ.
- ์คํฌ๋กค ์์, ๋ ๋ง์ ์ปจํ ์ธ ๊ฐ ๋ก๋ ๋ฐ ๋ ๋๋ง๋์ด ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์ด๋ํ์ง ์์๋ ๋๊ฒ ํ๋ infinite-scroll ์ ๊ตฌํ.
- ๊ด๊ณ ์์ต์ ๊ณ์ฐํ๊ธฐ ์ํ ์ฉ๋๋ก ๊ด๊ณ ์ ๊ฐ์์ฑ ๋ณด๊ณ .
- ์ฌ์ฉ์์๊ฒ ๊ฒฐ๊ณผ๊ฐ ํ์๋๋ ์ฌ๋ถ์ ๋ฐ๋ผ ์์ ์ด๋ ์ ๋๋ฉ์ด์ ์ ์ํํ ์ง ์ฌ๋ถ๋ฅผ ๊ฒฐ์ .
Intersection Observer
๊ฐ์ ๊ฒฝ์ฐ scroll
์ด๋ฒคํธ๋ณด๋ค ์ฝ๊ฐ ์ ์ฝ์ฌํญ์ด ์๋๊ฒ ๋ชจ๋ ๋ธ๋ผ์ฐ์ ์์ ์ง์ํ์ง ์๋๋ค. ์ ๋งํ ๋ธ๋ผ์ฐ์ ๋ ๋ค ์ง์๋์ง๋ง IE
๋ฅผ ๊ผญ ์ง์ํด์ผํ๋ ์ํฉ์ด๋ผ๋ฉด ๊ทธ๊ฒ๋ ๊ทธ๊ฒ๋๋ก ๊ท์ฐฎ๊ฒ ๋ค.
์ผ๋จ ๋น๋๊ธฐ์ ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ๋ฉ์ธ์ค๋ ๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๊ทธ๋งํผ ์ฑ๋ฅ์์ ์ด์ ์ ์ป์ ์ ์๋ค. scroll
์ด๋ฒคํธ๋ก ๊ตฌํํ ๋ ์ฒ๋ผ throttle
์ด๋ debounce
์ฒ๋ฆฌ๋ฅผ ํด์ค ํ์๊ฐ ์๋ค.
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
DependencyList, useCallback, useEffect, useRef,
} from 'react'
interface UseIntersectionObserver {
(cb: (entry: IntersectionObserverEntry) => void,
options: IntersectionObserverInit,
deps: DependencyList): (node: any) => void
}
const useIntersectionObserver: UseIntersectionObserver = (cb, options, deps) => {
const intersectionObserver = useRef<IntersectionObserver | null>(null)
return useCallback((node) => {
if (intersectionObserver.current) {
intersectionObserver.current.disconnect()
}
intersectionObserver.current = new IntersectionObserver(([entry]) => {
cb(entry)
}, options)
if (node) intersectionObserver.current.observe(node)
}, deps)
}
export default useIntersectionObserver
์ผ๋จ ์ด๋ ๊ฒ ์๊ธด custom hook ์ ๋ง๋ค์ด ๋๋๋ค. ์ฌ๊ธฐ์ ํด์ฃผ๋ ์ผ์ ํน์ ์์ญ์ ๋ถ์ผ ์ ์๋ ํจ์๋ฅผ ์์ฑํด์ฃผ๋๊ฒ. ์ด ํจ์๋ (node) => void
ํํ์ธ๋ฐ ์ด๋ฐ ํํ์ ํจ์๋ jsx
์์ ref
์์ฑ์ ๋ฃ์ด์ค ์ ์๋ค. ๊ทธ๋ฌ๋ฉด ํด๋น element
๊ฐ viewport
์ ๊ต์ฐจ๋๋ฉด ํจ์๊ฐ ์คํ๋๋ ํํ๋ค.
์ง๊ธ ์นดํค์คํก ์ฑํ
๋ฐฉ์์๋ ๋ฐฉ ์ปดํฌ๋ํธ ์์ ์ฑํ
์ ๋ณด์ฌ์ฃผ๋ ์ปดํฌ๋ํธ๊ฐ ์๊ณ ๊ฑฐ๊ธฐ์๋ ์ปดํฌ๋ํธ๊ฐ ๋ ์ชผ๊ฐ์ง๋ค. ํ๋ฒ์ ๋ชฐ์์ฐ๋ฉด ๊ตฌํ์ด ์ฌ์์ง๊ฒ ์ง๋ง ๊ด๋ฆฌํ๊ธฐ๊ฐ ๋๋ฌด ๋๋ฌ์์ง๊ธฐ ๋๋ฌธ์ ์ ์ ํ ์ชผ๊ฐ๋ค ๋ณด๋ forward ref
๋ฅผ ์ด์ฉํด์ useIntersectionObserver hook
์ ํตํด ์์ฑํ ํจ์๋ฅผ ์ ๋ฌํด์ ref
์์ฑ์ ๋ฃ์ด์ฃผ๊ฒ ๋์๋ค.
// firstChat ๋ ์ฒซ๋ฒ์งธ ์ฑํ
์ ์๋ฏธํจ
// ์ต์๋จ chat ์ปดํฌ๋ํธ์ ๋ถ์ฌ์ค๋ค.
const firstChat = useIntersectionObserver(
(entry: IntersectionObserverEntry) => {
if (entry.time < 5000) return
if (!entry.isIntersecting) {
return
}
dispatch(loadMoreRequest(roomUuid))
},
{ root: root.current },
[roomUuid]
)
...
chats.map((chat, idx) => (
<div key={chat.uuid} ref={idx === chats.length - 1 ? lastTop : null}>
<ChatBox
chat={chat}
isMine={userUuid === chat.metaInfo.sender.uuid}
// ์ต์๋จ ์ฑํ
์ด๋ฉด์ ์ฒซ๋ฒ์งธ ์ฑํ
์ด ์๋๊ฒฝ์ฐ (์ฒซ๋ฒ์จฐ ์ฑํ
์ด๋ฉด ๋ ์์ฒญํ ๊ฒ ์์ผ๋ฏ๋ก)
ref={idx === chats.length - 1 && chat.uuid !== firstChat.uuid ? first : null}
/>
</div>
))