[movie-app] Intersection Observer api 무한스크롤 구현

쏘소·2022년 5월 13일
0

프로젝트

목록 보기
9/18

이틀 전에 intersection observer를 이용해서 무한 스크롤을 구현하고자 하였지만 실패로 끝나 다른 기능을 먼저 구현하면서 머리를 식히고 오늘 다시 시도해보려고 하였다. 현재 나는 무한 스크롤 기능을 미루고,, 다른 기능들은 거의 다 구현하였다.(디자인과 리팩토링은...많이 해야할듯 하다...😂) 아무래도 전에 프로젝트를 진행하면서 react-beautiful-dnd를 이용해 드래그 앤 드롭을 했던 것이 많은 도움이 되었다.

현재 문제는 무한 스크롤이 되긴하는데 아.. 무한 스크롤만 되고 같은 페이지만 계속 불러온다.

const target = useRef(null);

useEffect(() => {
    let observer
    if (target.current) {
      observer = new IntersectionObserver(async ([entry]) => {
        if (!entry.isIntersecting) return
          setisLoading(true)
          observer.unobserve(entry.target)
      	  setPage(prevState => prevState + 1) 
          await callMovieApi(page)
          setisLoading(false)
          observer.observe(target.current)
      }, {
        threshold: 1,
      })
      observer.observe(target.current)
    }
    return () => observer && observer.disconnect()
  }, [target.current])

다음과 같이 intersection observer가 target을 감지하여 entry.intersecting이 true가 될 때 기존 page 보다 1을 더한 다음 페이지를 axios api 함수에서 불러와야 하는데 이 setState이 비동기이다 보니까 적용이 안 된 채로 계속 같은 페이지를 불러오고 있는 것이다. 그리고 또 다른 문제는 useEffect의 dependency 배열 안에 들어가는 인수에 target.current 를 넣었는데 vscode가 경고해댄다. Vscode에서 말하는 인수를 넣으면 entry.intersecting이 true가 되는 순간 api를 무한,,,불러와 스크롤이 점(.)만해진다. 아무래도 코드를 잘못 짰다는 것이겠지... 이 문제를 해결하기 위해 예전에 배웠지만 100퍼센트 깊에 이해는 하지 못했던 것으로 보이는 비동기 개념을 오늘 하루종일 파보았다.

문제 확인

movieApi를 너무 빨리 불러오는 문제를 확인하기 위해서 먼저 가짜 fetch함수를 넣어주었다.

const testFetch = () =>
    new Promise((res) => setTimeout(res, 4000))
...
 useEffect(() => {
		...
        setisLoading(true)
        await testFetch()
        observer.unobserve(entry.target)
        setPage(prevState => prevState + 1)
        console.log(page)
        // await callMovieApi(page)
        console.log('api done')
        await testFetch()
        setisLoading(false)
        ...
  }, [page, target])

페이지를 어떻게 불러오고 있는지 확인하고 싶어서 다음과 같이 콘솔창에 api 함수 호출 전후로 불러오는 page와 'api done'을 띄워주도록 했다.
그런데 결과 너무나 충격...

난 분명히 api 함수 전후로(주석처리 해줌) 콘솔에 띄워주도록 했는데 일단 한 번에 4~5개의 api done을 띄워주고 있었고, page는 분명 1씩 증가하도록 하였는데 그냥 맘대로 보여주고 있었다.

해결방법

useRef -> useState

 const [target, setTarget] = useState<HTMLElement | null | undefined>(null)

알고보니 감지할 대상 객체는 계속해서 바뀌는데, useRef는 참조값의 변경사항을 알리지 않아 useEffect가 트리거(발생)되지 않는다. 그래서 다음과 같이 useState의 setState를 이용해서 target을 지정해주었다.

useState 비동기 처리

setState({ number: number + 1 }); //2
setState({ nextNumber: number + 1 });//2
setState({ nextNextNumber: nextNumber + 1 });//2

useState 는 다음과 같이 비동기적으로 작동한다. useState를 동기적으로 만들어주기 위해서는 콜백함수를 사용하는 방법과 updater인 prevState 를 사용하는 방법이 있다.

하지만 알다시피 콜백함수로 처리해주게 되면

this.setState({ number: this.state.number + 1 }, () => {
  this.setState({ nextNumber: this.state.number + 1 }, () => {
    this.setState({ nextNextNumber: this.state.nextNumber + 1 }, () => {
      console.log(
        this.state.number,
        this.state.nextNumber,
        this.state.nextNextNumber
      );
    });
  });
});

다음과 같이 콜백지옥..에 빠지게 된다.

setState({ number: number + 1 }); //2
setState(prevState => ({ nextNumber: prevState.number + 1 })); //3
setState(prevState => ({ nextNextNumber: prevState.nextNumber + 1 })); //4

이렇게 prevState를 사용해주면 쉽게 비동기처리가 가능하다.

밑에서 useState 비동기처리에 관해 포스팅 했듯이 useState는 제대로 비동기처리가 된 채 작동이 되고 있었다!

Loading 추가

다시 위의 콘솔창이 보여준 기괴한 결과에 집중해보자. 먼저 api done을 한 번에 여러 개씩 띄워주는 건 IntersectionObserver가 target을 계속 감지하게 된다는 것 같았다. 이에 loading을 추가하고 스타일을 입혔다.

<div className="lastMovie" ref={setTarget}>{isLoading && <div className={styles.loading}>loading</div>}</div>

로딩될 때 loading으로 가려 target과 루트 영역의 교차상태를 false로 만들어 주었다. 이렇게 하니 api done이 4초에 하나씩 콘솔에 찍히게 되었다.

IntersectionObserver 중복 감지 처리

하지만 다시 api함수 주석을 취소하고 시행해보니 한 번에 여러 번의 api 함수를 호출하며 페이지를 계속해서 띄웠다.

  useEffect(() => {
    let observer: IntersectionObserver
    if (target) {
      observer = new IntersectionObserver(async ([entry]) => {
        if (!entry.isIntersecting) return
        setisLoading(true)
        await testFetch()
        observer.unobserve(entry.target)
        console.log(page)
        setPage(prevState => prevState + 1)
        MovieCallApi(page)
        console.log('api done')
        setisLoading(false)
        observer.observe(target)
      }, {
        threshold: 1,
      })
      observer.observe(target)
    }
    return () => observer && observer.disconnect()
  }, [MovieCallApi, page, target])

useEffect의 dependency를 살펴 보니(vscode가 안내해주는 dependency 값으로 update한 결과) page가 바뀔 때마다 IntersectionObserver가 target을 재감지 하도록 되어있었다. 이는 api함수를 다시 중복해서 불러오게 한다.

useEffect(() => {
    let observer: IntersectionObserver
    if (target) {
      observer = new IntersectionObserver(async ([entry]) => {
        if (!entry.isIntersecting) return
        setisLoading(true)
        await testFetch()
        observer.unobserve(entry.target)
        setPage(prevState => prevState + 1) 
        setisLoading(false)
        observer.observe(target)
      }, {
        threshold: 1,
      })
      observer.observe(target)

이를 해결해주기 위해 IntersectionObserver 내에서 api함수를 호출해주지 않고 api함수를 useEffect 안에 넣어 page값이 변경될 때마다 호출되도록 하였다. 이렇게 하였더니 무한 스크롤 기능을 제대로 볼 수 있었다.

useState 비동기처리 참고자료

profile
개발하면서 행복하기

0개의 댓글