ComponentsInViewTrigger

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>

๊ด€๋ จ ๋ฌธ์„œ


์ฐธ๊ณ