[TIL] Intersection Observer 와 React에 대해

Dico·2021년 7월 28일
3

[TIL]

목록 보기
10/13

무한 스크롤을 구현하려다 날밤새게 만든 Intersection Observer,
useEffect에서 무한히 생겨나던 fetch요청...
기억이 휘발되기 전 삽질의 흔적을 남겨보자 🙉


Intersection Observer로 무한 스크롤 구현하기

Intersection Observer를 사용해 구현된 CardListContainer:

const CardListContainer = () => {
  const [cardListInfo, setCardListInfo] = useState([]);
  const [page, setPage] = useState(1);
  const [io, setIo] = useState(null);

  const fetchAPIData = async () => {
    const data = await API.get.comments(page);
    setCardListInfo([...cardListInfo, ...data]);
  }

  const registerObservingEl = (el) => {
    io.observe(el);
  }

  useEffect(() => {
    const targetIO = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          setPage(page + 1);
          if (io !== null) io.disconnect();
        }
      })
    })
    setIo(targetIO);
    fetchAPIData();
  }, [page])

  return (
    !cardListInfo.length
    ? <></>
    : <CardListPresenter cardListInfo={cardListInfo} registerObservingEl={registerObservingEl} />
  )
}

export default CardListContainer;

구현전략

  1. intersectionObserver의 인스턴스를 생성하고, 콜백함수로 관찰대상 요소가 들어올 경우 어떤 후속처리를 할 지 정의해준다.

    • useEffect에서 선언.
  2. 관찰대상 DOM요소를 등록하는 함수를 정의한다.

  const registerObservingEl = (el) => {
    io.observe(el);
  }
  1. 위 등록 함수를 관찰대상 DOM이 생성되는 곳까지 props로 내려준다.
    • propsCardPresenter컴포넌트까지 registerObservingEl함수를 내려줌.
  useEffect(() => {
    const targetIO = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          //다음 페이지의 card데이터를 fetch 해온다.
          setPage(page + 1);
          if (io !== null) io.disconnect();
        }
      })
    })
    setIo(targetIO);
    fetchAPIData();
  }, [page])

Intersection Observer 인스턴스 만들기

new로 Intersection Observer의 인스턴스 객체를 생성하면, 이 객체는 관찰 대상 요소와 viewport와의 교차영역에 대한 변화를 비동기적으로 감지해준다.
(기본적으로는 viewport가 되지만 option으로 root를 설정해주면 이 또한 바꿀 수 있다)

이 때 전달해 줄 수 있는 인자는 2가지:

  • callback 함수
  • (선택) option 객체
const targetIO = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        //후속처리 함수가 들어가는 자리
          if (io !== null) io.disconnect();
        }
      }, {/* option객체 */})

위 코드의 callback함수가 받는 entries는 어떻게 들어오는 걸까?
그래서 사용되는 것이 .observe()메서드인 것!

.observe()

생성된 Intersection Observer 인스턴스 객체는 편의상 io라고 부르자.
io.observe(돔 요소);를 하게되면 해당 돔 요소는 io객체가 관찰하는 대상으로 등록이 된다.

  const registerObservingEl = (el) => {
    io.observe(el);
  }

이번 과제에서는 'viewport에 카드리스트의 가장 마지막 카드가 들어오면 후다닥 다음 페이지의 API를 fetch해서 보여주자!'라는 것이 목표였던만큼,observe메서드를 props로 전달해주기 위해 별도의 함수표현식으로 만들었다.

.disconnect()

관찰을 멈추게 하는 메서드.
등록되었던 관찰대상과 io와의 커넥션을 끊는다.

💡 이 부분에서 겪었던 시행착오

처음에는 useState()를 이용해 상태로 io객체를 관리하면 될 것이라 생각했다❗️

const [io, setIO] = useState(null);

당연히setIO가 호출되고 리렌더링 되는 순간 앞전에 생겨난 io객체가 garbage collecting되어 사라질꺼라 생각했지만 network탭을 통해 요청내역을 보니 그렇지가 않았다 😱


스크롤이 내려갈 때 뿐만 아니라 올라갈 때도 get요청이 가고 있다는 건 이전 렌더 때 생겨난 io가 여전히 관찰을 이어가고 있다는 것! 없어지지 않았다는 사실 !!

그래서 #destroy intersection observer(스택오버플로우 참고) 라는 키워드로 검색을 해보니 io객체를 삭제하는 방법 대신 disconnect라는 메서드로 연결을 끊는 방법을 추천하고 있었다. 실제로 mdn 스펙상 destroy메서드는 구현되지 않은 것을 확인할 수 있었다.

그래서 io객체의 처리를 useState()의 상태로 다룰 때, 만들어진io객체가 있다면(a.k.a 이전에 만들어진 io객체) io.disconnect() 로 연결을 끊어주는 방식으로 변경했다.

const newIO = new IntersectionObserver(...);

...

if (io !== null) io.disconnect();
setIO(newIO);

이제는 스크롤을 올려도 get요청이 번복되지 않는다!!

😳 이번에야말로 최적화에 한 걸음 다가간 것이 아닐까 (흐뮷)
#Intersection Observer는 삭제말고 연결끊기

끝!

profile
프린이의 코묻은 코드가 쌓이는 공간

1개의 댓글

comment-user-thumbnail
2022년 6월 17일

덕분에 버그 해결했습니다. 소중한 기록 감사합니다!

답글 달기