์ด์ ์ offset ๊ธฐ๋ฐ ๋ฌดํ์คํฌ๋กค์ ๊ตฌํํ์ ๋๋ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํด์ ๋ทฐํฌํธ๋ฅผ ๊ฐ์งํ๊ณ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ๋ก๋ํ๋๋ก ํ์๋๋ฐ, ์๋ฐ์คํฌ๋ฆฝํธ ๋ฌธ์๋ฅผ ์ญ ์ฝ์ด๋ณด๋ค๊ฐ ํ์ด์ง ๋ด์ ๊ฐ ์์๊ฐ ๊ฐ๊ฐ์ ๋ชฉ์ (๊ด๊ณ , ๋ ์ด์ง ๋ก๋ฉ, ๋ฌดํ ์คํฌ๋กค ๋ฑ)์ ์ด์ ๋ก scroll ์ด๋ฒคํธ๋ฅผ ๋ฆฌ์ค๋ํ๊ธฐ ๋๋ฌธ์ ์ด์ ์์ํ๋ ์ฝ๋ฐฑ์ด ๋ฌด์ํ ์คํ๋ ์๋ ์๊ณ ์ด๋ ๋ฉ์ธ ์ค๋ ๋์ ํฐ ๋ถํ๋ฅผ ์ค ์ ์๋ค๋ ๊ฒ์ ์๊ฒ ๋์์ต๋๋ค.
๊ทธ๋์ ๋ค๋ฅธ ๊ตฌํ ๋ฐฉ๋ฒ์ ์์๋ณด๋ค๊ฐ ํ๊ฒ ์์์ ๋ทฐํฌํธ์ ๊ต์ฐจ์ ์ ๋น๋๊ธฐ์ ์ผ๋ก ๊ด์ฐฐํด์ ์์๊ฐ ๋ทฐํฌํธ์ ํฌํจ๋๋์ง ์๋์ง ๊ตฌ๋ณํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ Intersection Observer API๋ฅผ ์๊ฒ ๋์์ต๋๋ค. ์คํฌ๋กค ์ด๋ฒคํธ์ ๋น๊ตํ์ ๋ throttling/debouncing์ด ๋ถํ์ํ๋ค๋ ์ , ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ก ๋ฉ์ธ ์ค๋ ๋ ๋ธ๋กํน์ ๋ฐฉ์งํ ์ ์๋ค๋ ์ , ๋ฆฌํ๋ก์ฐ๋ฅผ ์ต์ํ์ํจ๋ค๋ ์ ์ด ์ฅ์ ์ด์๊ธฐ์ ์ด๊ฒ์ ์ฌ์ฉํด์ ๋ทฐํฌํธ๋ฅผ ๋ชจ๋ํฐ๋งํ๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค๊ฒ ๋์์ต๋๋ค.
๋จผ์ observerElement๋ผ๋ ref๋ฅผ ์์ฑํด์ ๊ด์ฐฐํ DOM ์์๋ฅผ ์ฐธ์กฐํ๊ณ , useEffect๋ก ์ํ๋ฅผ ๊ฐ์งํ๊ณ ์ถ๊ฐ ๋ก๋๋ฅผ ์ฒ๋ฆฌํ์ต๋๋ค.
type Props = {
isLoadingInitial: boolean;
isLoadingMore: boolean;
children: React.ReactNode;
loadMore: () => void;
}
์๋ก์ด IntersectionObserver ์ธ์คํด์ค๋ฅผ ์์ฑํ๊ณ , ๊ด์ฐฐ์์๊ฒ ์ฌ์ฉํ ์ฝ๋ฐฑ handleIntersection๊ณผ ์ค์ ์ ์ ๋ฌํฉ๋๋ค. Intersection Observer๊ฐ ๊ฐ์งํ ์ํธ๋ฆฌ๋ค์ ์ํํ๋ฉด์ ์กฐ๊ฑด์ ๋ง์กฑํ ๊ฒฝ์ฐ loadMore ํจ์๋ฅผ ํธ์ถํฉ๋๋ค.
- ๋ทฐํฌํธ๋ฅผ ๊ธฐ์ค์ผ๋ก, 100px ์ฌ์ ๋ฅผ ๋์ด ๋ทฐํฌํธ์ ๊ฐ๊น์์ก์ ๋ ๋ก๋๊ฐ ์์๋๋๋ฐ ์์์ ์ผ๋ถ๋ผ๋ ๋ณด์ผ ๊ฒฝ์ฐ ์ฝ๋ฐฑ์ด ํธ์ถ๋๋๋ก ์ค์ ํ์ต๋๋ค.
- observerElement๊ฐ ํ์ฌ ์กด์ฌํ๋ค๋ฉด ์ด๋ฅผ ๊ด์ฐฐํ๊ฒ ์ค์ ํ๊ณ , ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์ observer์ ๊ด์ฐฐ์ ์ค์งํ๋๋ก ํฉ๋๋ค.
์์กด์ฑ์๋ isLoadingMore, isLoadingInitial, loadMore์ ๋ด์์ ํ์ํ ๊ฒฝ์ฐ์๋ง useEffect๊ฐ ์คํ๋๋๋ก ํ์ต๋๋ค.
export default function InfiniteScroll({ isLoadingInitial, isLoadingMore, children, loadMore }: ScrollProps) {
const observerElement = useRef<HTMLDivElement | null>(null);
useEffect(() => {
function IntersectionAPI(entries: IntersectionObserverEntry[]) {
entries.forEach((entry) => {
if (entry.isIntersecting && (!isLoadingMore || !isLoadingInitial)) {
loadMore();
}
});
}
const observer = new IntersectionObserver(IntersectionAPI, {
root: null,
rootMargin: "100px",
threshold: 0,
});
if (observerElement.current) {
observer.observe(observerElement.current);
}
return () => observer.disconnect();
}, [isLoadingMore, isLoadingInitial, loadMore]);
return (
<>
{children}
<div ref={observerElement} id="obs">
</div>
</>
);
}
{children} ์ ๊ฐ๋ค์ด ์ญ ์คํฌ๋กค ๋๋ค๊ฐ ref๊ฐ ๊ฑธ๋ฆฐ <div>์ ๋ฟ์ผ๋ฉด observerElement๊ฐ ์คํ๋๋๋ก ํ์ต๋๋ค.
data fetching ํ๋ ํจ์๋ zustand๋ฅผ ์ฌ์ฉํด์ ์ ์ญ์์ ์ํ ๊ด๋ฆฌ ์ค์ด์๋๋ฐ, ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ๋ฉด์ tasks, currentPage, hasMore ํญ๋ชฉ์ ์ถ๊ฐํ์ต๋๋ค.
์ฌ์ฉํ fetchTaskToday์ ๋งค๊ฐ๋ณ์๋ก page๋ฅผ ์ฃผ์๊ณ , ๊ธฐ๋ณธ๊ฐ์ 0์ ๋๋ค.
export const useTaskStore = create<TodoState>(set => ({
tasks: [],
currentPage: 0,
hasMore: true,
fetchTaskToday: async (page: number = 0) => {
const startDayTime = new Date()
startDayTime.setHours(0, 0, 0, 0)
startDayTime.setHours(startDayTime.getHours() + 9)
const endDayTime = new Date()
endDayTime.setHours(23, 59, 59, 999)
endDayTime.setHours(endDayTime.getHours() + 9)
const supabase = createClient()
const { data, error } = await supabase
.from('todolist')
.select('*')
.gte('due_date', startDayTime.toISOString())
.lte('due_date', endDayTime.toISOString())
.range(page * 20, (page + 1) * 20 - 1)
if (error) {
console.error('Fetching today task ERROR:', error)
return
}
if (data) {
const tasks: Task[] = data as Task[]
set(state => ({
tasks: page === 0 ? tasks : [...state.tasks, ...tasks],
hasMore: tasks.length > 0 && tasks.length === 20,
currentPage: page,
}))
}
},
...
}))
๋จผ์ ํ์ด์ง๋ค์ด์ ์ ์ํด range ๋ฉ์๋๋ก ๊ฐ ํ์ด์ง์ 20๊ฐ์ ํญ๋ชฉ๋ง ๊ฐ์ ธ์ค๋๋ก ์ค์ ํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ set ํจ์๋ฅผ ์ฌ์ฉํด Zustand์ ์ํ๋ฅผ ์ ๋ฐ์ดํธํ์ต๋๋ค.
- tasks๋ก ๊ธฐ์กด ํ์คํฌ ๋ชฉ๋ก์ ์ ์งํ๊ณ ์ดํ ํ์ด์ง์ ๋ํ ์๋ก์ด ํ์คํฌ๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
- hasMore๋ ํ์คํฌ ์๊ฐ 20์ธ ๊ฒฝ์ฐ true๋ก ์ค์ ๋์ด ์ถ๊ฐ ๋ฐ์ดํฐ๊ฐ ๋ ์๋์ง๋ฅผ ๋ํ๋ ๋๋ค.
- currentPage๋ฅผ ์ ๋ฐ์ดํธํ์ฌ ํ์ฌ ํ์ด์ง๋ฅผ ์ถ์ ํ๋๋ก ํ์ต๋๋ค.
๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ ์ปดํฌ๋ํธ์์ useTaskStore์์ ์ ์ธํ tasks, fetchTaskToday, currentPage, hasMore์ ๊ฐ์ ธ์๊ณ
useState๋ก isLoadingMore์ ๊ด๋ฆฌํ๋๋ก ํ๋ฉด์ InfiniteScroll ์ปดํฌ๋ํธ์ props๋ก ๋ด๋ ค์ค isLoadingMore={} ์ ํ ๋นํด์ฃผ์์ต๋๋ค.
const tasks = useTaskStore(state => state.tasks)
const fetchTaskToday = useTaskStore(state => state.fetchTaskToday)
const currentPage = useTaskStore(state => state.currentPage)
const hasMore = useTaskStore(state => state.hasMore)
const [isLoadingMore, setIsLoadingMore] = useState(false)
useEffect(() => {
const loadTasks = async () => {
if (!hasMore) return
setIsLoadingMore(true)
await fetchTaskToday()
setIsLoadingMore(false)
}
loadTasks()
}, [fetchTaskToday, hasMore])
const loadMoreTasks = async () => {
if (!hasMore || isLoadingMore) return
setIsLoadingMore(true)
await fetchTaskToday(currentPage + 1)
setIsLoadingMore(false)
}
์ปดํฌ๋ํธ์ ๋ง์ดํธ ๋์์ ๋ fetchTaskToday()๋ฅผ ํธ์ถํ๋๋ก ํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ hasMore๊ณผ isLoadingMore ๋ ์ค ํ๋๋ผ๋ false๋ผ๋ฉด return ํ๋๋ก ํ๋ฉด์ ๋๋ค ๋ง์กฑํ๋ค๋ฉด fetchTaskToday์ ํ์ด์ง๋ฅผ ๋๊ฒจ์ ํธ์ถํ๋๋ก ํ๋ ํจ์๋ฅผ ์์ฑํ๊ณ InfiniteScroll ์ปดํฌ๋ํธ์ props๋ก ๋ด๋ ค์ฃผ๋๋ก ํด์ ์คํฌ๋กค์ด ๋ฐ๋ฅ์ ๋๋ฌํ์ ๋ ์ถ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๋๋ก ํ์ต๋๋ค.
<InfiniteScroll isLoadingInitial={false} isLoadingMore={isLoadingMore} loadMore={loadMoreTasks}>
{tasks.map(task => (
<TodoItem
key={task.todo_id}
task={task}
viewTaskBtn={viewTaskBtn}
checkTask={checkingBox}
checked={!!checkedTask[task.todo_id.toString()]}
/>
))}
</InfiniteScroll>
์ด๋ ๊ฒ IntersectionObserver๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ด์์ ๋ณํ์ ๋ํ ์ฑ๋ฅ ๋ถ๋ด์ ์ค์ด๊ณ , ํด๋ผ์ด์ธํธ์ ๋ถํ๋ฅผ ์ต์ํํ๋ ์คํฌ๋กค ๊ฐ์ง ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์ต๋๋ค.
๊ตฌํ ์ ๋ชจ์ต์์๋ ์ฒ์ ํ์ด์ง๊ฐ ๋ก๋๋์์ ๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ๋ฒ์ ๊ฐ์ ธ์ต๋๋ค.
๊ตฌํ ์ (๋ชจ๋ฐ์ผ)

๊ตฌํ ํ, ์คํฌ๋กค๋ฐ๋ฅผ ๋ณด๋ฉด rootMargin ์ค์ ์ผ๋ก ์ธํด ๋ฐ๋ฅ์ ๋ฟ๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ๋ก ๊ฐ์ ธ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
๊ตฌํ ํ

'What I Learn' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| useCustom hook์ ๋ง๋ค์ด ์๋น์ค ํค๋์ ๋ค๋ก๊ฐ๊ธฐ ๊ธฐ๋ฅ ๊ตฌํ (0) | 2025.02.05 |
|---|---|
| husky์ GitHub Actions์ ์ฌ์ฉํ CICD (2) | 2025.02.05 |
| Next.js ๋ณ๋ ฌ ๋ผ์ฐํ & ๊ฒฝ๋ก ๊ฐ๋ก์ฑ๊ธฐ๋ฅผ ํตํ ๋ชจ๋ฌ ํ์ด์ง ๊ตฌํ 2 (0) | 2024.12.26 |
| e.stopPropagation(), ์ด๋ฒคํธ ์ ํ ๋ฐฉ์ง (0) | 2024.12.10 |
| format date as 'YYYY.MM.DD AMhh:mm' (0) | 2024.12.10 |
