InViewTrigger
Intersection Observer ๊ธฐ๋ฐ ํ๋ฉด ์ง์ /์ดํ ๊ฐ์ง ์ปดํฌ๋ํธ์ ๋๋ค.
์ธ์ ์ฌ์ฉํ๋์?
- โพ๏ธ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ
- ๐ผ๏ธ ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ
- โจ ์คํฌ๋กค ์ ๋๋ฉ์ด์ ํธ๋ฆฌ๊ฑฐ
- ๐ ์กฐํ์ ํธ๋ํน
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
import { InViewTrigger } from '@frontend-toolkit-js/components';
<InViewTrigger onInView={() => console.log('ํ๋ฉด์ ๋ณด์!')}>
<div>๋ด์ฉ</div>
</InViewTrigger>;์ฃผ์ ์ฌ์ฉ ์ฌ๋ก
1. ๋ฌดํ ์คํฌ๋กค
function InfiniteScroll() {
const [items, setItems] = useState([1, 2, 3]);
const [loading, setLoading] = useState(false);
const loadMore = () => {
if (loading) return;
setLoading(true);
setTimeout(() => {
setItems(prev => [...prev, prev.length + 1, prev.length + 2]);
setLoading(false);
}, 1000);
};
return (
<div>
{items.map(item => (
<div key={item}>Item #{item}</div>
))}
<InViewTrigger onInView={loadMore} threshold={0.1}>
<div>{loading ? '๋ก๋ฉ ์ค...' : '๐ ๋ ๋ณด๊ธฐ'}</div>
</InViewTrigger>
</div>
);
}2. ์ด๋ฏธ์ง ์ง์ฐ ๋ก๋ฉ
function LazyImage({ src }: { src: string }) {
const [loaded, setLoaded] = useState(false);
return (
<InViewTrigger triggerOnce onInView={() => setLoaded(true)} threshold={0.3}>
<div style={{ minHeight: 200 }}>
{loaded ? <img src={src} alt="lazy" /> : <div>Loading...</div>}
</div>
</InViewTrigger>
);
}3. ์คํฌ๋กค ์ ๋๋ฉ์ด์
function FadeInOnScroll() {
const [visible, setVisible] = useState(false);
return (
<InViewTrigger
onInView={() => setVisible(true)}
onOutView={() => setVisible(false)}
threshold={0.5}
>
<div
style={{
opacity: visible ? 1 : 0,
transform: visible ? 'translateY(0)' : 'translateY(20px)',
transition: 'all 0.5s',
}}
>
Fade In!
</div>
</InViewTrigger>
);
}4. ์กฐํ์ ํธ๋ํน (๋๋ฐ์ด์ค)
function ArticleView({ articleId }: { articleId: string }) {
const trackView = () => {
analytics.track('article_view', { articleId });
};
return (
<InViewTrigger
triggerOnce
debounce={1000}
threshold={0.8}
onInView={trackView}
>
<article>{content}</article>
</InViewTrigger>
);
}API Reference
interface InViewTriggerProps {
onInView?: (entry?: IntersectionObserverEntry) => void;
onOutView?: (entry?: IntersectionObserverEntry) => void;
threshold?: number | number[];
root?: Element | null;
rootMargin?: string;
triggerOnce?: boolean;
disabled?: boolean;
debounce?: number;
children: ReactNode;
className?: string;
style?: CSSProperties;
}Props
onInView
ํ๋ฉด์ ์ง์ ํ์ ๋ ์คํ๋๋ ์ฝ๋ฐฑ์ ๋๋ค.
<InViewTrigger onInView={(entry) => {
console.log('๋ณด์!', entry.intersectionRatio);
}}>- ํ์
:
(entry?: IntersectionObserverEntry) => void - ํ์: โ
onOutView
ํ๋ฉด์์ ๋ฒ์ด๋ฌ์ ๋ ์คํ๋๋ ์ฝ๋ฐฑ์ ๋๋ค.
<InViewTrigger
onInView={() => setVisible(true)}
onOutView={() => setVisible(false)}
>- ํ์
:
(entry?: IntersectionObserverEntry) => void - ๊ธฐ๋ณธ๊ฐ:
undefined
threshold
์ผ๋ง๋ ๋ณด์ฌ์ผ ํธ๋ฆฌ๊ฑฐํ ์ง ๊ฒฐ์ ํฉ๋๋ค. (0.0 ~ 1.0)
<InViewTrigger threshold={0.1} /> // 10% ๋ณด์ด๋ฉด ์คํ
<InViewTrigger threshold={0.5} /> // 50% ๋ณด์ด๋ฉด ์คํ
<InViewTrigger threshold={1.0} /> // 100% ๋ณด์ด๋ฉด ์คํ- ํ์
:
number | number[] - ๊ธฐ๋ณธ๊ฐ:
0.5
rootMargin
๊ฐ์ง ์์ญ์ ํ์ฅ/์ถ์ํฉ๋๋ค.
// 200px ์ ์ ๋ฏธ๋ฆฌ ๋ก๋
<InViewTrigger rootMargin="200px 0px" onInView={loadMore}>
<LoadMoreButton />
</InViewTrigger>- ํ์
:
string - ๊ธฐ๋ณธ๊ฐ:
"0px"
triggerOnce
ํ ๋ฒ๋ง ์คํํ ์ง ์ฌ๋ถ์ ๋๋ค.
// ์ด๋ฏธ์ง ๋ก๋ฉ์ ํ ๋ฒ๋ง
<InViewTrigger triggerOnce onInView={loadImage}>
<img />
</InViewTrigger>- ํ์
:
boolean - ๊ธฐ๋ณธ๊ฐ:
false
debounce
์ฝ๋ฐฑ ์คํ ์ง์ฐ ์๊ฐ(ms)์ ๋๋ค.
// ๋น ๋ฅธ ์คํฌ๋กค ์ ๋ถํ์ํ ํธ์ถ ๋ฐฉ์ง
<InViewTrigger debounce={300} onInView={track}>
<Article />
</InViewTrigger>- ํ์
:
number - ๊ธฐ๋ณธ๊ฐ:
0
disabled
Observer๋ฅผ ๋นํ์ฑํํฉ๋๋ค.
<InViewTrigger disabled={!shouldTrack} onInView={track}>
<Content />
</InViewTrigger>- ํ์
:
boolean - ๊ธฐ๋ณธ๊ฐ:
false
์ฑ๋ฅ ํ
โ ์ข์ ํจํด
// ํ ๋ฒ๋ง ์คํ (triggerOnce)
<InViewTrigger triggerOnce onInView={loadImage} />
// ๋๋ฐ์ด์ค ํ์ฉ
<InViewTrigger debounce={300} onInView={track} />
// ์ ์ ํ threshold
<InViewTrigger threshold={0.1} onInView={loadMore} /> // ๋ฏธ๋ฆฌ ๋ก๋โ ํผํด์ผ ํ ํจํด
// ๋ถํ์ํ ๋งค๋ฒ ์คํ
<InViewTrigger onInView={loadImage} /> // triggerOnce ๋น ์ง!
// ๋๋ฐ์ด์ค ์์ด ํธ๋ํน
<InViewTrigger onInView={track} /> // ๋น ๋ฅธ ์คํฌ๋กค ์ ๊ณผ๋ํ ํธ์ถ
// ๋๋ฌด ๋์ threshold
<InViewTrigger threshold={1.0} onInView={loadMore} /> // ๋ฆ๊ฒ ๋ก๋๋จ๋ด๋ถ ๋์
Intersection Observer API
์ด ์ปดํฌ๋ํธ๋ Intersection Observer API๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// ๋ด๋ถ ๊ตฌํ (๊ฐ๋ตํ)
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
onInView(entries[0]);
}
},
{ threshold, rootMargin, root }
);
observer.observe(element);์ฑ๋ฅ ํน์ฑ
๋ฒ๋ค ํฌ๊ธฐ
- ~2.5 KB (minified)
- ~1.0 KB (gzipped)
๋ฉ๋ชจ๋ฆฌ
- Observer ์ธ์คํด์ค ์๋ ์ ๋ฆฌ
- ์ธ๋ง์ดํธ ์ disconnect
๋ธ๋ผ์ฐ์ ์ง์
- Chrome 51+
- Firefox 55+
- Safari 12.1+
- Edge 15+
IE 11์ polyfill ํ์
ํธ๋ฌ๋ธ์ํ
Q: Observer๊ฐ ๋์ํ์ง ์์์
A: ์์์ ๋์ด๊ฐ ์๋์ง ํ์ธํ์ธ์
// โ ๋์ด๊ฐ 0์ด๋ฉด ๊ฐ์ง ์ ๋จ
<InViewTrigger onInView={...}>
<div style={{ height: 0 }}>Content</div>
</InViewTrigger>
// โ
๋์ด ์ง์
<InViewTrigger onInView={...}>
<div style={{ minHeight: '100px' }}>Content</div>
</InViewTrigger>Q: ์ฝ๋ฐฑ์ด ๋๋ฌด ์์ฃผ ์คํ๋ผ์
A: debounce๋ฅผ ์ถ๊ฐํ์ธ์
<InViewTrigger debounce={300} onInView={...}>
<div>Content</div>
</InViewTrigger>Q: triggerOnce๋ฅผ ๋ฆฌ์ ํ๊ณ ์ถ์ด์
A: disabled๋ฅผ ํ ๊ธํ์ธ์
const [disabled, setDisabled] = useState(false);
// ๋ฆฌ์
setDisabled(true);
setTimeout(() => setDisabled(false), 0);TypeScript
๋ชจ๋ props์ ํ์ ์ด ์ ์๋์ด ์์ต๋๋ค.
<InViewTrigger
onInView={entry => {
entry?.intersectionRatio;
// ^? number
}}
threshold={0.5}
// ^? number | number[]
>
<div>Content</div>
</InViewTrigger>