[React] ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด๋ณด๋ ๋ฌดํ ์คํฌ๋กค Infinite Scroll
1) ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ ๋ฐฉ๋ฒ
๋ฌดํ ์คํฌ๋กค์ ํ์ฌ ํ์ด์ง์์ ์คํฌ๋กค๋ฐ๊ฐ ๋ง์ง๋ง ์ฝํ ์ธ ์ง์ ์ ์์ ๋ ๋ค์ ์ฝํ ์ธ ๋ฅผ ์๋์ผ๋ก ๋ถ๋ฌ์ค๋ ๊ตฌํ ๋ฐฉ์์ ๋งํ๋ค. โ์คํฌ๋กคํด์ ๊ฐ๋ ค์ง ์์ญ์ ๋์ด์ โํ์ฌ ํ๋ฉด(๋ทฐํฌํธ)์ ๋์ด๋ฅผ ๋ํ ๊ฐ์ด โ์ ์ฒด ๋ฌธ์์ ๋์ด์ ๊ฐ๋ค๋ฉด ํ์ฌ ์คํฌ๋กค์ด ๊ฐ์ฅ ํ๋จ ๋์ ๋๋ฌํ๋ค๋ ๊ฑธ ์ ์ ์๋ค.
์์์ผ ํ ๊ธฐํ ํ๋กํผํฐ
โถ ์คํฌ๋กคํด์ ๊ฐ๋ ค์ง ์ฝํ
์ธ ์์ญ์ ๋์ด : document.documentElement.scrollTop
โท ํ์ฌ ํ๋ฉด(๋ทฐํฌํธ)์ ๋์ด
window.innerHeight
: ์คํฌ๋กค๋ฐ ํฌํจdocument.documentElement.clientHeight
: ์คํฌ๋กค๋ฐ ์ ์ธ
โธ ์ ์ฒด ๋ฌธ์์ ๋์ด
// ๋ฌธ์์ ์ ํํ ์ ์ฒด ๋์ด๋ฅผ ๊ตฌํ๊ธฐ ์ํ ์ฝ๋
const scrollHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.body.clientHeight,
document.documentElement.clientHeight,
);
์ ์ด๋ฐ ๋ฐฉ์์ผ๋ก ๋ฌธ์ ์ ์ฒด ๋์ด๋ฅผ ๊ตฌํด์ผ ํ๋ ๊ฑธ๊น์? ์ด์ ๋ ์์๋ณด์ง ์๋ ๊ฒ ๋ซ์ต๋๋ค.
์ด๋ฐ ์ด์ํ ๊ณ์ฐ๋ฒ์ ์์ฃผ ์ค๋ ์ ๋ถํฐ ์์๊ณ ๊ทธ๋ค์ง ๋ ผ๋ฆฌ์ ์ด์ง ์์ ์ด์ ๋ก ๋ง๋ค์ด์ก๊ธฐ ๋๋ฌธ์ ๋๋ค.
— JavaScript Info
Custom Hook ์์ฑ
์ปดํฌ๋ํธ์ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ ์ฝ๋๋ฅผ ์ง์ ์์ฑํ๊ธฐ๋ณด๋จ ์ฌ์ฌ์ฉํ ์ ์๋๋ก Custom Hook์ผ๋ก ๋ง๋๋ ๊ฒ ์ข๋ค. ๋ฌดํ ์คํฌ๋กค Custom Hook์ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํด์ ๋ฐ์ดํฐ๋ฅผ ๋ ๋ฐ์์ฌ์ง ์ฌ๋ถ๋ฅผ ์๋ ค์ฃผ๋ ์ญํ ์ ํ๋ค.
โ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ํ์ฌ ์คํฌ๋กค ์์น๊ฐ ์ ์ฒด ๋ฌธ์์ ๋์ ๋๋ฌํ๋์ง ํ์ธํ๋ค. โ์คํฌ๋กค์ด ๋ฌธ์ ๋์ ์์น ํ๋ค๋ฉด isFetching
์ํ๋ฅผ true
๋ก ๋ณ๊ฒฝํ๊ณ , โํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ callback
์ ํธ์ถํ๋ค. callback
ํจ์๋ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์์
์ ์ํํ๋ค.
// useInfiniteScroll.js
import { useEffect, useState } from 'react';
import { getScrollHeight } from '../../lib/utils';
const useInfiniteScroll = (callback) => {
const [isFetching, setIsFetching] = useState(false);
const handleScroll = () => {
// โต ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ์ ์ฒด ๋ฌธ์์ ๋์ ์์นํ๋์ง ํ์ธ
const scrollHeight = getScrollHeight(); // ์ ์ฒด ๋ฌธ์์ ๋์ด๋ฅผ ๊ตฌํ๋ ์ ํธ ํจ์
const currentScroll = window.innerHeight + document.documentElement.scrollTop;
if (currentScroll === scrollHeight && !isFetching) setIsFetching(true);
};
useEffect(() => {
// โด ์คํฌ๋กค ์ด๋ฒคํธ ๊ฐ์ง
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (isFetching) callback(); // โถ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ callback ์คํ
}, [isFetching]);
return [isFetching, setIsFetching];
};
export default useInfiniteScroll;
Custom Hook ์ ์ฉ
๋ฆฌ์คํธ๋ฅผ ์ถ๋ ฅํ๋ InfiniteScrollNonIO
์ปดํฌ๋ํธ์์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์์
์ ์ํํ๋ ํจ์๋ฅผ Custom Hook์ ์ธ์(callback
)๋ก ๋๊ธด๋ค. ๊ทธ๋ผ ์คํฌ๋กค์ด ๋ฌธ์ ๋์ ๋๋ฌํ์ ๋ ์ธ์๋ก ๋๊ธด ํจ์๊ฐ ์คํ๋๊ณ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์จ ํ ํ๋ฉด์ ๋ ๋๋งํ๋ค.
๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ์์
์ ์ํํ๊ธฐ ์ ์ปค์คํ
ํ
์์์ ๋จผ์ isFetching
์ํ๋ฅผ true
๋ก ๋ณ๊ฒฝํ ํ ๋ฐํํ๋ค. ์ด๋ฅผ ๋ฐ์ InfiniteScrollNonIO
์ปดํฌ๋ํธ๋ “๋ก๋ฉ ์ค...” ๋ฌธ๊ตฌ๋ฅผ ํ์ํ๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐ์์ค๋ฉด isFetching
์ํ๊ฐ ๋ณ๊ฒฝ๋ผ์ “๋ก๋ฉ ์ค...” ๋ฌธ๊ตฌ๊ฐ ์ฌ๋ผ์ง๊ณ , ์๋ก ๋ฐ์์จ ๋ฐ์ดํฐ๋ฅผ ํ๋ฉด์ ์ถ๋ ฅํ๋ค.
import React, { useState } from 'react';
import useInfiniteScroll from './useInfiniteScroll';
// InfiniteScrollNonIO.js
export default function InfiniteScrollNonIO() {
const [listItems, setListItems] = useState(
Array.from(Array(30).keys(), (n) => n + 1),
);
const [isFetching, setIsFetching] = useInfiniteScroll(fetchMoreListItems);
function fetchMoreListItems() {
setTimeout(() => {
setListItems((prevState) => [
...prevState,
...Array.from(Array(20).keys(), (n) => n + prevState.length + 1),
]);
setIsFetching(false);
}, 2000); // 2์ด๊ฐ ๋๋ ์ด
}
return (
<section className="flex flex-col justify-center items-center p-4">
<ul className="space-y-4 mb-4">
{listItems.map((item, i) => (
<li
key={i}
className="border text-center w-56 h-12 grid place-content-center"
>
List Item {item}
</li>
))}
</ul>
<div className={`${isFetching ? 'visibility' : 'invisible'}`}>
๐๏ธ Fetching more items...
</div>
</section>
);
}
๐๏ธ Array.keys()
๋ฉ์๋๋ ๋ฐฐ์ด์ ๊ฐ ์ธ๋ฑ์ค๋ฅผ ํค ๊ฐ์ผ๋ก ๊ฐ์ง๋ ์๋ก์ด Array Iterator ๊ฐ์ฒด(next
๋ฉ์๋๊ฐ ๊ตฌํ๋์ด ์๋ ์ดํฐ๋ ์ดํฐ ๊ฐ์ฒด)๋ฅผ ๋ฐํํ๋ค. ์ดํฐ๋ฌ๋ธ์ด๋ฏ๋ก for of
๋ฐ๋ณต๋ฌธ์ผ๋ก ์ํํ ์ ์๋ค.
for (const key of Array(3).keys()) console.log(key); // 0, 1, 2
Array.from(Array(3).keys(), (n) => n * 2); // [0, 2, 4]
์ค๋กํ ์ ์ฉ
๐ก ๋๋ฐ์ด์ค๋ input ์ด๋ฒคํธ์(๋ฆฌ์กํธ์์ onChange
), ์ค๋กํ์ scroll ์ด๋ฒคํธ์ ์์ฃผ ์ฌ์ฉ๋๋ค.
ํ๋ฉด์ ์คํฌ๋กคํ ๋๋ง๋ค ์คํฌ๋กค ์ด๋ฒคํธ์ ํธ๋ค๋ฌ๊ฐ ๋์์์ด ํธ์ถ๋๋ค. ๋ถํ์ํ ํธ์ถ์ด ๋ง์ผ๋ฏ๋ก ์ค๋กํ์ ์ ์ฉํด์ ์ผ์ ๊ฐ๊ฒฉ์ผ๋ก๋ง ํธ๋ค๋ฌ๊ฐ ํธ์ถ๋๋๋ก ์์ ํ๋ค. ์ค๋กํ ํจ์๋ ์ฌ๋ฌ ๊ณณ์์ ์ฌ์ฉํ ์ ์๋๋ก ์๋์ฒ๋ผ ์ ํธ ํจ์๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋ค.
// utils.js
export const throttle = (callback, ms) => {
let timeout;
// ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ํ ๋น๋ ํจ์
return function (...args) {
if (!timeout) {
timeout = setTimeout(() => {
callback(...args);
timeout = null;
}, ms);
}
};
};
useInfiniteScroll
์ปค์คํ
ํ
์์ ์ค๋กํ ํจ์๋ฅผ ๋ถ๋ฌ์จ ํ, ๊ธฐ์กด handleScroll
ํธ๋ค๋ฌ๋ฅผ throttle
ํจ์์ ์ธ์(์ฝ๋ฐฑ)๋ก ๋๊ธด๋ค. ๊ทธ๋ผ handleScroll
๋ณ์์ throttle
ํจ์๊ฐ ๋ฐํํ ๋ด๋ถ ํจ์๊ฐ ํ ๋น๋๋ค. ์ฆ, ์ด๋ฒคํธ ํธ๋ค๋ฌ์ throttle
ํจ์๊ฐ ๋ฐํํ ๋ด๋ถ ํจ์๊ฐ ํ ๋น๋๋๋ก ํ๋ค.
- ์คํฌ๋กค ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ๋ด๋ถ ํจ์ ์ธ์
(...args)
๋ก ์ด๋ฒคํธ ๊ฐ์ฒด๊ฐ ์ ๋ฌ๋๋ค. - ํ์ด๋จธ ID๊ฐ ์๋ค๋ฉด
ms
์ด ๋ค์ ์ธ์๋ก ๋ฐ์ ์ฝ๋ฐฑhandleScroll
ํจ์๋ฅผ ์คํํ๋ค. - ํ์ด๋จธ ID๊ฐ ํ ๋น๋ ์ํ์์ ๋ ๋ค๋ฅธ ์คํฌ๋กค ์ด๋ฒคํธ๋ฅผ ๋ฐ์ผ๋ฉด ์๋ฌด์ผ๋ ์ผ์ด๋์ง ์๋๋ค.
import { getScrollHeight, throttle } from '../../lib/utils';
// useInfiniteScroll.js
const useInfiniteScroll = (callback) => {
// ...์๋ต
const handleScroll = throttle(
(/* event */) => {
const scrollHeight = getScrollHeight();
const currentScroll = window.innerHeight + document.documentElement.scrollTop;
if (currentScroll === scrollHeight && !isFetching) setIsFetching(true);
},
500,
);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// ...์๋ต
};
export default useInfiniteScroll;
๐ก ์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ์ ๋์ ๋์ผํ ํธ๋ค๋ฌ์ capture ์ต์
์ ๋ช
์ํด์ผ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ ๊ฑฐํ ์ ์๋ค. ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ธ์์ throttle(handleScroll, 500)
ํํ๋ก ์์ฑํ๋ฉด ์ด๋ฒคํธ ๋ฑ๋ก / ์ ๊ฑฐ์ ๋ฑ๋กํ ํธ๋ค๋ฌ๊ฐ ๋ค๋ฅด๋ฏ๋ก(throttle
ํจ์๊ฐ ๋ฐํํ ๋ด๋ถ ํจ์์ ์ฃผ์๊ฐ์ด ๊ฐ๊ฐ ๋ค๋ฆ) ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ ๊ฑฐ๋์ง ์๋๋ค. ๋ ์์ธํ ๋ด์ฉ์ ๋งํฌ ์ฐธ๊ณ .
Memory Leak ์ค๋ฅ ํด๊ฒฐ
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup
์คํฌ๋กค์ด ํ๋ฉด ๋์ ๋๋ฌํ๋ฉด ms์ด ํ ์คํ๋๋ ์ฝ๋ฐฑ(handleScroll
)์ ์์ฝํ๋ค. ์ด๋ router๋ฅผ ์ด๋(๋ค๋ก๊ฐ๊ธฐ ๋ฑ)ํ๋ฉด ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. router ์ด๋ ํ, ์ด์ ์ปดํฌ๋ํธ์์ ์์ฝํ ์ฝ๋ฐฑ์ด ์คํ๋ผ์ ์ํ(isFetching
) ๋ณ๊ฒฝ์ ์๋ํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ๋ ์๋ฌ๋ค.
์ด ์๋ฌ๋ ์ธ๋ง์ดํธ ํ ์ํ ๋ณ๊ฒฝ์ ์ ๋ฐํ๋ isFetching
์ํ๋ฅผ false
๋ก ๋ณ๊ฒฝํ๋ ์ฝ๋๋ฅผ useEffect
์ ํด๋ฆฐ์
ํจ์์ ์ถ๊ฐํ๋ฉด ํด๊ฒฐํ ์ ์๋ค. — ์ฐธ๊ณ ๊ธ
// useInfiniteScroll.js
import { getScrollHeight, throttle } from '../../lib/utils';
const useInfiniteScroll = (callback) => {
// ...์๋ต
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
setIsFetching(false); // Can't perform a React state update... ์ค๋ฅ ๋์
window.removeEventListener('scroll', handleScroll);
};
}, []);
};
export default useInfiniteScroll;
๋ ํผ๋ฐ์ค
- How to Build an Infinite Scroll Component in React Using Hooks
- How To Create a Custom useInfiniteScroll() With React Hooks
2) Intersection Observer API๋ฅผ ์ด์ฉํ ๋ฐฉ๋ฒ โญ๏ธ
Intersection Observer๋ ํ๊ฒ ์์๊ฐ ๋ทฐํฌํธ(ํน์ ๋ค๋ฅธ ๋ถ๋ชจ ์์)์ ๋ ธ์ถ๋๋์ง ์ฌ๋ถ๋ฅผ ์๋ ค์ฃผ๋ API๋ค. ๋ฌดํ ์คํฌ๋กค, Lazy Loading, ๊ด๊ณ ๊ฐ์์ฑ(๊ด๊ณ ๋ ธ์ถ ์ฌ๋ถ๋ฅผ ํ์ ํด์ ๊ด๊ณ ์์ต ๊ณ์ฐ) ๋ฑ์ ๊ตฌํํ ๋ ํ์ฉํ ์ ์๋ค. ์ฌ์ฉ๋ฒ๋ ๊ฐ๋จํ๊ณ ์ฑ๋ฅ๋ ์ข๋ค. ์ด๋ฅผ ์ง์ ๊ตฌํํ๋ ค๋ฉด scroll ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค ์ง์ ํ ์์๊ฐ ํ๋ฉด์ ์กด์ฌํ๋์ง ๊ณ์ฐํ๋ ์ฝ๋๋ฅผ ์์ฑํด์ผ ๋๋ค.
// ์์ ์ฝ๋
const options = { threshold: 1.0 };
const callback = (entries, observer) => { // โถ
// entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
entries.forEach((entry) => {
if (entry.isIntersecting) {
// โท ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
observer.unobserve(entry.target); // โธ ๊ตฌ๋
ํด์ (ํด๋น ํ๊ฒ์ ๋์ด์ ๊ด์ฐฐํ์ง ์์)
console.log('ํ๋ฉด์์ ๋
ธ์ถ๋จ');
} else {
console.log('ํ๋ฉด์์ ์ ์ธ๋จ');
}
});
};
const observer = new IntersectionObserver(callback, options); // โด IO ๊ฐ์ฒด(์ธ์คํด์ค) ์์ฑ
observer.observe(document.querySelector('.element')); // โต ๊ด์ฐฐ ๋์ ์ถ๊ฐ
- ์ฝ๋ฐฑ๊ณผ ์ต์ ์ ์ธ์๋ก ๋ฐ์ Intersection Observer ๊ฐ์ฒด(์ธ์คํด์ค) ์์ฑ.
- ๊ด์ฐฐ ๋์์ผ๋ก ์ง์ ํ ํ๊ฒ ์์ ์ถ๊ฐ. ์ด๋ IO ๊ฐ์ฒด์ ๋ฑ๋กํ ์ฝ๋ฐฑ์ด 1ํ ์คํ๋๋ค.
- ํ๊ฒ ์์๊ฐ
threshold
์ต์ ์ ์ ์ํ ํผ์ผํธ๋งํผ ํ๋ฉด์ ๋ ธ์ถ๋๊ฑฐ๋ ์ฌ๋ผ์ง๋ฉด ์ฝ๋ฐฑ ํจ์ ํธ์ถ - ์ฝ๋ฐฑ ํจ์์ ์ธ์๋ก ์ ๋ฌ๋ฐ์
entries
๋ฐฐ์ด์ ํตํด ๋ ธ์ถ ์ฌ๋ถ ํ์ธ - ํ๊ฒ ์์๋ฅผ ๋์ด์ ๊ด์ฐฐํ ํ์๊ฐ ์๋ค๋ฉด
observer.unobserve
๋ฉ์๋๋ก ๊ตฌ๋ (๊ด์ฐฐ) ํด์
์ฝ๋ฐฑ ํบ์๋ณด๊ธฐ
๊ด์ฐฐํ ๋์์ ๋ฑ๋กํ๊ฑฐ๋, ํ๊ฒ ์์์ ๊ฐ์์ฑ(๋ ธ์ถ / ๋น๋ ธ์ถ)์ ๋ณํ๊ฐ ์๊ธฐ๋ฉด, ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ๊ณ ์ฝ๋ฐฑ์ ์ฒซ๋ฒ์งธ ์ธ์์ Intersection Observer Entry ๊ฐ์ฒด๊ฐ ๋ด๊ธด ๋ฐฐ์ด์ด ์ ๋ฌ๋๋ค. ํ๊ฒ์ด ์ฒ์ ์ง์ ๋๋ฉด(๊ด์ฐฐ ๋์ ๋ฑ๋ก) ๋ชจ๋ ์์์ ๊ฐ์์ฑ์ ์ฒดํฌํ๊ธฐ ๋๋ฌธ์ ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ํ๊ฒ ์์๋ฅผ Entry ๋ฐฐ์ด์ ๋ฃ์ด ์ฝ๋ฐฑ์ ํธ์ถํ๋ค. ์ฝ๋ฐฑ ํจ์์ ๋๋ฒ์งธ ์ธ์๋ ๊ด์ฐฐ์ ๊ฐ์ฒด๋ฅผ ๋ฐ๋๋ค.
entries
: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด (Intersection Observer Entry ์ธ์คํด์ค์ ๋ฐฐ์ด)observer
: ๊ด์ฐฐ์ ๊ฐ์ฒด (์ฝ๋ฐฑ ํจ์๊ฐ ์คํ๋๊ณ ์๋ observer ์ธ์คํด์ค ์ฐธ์กฐ)
(์ฝ๋ฐฑ์ ์ฒซ๋ฒ์งธ ์ธ์) entries ์์ฑ
โถ target
: ๊ด์ฐฐ ๋์ ์์
โท time
: ๊ต์ฐจ ์ํ ๋ณ๊ฒฝ(๋
ธ์ถ / ๋น๋
ธ์ถ)์ด ๋ฐ์ํ ์๊ฐ์ ๋ํ๋ด๋ DOMHighResTimeStamp
โธ isIntersecting
: ๋
ธ์ถ ์ฌ๋ถ — target๊ณผ root(๋ทฐํฌํธ)๊ฐ ๊ต์ฐจ ์ํ์ธ์ง ์ฌ๋ถ true|false
โน intersectionRatio
: ๋
ธ์ถ๋ ๋น์จ — target๊ณผ root๊ฐ ์ผ๋ง๋ ๊ต์ฐจ๋๊ณ ์๋์ง์ ๋ฐฑ๋ถ์จ(0~1)
โบ intersectionRect
: ๋
ธ์ถ๋ ์์ญ — target๊ณผ root๊ฐ ๊ต์ฐจ๋๊ณ ์๋ ์์ญ์ ์ ๋ณด
โป boundingClientRect
: target ์์ ์ ๋ณด top
, right
, bottom
, left
, width
, height
, x
, y
โผ rootBounds
: root ์์ ์ ๋ณด. IO ์์ฑ ์ต์
์ root๋ฅผ ์ง์ ํ์ง ์์๋ค๋ฉด(๊ธฐ๋ณธ๊ฐ) viewport ํฌ๊ธฐ
(์ฝ๋ฐฑ์ ๋๋ฒ์งธ ์ธ์) observer ๋ฉ์๋
observe(target)
: ๋์ ์์์ ๊ด์ฐฐ ์์unobserve(target)
: ๋์ ์์์ ๊ด์ฐฐ ์ค์งdisconnect()
: IntersectionObserver ์ธ์คํด์ค๊ฐ ๊ด์ฐฐํ๋ ๋ชจ๋ ์์์ ๊ด์ฐฐ ์ค์งtakeRecords()
: IntersectionObserverEntry ๊ฐ์ฒด์ ๋ฐฐ์ด ๋ฐํ (์ฌ์ฉํ ์ผ ๊ฑฐ์ ์์)
Observer ๊ฐ์ฒด ์์ฑ ์ต์ ํบ์๋ณด๊ธฐ
Intersection Observer ์์ฑ ์ ์๋ 3๊ฐ์ง ์ต์ ์ ์ค์ ํ ์ ์๋ค.
โถ root
: ๋
ธ์ถ / ๋น๋
ธ์ถ ์ฌ๋ถ๋ฅผ ์ด๋ค ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ํ ์ง ์ง์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ(null
)์ ๋ทฐํฌํธ. ๋ง์ฝ ํ๊ฒ ์์๊ฐ root๋ก ์ง์ ํ ์์์ ์์์ผ๋ก ์์ง ์๋ค๋ฉด ํ๋ฉด์ ๋
ธ์ถ๋๋๋ผ๋ ๋
ธ์ถ๋ก ์ฌ๊ธฐ์ง ์์.
โท rootMargin
: ๋ฐ๊นฅ ์ฌ๋ฐฑ(Margin)์ ์ด์ฉํด Root ๋ฒ์๋ฅผ ํ์ฅ / ์ถ์ํ๋ ์ต์
. threshold
๋ ์ง์ ํ rootMargin
๋งํผ ๋ํด์ ๊ณ์ฐ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ 0px 0px 0px 0px
(px
ํน์ %
๋จ์ ํ์ ์
๋ ฅ).
โธ threshold
: ํ๋ฉด์ ์ผ๋งํผ ๋
ธ์ถ๋ผ์ผ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ ์ต์
์ผ๋ก ๊ธฐ๋ณธ๊ฐ์ 0
(0%). ์ต๋ 1
(100%)๊น์ง ์ง์ ๊ฐ๋ฅ. Array<number>
ํํ๋ก ์ฌ๋ฌ ๋น์จ์ ์ง์ ํ ์๋ ์๋ค.
- ๊ธฐ๋ณธ๊ฐ(0)์ผ ๋ ํ๊ฒ ์์๊ฐ 1px๋ผ๋ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ์ ํธ์ถํ๋ค.
0.5
๋ก ์ง์ ํ๋ฉด ํ๋ฉด์ 50% ์ด์ ๋ณด์ผ๋๋ถํฐ ์ฝ๋ฐฑ์ ํธ์ถํ๋ค.- ๋ฐฐ์ด๋ก ์ง์ ํ๋ฉด ๊ฐ ๋น์จ๋ก ๋
ธ์ถ๋ ๋๋ง๋ค ์ฝ๋ฐฑ์ ํธ์ถํ๋ค. e.g.
{ threshold: [0, 0.2] }
React์ ์ ์ฉํ๊ธฐ
๋ ๋๋ง ๋ฆฌ์คํธ ๊ฐ์ฅ ์๋์ ๋น ์์๋ฅผ ์ถ๊ฐํ๊ณ , Intersection Observer๋ฅผ ์ด์ฉํด ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋กํ๋ค. ์คํฌ๋ฅผ์ด ๊ฐ์ฅ ํ๋จ์ ๋๋ฌํ๋ฉด(target๊ณผ root์ ๊ต์ฐจ ์์ ) IO์ ๋ฑ๋กํ ์ฝ๋ฐฑ์ด ์คํ๋๊ณ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ์์ (ํน์ page ์ํ +1 → ํด๋น page์ ๋ํ ๋ฐ์ดํฐ ์์ฒญ)์ ์ํํ๋๋ก ์์ฑํ๋ฉด ๋๋ค.
โถ Intersection Observer ๊ฐ์ฒด์ ๋ฑ๋กํ ์ฝ๋ฐฑ ํจ์ ์ ์
export default function InfiniteScrollIO() {
const [isFetching, setIsFetching] = useState(false);
const [listItems, setListItems] = useState(
Array.from(Array(30).keys(), (n) => n + 1),
);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const handleObserver = useCallback((entries, observer) => {
const target = entries[0]; // ๋ฆฌ์คํธ ๊ฐ์ฅ ์๋์ ๋น์์
if (target.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
setIsFetching(true); // '๋ก๋ฉ ์ค' ํ์
setTimeout(() => {
// (dummy) ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
setListItems((prev) => [
...prev,
...Array.from(Array(20).keys(), (n) => n + prev.length + 1),
]);
setIsFetching(false); // '๋ก๋ฉ ์ค' ํ์ ํด์
}, 1000);
}
}, []);
// ...
}
โท Intersection Observer ๊ฐ์ฒด ์์ฑ / ์ฝ๋ฐฑ&์ต์ ๋ฑ๋ก
export default function InfiniteScrollIO() {
// ...
useEffect(() => {
const options = {
root: null, // ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '30px', // ์คํฌ๋กค์ด ์ตํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ค์
threshold: 0, // ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
};
const observer = new IntersectionObserver(handleObserver, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
}, []);
// ...
}
โธ ๋น ์์(๋ฆฌ์คํธ ๊ฐ์ฅ ์๋ ์์)๋ฅผ IO ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
export default function InfiniteScrollIO() {
const loader = useRef(null);
// ...
useEffect(() => {
// ...
if (loader.current) observer.observe(loader.current); // ๋น์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
}, []);
return (
<section className="flex flex-col justify-center items-center p-4">
{/* ...์๋ต */}
<div ref={loader} className="w-full" />
</section>
);
}
โน ์ธ๋ง์ดํธ ๋ก์ง ์ถ๊ฐ
export default function InfiniteScrollIO() {
// ...
useEffect(() => {
// ...
return () => {
setIsFetching(false); // Can't perform a React state update... ์ค๋ฅ ๋์
observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
};
}, []);
// ...
}
import React, { useCallback, useEffect, useRef, useState } from 'react';
export default function InfiniteScrollIO() {
const [isFetching, setIsFetching] = useState(false);
const [listItems, setListItems] = useState(
Array.from(Array(30).keys(), (n) => n + 1),
);
const loader = useRef(null);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const handleObserver = useCallback((entries, obersver) => {
const target = entries[0]; // ๋ฆฌ์คํธ ๊ฐ์ฅ ์๋์ ๋น์์
if (target.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
setIsFetching(true); // '๋ก๋ฉ ์ค' ํ์
setTimeout(() => {
// (dummy) ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
setListItems((prev) => [
...prev,
...Array.from(Array(20).keys(), (n) => n + prev.length + 1),
]);
setIsFetching(false); // '๋ก๋ฉ ์ค' ํ์ ํด์
}, 1000);
}
}, []);
useEffect(() => {
const options = {
root: null, // ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '30px', // ์คํฌ๋กค์ด ์ตํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ต์
์ค์
threshold: 0, // ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
};
const observer = new IntersectionObserver(handleObserver, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
if (loader.current) observer.observe(loader.current); // ๋น์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
return () => {
setIsFetching(false); // Can't perform a React state update... ์ค๋ฅ ๋์
observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
};
}, []);
return (
<section className="flex flex-col justify-center items-center p-4">
<ul className="space-y-4 mb-4">
{listItems.map((item, i) => (
<li
key={i}
className="border text-center w-56 h-12 grid place-content-center"
>
List Item {item}
</li>
))}
</ul>
<div className={`${isFetching ? 'visibility' : 'invisible'}`}>
๐๏ธ Fetching more items...
</div>
<div ref={loader} className="w-full" />{' '}
{/* ํ๋ฉด ๋
ธ์ถ ์ฌ๋ถ ํ์ธ์ ์ํ ๋น์์ */}
</section>
);
}
Hook์ผ๋ก ๋ถ๋ฆฌํ๊ธฐ โญ๏ธ
Intersection Observer API๋ ๋ฌดํ ์คํฌ๋กค์ ๋ฌผ๋ก Lazy Loading, ๊ด๊ณ ๊ฐ์์ฑ ํ์ธ ๋ฑ ๋ค์ํ ๊ณณ์์ ์ฌ์ฉํ๋ฏ๋ก ์ปค์คํ Hook์ผ๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฌ์ฉํ๋ฉด ์ข๋ค. ์ปค์คํ Hook์ ์ธ์๋ ์๋ 3๊ฐ๋ฅผ ๋ฐ๋๋ก ์์ฑํ๋ค. ํจ์ ํธ์ถ์ ์ธ์ ์์์ ๊ตฌ์ ๋ฐ์ง ์๋๋ก RORO ํจํด์ผ๋ก ์์ฑํ๋ค.
callback
: ๊ต์ฐจ ์ํ์์ ์คํํ ๋ก์ง์ด ๋ด๊ธด ์ฝ๋ฐฑ ํจ์unObserver
: ๊ต์ฐจ ์ํ ํ ํด๋น ํ๊ฒ์ ๊ด์ฐฐ์ ์ค์งํ ์ง ์ฌ๋ถ.true | false
๐ก ๋ฌดํ์คํฌ๋กค์ ๊ต์ฐจ ์ํ ์ดํ์๋ ๊ฐ์ฅ ํ๋จ์ ๋น ์์๋ฅผ ๊ณ์ ๊ด์ฐฐํด์ผ ํ์ง๋ง(false
), Lazy Loading ๋ฑ์ ๊ตฌํํ ๋ ์ด๋ฏธ ํ๋ฉด์ ๋ ธ์ถ๋ผ์ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ ์ํ๋ฉด ๋์ด์ ๊ด์ฐฐํ ํ์๊ฐ ์๋ค(true
).options
: Intersection Observer ์ธ์คํด์ค ์์ฑ ์ ๋๊ธธ ์ต์
์ปค์คํ
Hook์ Ref ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋๋ก ํ๋ค. ๊ทธ๋ผ Hook์ ํธ์ถํ๋ ์ปดํฌ๋ํธ์์ Ref ๊ฐ์ฒด๋ฅผ ๋ฐ์ ๋ค ๊ด์ฐฐํ๊ณ ์ถ์ ์ปดํฌ๋ํธ์ ref
์์ฑ์ ๋ฐ๋ก ํ ๋นํ๋ฉด ๋๋ค. ์ฌ์ฉํ๊ธฐ ๋ ํธํ๋ค
// ์ฝ๋ ์ผ๋ถ ์ฐธ๊ณ : https://mrcoles.com/intersection-observer-react-hook/
// hooks/useIntersectionObserver.js
import { useCallback, useEffect, useRef } from 'react';
export default function useIntersectionObserver({
callback,
unObserve = false,
options = {
root: null, // ๋
ธ์ถ&๋น๋
ธ์ถ ์ฌ๋ถ๋ฅผ ์ด๋ค ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ํ ์ง ์ง์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '0px', // ๋ฐ๊นฅ ์ฌ๋ฐฑ(Margin)์ ์ด์ฉํด Root ๋ฒ์๋ฅผ ํ์ฅ/์ถ์ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0px
threshold: 0, // ํ๋ฉด์ ์ผ๋งํผ ๋
ธ์ถ๋ผ์ผ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
},
}) {
const ioRef = useRef(null);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const ioHandler = useCallback((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
callback(); // ๊ต์ฐจ ์์ ์(๊ด์ฐฐ ๋์์ด ๋ทฐํฌํธ์ ๋
ธ์ถ) ์ธ์๋ก ๋ฐ์ ์ฝ๋ฐฑ ํธ์ถ
if (unObserve) observer.unobserve(entry.target); // (์กฐ๊ฑด ๋ง์กฑ์) ํด๋น ํ๊ฒ์ ๊ด์ฐฐ ์ค์ง
}
});
}, []);
useEffect(() => {
if (window.IntersectionObserver) {
const observer = new IntersectionObserver(ioHandler, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
observer.observe(ioRef.current); // ์ธ์๋ก ๋๊ธด ์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
return () => observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
}
}, []);
return ioRef;
}
// ์ฝ๋ ์ผ๋ถ ์ฐธ๊ณ : https://mrcoles.com/intersection-observer-react-hook/
// hooks/useIntersectionObserver.tsx
import { useCallback, useEffect, useRef } from 'react';
interface IOProps {
callback: VoidHandler;
unObserve?: boolean;
options?: IntersectionObserverInit;
}
export default function useIntersectionObserver<T extends HTMLElement>({
callback,
unObserve = false,
options = {
root: null, // ๋
ธ์ถ&๋น๋
ธ์ถ ์ฌ๋ถ๋ฅผ ์ด๋ค ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ํ ์ง ์ง์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ null(๋ทฐํฌํธ)
rootMargin: '0px', // ๋ฐ๊นฅ ์ฌ๋ฐฑ(Margin)์ ์ด์ฉํด Root ๋ฒ์๋ฅผ ํ์ฅ/์ถ์ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0px
threshold: 0, // ํ๋ฉด์ ์ผ๋งํผ ๋
ธ์ถ๋ผ์ผ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ ์ง ๊ฒฐ์ ํ๋ ์ต์
. ๊ธฐ๋ณธ๊ฐ 0(1px ๋ผ๋ ํ๋ฉด์ ๋ณด์ด๋ฉด ์ฝ๋ฐฑ ํธ์ถ)
},
}: IOProps) {
const ioRef = useRef<T>(null);
// Intersection Observer ์ฝ๋ฐฑ ์ ์(useCallback ์ ์ฉ์ ้้กน)
// (์ธ์1) entries: ๊ด์ฐฐ ์ค์ธ ๋ชจ๋ ๋์์ด ๋ด๊ธด ๋ฐฐ์ด, (์ธ์2) observer: ๊ด์ฐฐ์ ๊ฐ์ฒด
const ioHandler: IntersectionObserverCallback = useCallback(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// ํ๋ฉด ์์ ํ๊ฒ ์์๊ฐ ๋ค์ด์๋์ง ์ฒดํฌ
callback(); // ๊ต์ฐจ ์์ ์(๊ด์ฐฐ ๋์์ด ๋ทฐํฌํธ์ ๋
ธ์ถ) ์ธ์๋ก ๋ฐ์ ์ฝ๋ฐฑ ํธ์ถ
if (unObserve) observer.unobserve(entry.target); // (์กฐ๊ฑด ๋ง์กฑ์) ํด๋น ํ๊ฒ์ ๊ด์ฐฐ ์ค์ง
}
});
},
[callback, unObserve],
);
useEffect(() => {
if (!window.IntersectionObserver || !ioRef.current) return undefined;
const observer = new IntersectionObserver(ioHandler, options); // ์ฝ๋ฐฑ&์ต์
๋ฑ๋ก ๋ฐ IO ๊ฐ์ฒด ์์ฑ
observer.observe(ioRef.current); // ์ธ์๋ก ๋๊ธด ์์๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ๋ฑ๋ก
return () => observer.disconnect(); // ์ธ๋ง์ดํธ์ ๋ชจ๋ ์์์ ๋ํ ๊ด์ฐฐ ์ค์ง
}, [ioHandler, options]);
return ioRef;
}
์ปค์คํ
Hook์ ํธ์ถํ๋ ์ปดํฌ๋ํธ์์ callback
options
์ Hook์ ์ธ์๋ก ๋๊ธด๋ค. callback
์ ๊ธฐ์กด ์์ฑํ๋ handleObserver
ํจ์์ ๋ณธ๋ฌธ๋ง ์ฎ๊ฒจ์ฃผ๋ฉด ๋๋ค. unObserve
์ธ์๋ ๋ช
์ํ์ง ์์์ผ๋ฏ๋ก ๊ธฐ๋ณธ๊ฐ false
๊ฐ ์ง์ ๋๋ค. ๊ทธ๋ผ ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋
ธ์ถ๋ ์ดํ์๋ ๊ด์ฐฐ์ ์ค์งํ์ง ์๋๋ค.
import useIntersectionObserver from '../../hooks/useIntersectionObserver';
// components/InfiniteScrollIO.js
export default function InfiniteScrollIO() {
// ...์๋ต
const loaderRef = useIntersectionObserver({
callback: () => {
setIsFetching(true);
setTimeout(() => {
/* (dummy) ๋ฐ์ดํฐ ์
๋ฐ์ดํธ ์ฝ๋ ์๋ต */
}, 1000);
},
options: { rootMargin: '30px' }, // ์คํฌ๋กค์ด ์ตํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ค์
});
useEffect(() => {
return () => setIsFetching(false); // Can't perform a React state update... ์ค๋ฅ ๋์
}, []);
return (
<section className="flex flex-col justify-center items-center p-4">
{/* ...์๋ต */}
<div ref={loaderRef} className="w-full" />{' '}
{/* ํ๋ฉด ๋
ธ์ถ ์ฌ๋ถ ํ์ธ์ ์ํ ๋น์์ */}
</section>
);
}
import React, { useEffect, useState } from 'react';
import useIntersectionObserver from '../../hooks/useIntersectionObserver';
// components/InfiniteScrollIO.js
export default function InfiniteScrollIO() {
const [isFetching, setIsFetching] = useState(false);
const [listItems, setListItems] = useState(
Array.from(Array(30).keys(), (n) => n + 1),
);
const loaderRef = useIntersectionObserver({
callback: () => {
setIsFetching(true);
setTimeout(() => {
setListItems((prev) => [
...prev,
...Array.from(Array(20).keys(), (n) => n + prev.length + 1),
]);
setIsFetching(false);
}, 1000);
},
options: { rootMargin: '30px' }, // ์คํฌ๋กค์ด ์ตํ๋จ์ ๋๋ฌํ์์ฆ์ ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ ์ํด rootMargin ์ค์
});
useEffect(() => {
return () => setIsFetching(false); // Can't perform a React state update... ์ค๋ฅ ๋์
}, []);
return (
<section className="flex flex-col justify-center items-center p-4">
<ul className="space-y-4 mb-4">
{listItems.map((item, i) => (
<li
key={i}
className="border text-center w-56 h-12 grid place-content-center"
>
List Item {item}
</li>
))}
</ul>
<div className={`${isFetching ? 'visibility' : 'invisible'}`}>
๐๏ธ Fetching more items...
</div>
<div ref={loaderRef} className="w-full" />{' '}
{/* ํ๋ฉด ๋
ธ์ถ ์ฌ๋ถ ํ์ธ์ ์ํ ๋น์์ */}
</section>
);
}
IO Custom hook ver.2 โญ๏ธ
@hyesungoh ๋ธ๋ก๊ทธ์์ ์ฐ์ฐํ ๋ฐ๊ฒฌํ ์ฝ๋. ๊ด์ฐฐํ ์์์ ๋ ํผ๋ฐ์ค๊ฐ ๋ด๊ธธ target
๋ณ์๋ฅผ useState
์ํ์ ํ ๋นํ๊ณ , ์ํ ์ค์ ํจ์ ์์ฒด๋ฅผ ๊ด์ฐฐ ๋์ ์์์ ref
์์ฑ์ ํ ๋นํ๋ ๋ฐฉ์. ref
๊ฐ ์ค์ / ํด์ ๋ ๋ ํน์ ์์
์ ์ํํ๋๋ก ํ๊ธฐ ์ํด callback ref ๋ฐฉ๋ฒ์ ์ฌ์ฉํ ๊ฒ. ์ฝ๋๋ฅผ ๋ ๊น๋ํ๊ฒ ์์ฑํ ์ ์๋ค.
import useIntersectionObserver from 'hooks/useIntersectionObserver';
const Foo = () => {
const onIntersect: IntersectionObserverCallback = ([{ isIntersecting }]) => {
console.log(`๊ฐ์ง๊ฒฐ๊ณผ : ${isIntersecting}`);
};
// ์ปค์คํ
ํ
์ฌ์ฉ
const { setTarget } = useIntersectionObserver({ onIntersect });
return <div ref={setTarget}></div>;
};
์ฝ๋ ์ฐธ๊ณ via hyesungoh.log
import { useEffect, useState } from 'react';
// useIntersectionObserver.ts
interface UseIntersectionObserverProps {
root?: null;
rootMargin?: string;
threshold?: number;
onIntersect: IntersectionObserverCallback;
}
const useIntersectionObserver = ({
root,
rootMargin = '0px',
threshold = 0,
onIntersect,
}: UseIntersectionObserverProps) => {
const [target, setTarget] = useState<HTMLElement | null>(null);
useEffect(() => {
if (!target) return undefined;
const observer: IntersectionObserver = new IntersectionObserver(
onIntersect,
{ root, rootMargin, threshold },
);
observer.observe(target);
return () => observer.unobserve(target);
}, [onIntersect, root, rootMargin, target, threshold]);
return { setTarget };
};
export default useIntersectionObserver;
๋ ํผ๋ฐ์ค
- [react] Infinite Scroll ๊ตฌํํ๊ธฐ
- Intersection Observer ๊ฐ๋จ ์ ๋ฆฌํ๊ธฐ
- Intersection Observer - ์์์ ๊ฐ์์ฑ ๊ด์ฐฐ
- ๋ด๊ฐ Intersection Observer ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ โ ngrok (0) | 2024.05.04 |
---|---|
[React] Blur ํจ๊ณผ๋ฅผ ํ์ฉํ ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ Image Lazy Loading (0) | 2024.05.04 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ (1) | 2024.05.03 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๋๋ ์ดํฐ Generator ์ด ์ ๋ฆฌ (0) | 2024.05.03 |
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ดํฐ๋ฌ๋ธ Iterable ์ด ์ ๋ฆฌ (0) | 2024.05.03 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ — ngrok
[DevTools] ์ธ๋ถ์์ ๋ก์ปฌ ์๋ฒ ์ ์ํ๊ธฐ — ngrok
2024.05.04 -
[React] Blur ํจ๊ณผ๋ฅผ ํ์ฉํ ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ Image Lazy Loading
[React] Blur ํจ๊ณผ๋ฅผ ํ์ฉํ ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ Image Lazy Loading
2024.05.04 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ํํ์ ํ๊ฐ ์์์ ๊ฒฐํฉ์ฑ
2024.05.03 -
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๋๋ ์ดํฐ Generator ์ด ์ ๋ฆฌ
[JS] ์๋ฐ์คํฌ๋ฆฝํธ ์ ๋๋ ์ดํฐ Generator ์ด ์ ๋ฆฌ
2024.05.03