React 무한스크롤 적용

seongjin·2023년 3월 19일
0

문제상황

팀원이 작성한 코드에서는 프로필 페이지의 내 게시물을 보여줄때 게시물 10개로 제한을 두었다. 때문에 게시물이 여러개라도 최근에 작성한 10개의 글 밖에 보지 못했다. 아마 한 번에 모든 게시물을 받아온다면 렌더링 속도가 느려질 것을 고려하여 이렇게 작성한 것 같다.

고려사항

한 번에 모든 게시물을 불러오지 않고, 순차적으로 10개씩 끊어서 다시 게시물을 받아오는 무한 스크롤을 구현하기로 하였다.

무한 스크롤을 구현하는데 크게 2가지가 있었다.

먼저 scroll event를 감지하는 방법은 전체 페이지 높이 <= 지금까지 스크롤한 길이 + 현재 보이는 부분을 계산하여 끝부분에 닿았는지 탐지하는 방법인데 이 방법은 불필요한 스크롤 이벤트가 너무 많이 일어난다고 생각하여 좋지 않다고 판단하였다.

Intersection Observer API
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다. 출처: MDN

Intersection Observer API를 사용하면 화면에 타겟 요소가 들어왔는지 탐지할 수 있는데, 데이터로 불러온 맨 마지막 게시물을 타겟으로 설정해두고, 맨 마지막 게시물이 보일시 api 요청을 보내 새로운 데이터를 받아오는 방법으로 처리하기로 하였다.

해결방안


파란색: target 요소

Intersection Observer API

const limitNum = useRef(10);
const snsPostData = useSelector((state) => state.snsPostSlice.snspost);
const isEnd = useSelector((state) => state.snsPostSlice.endpoint);
const target = useRef();
const observer = new IntersectionObserver(callback, { threshold: 1 });


useEffect(() => {
    dispatch(AxiosSnsPost(snsPostURL(limitNum.current)));
    console.log('처음 데이터 불러옴');
  }, [id]);

// 화면에 타겟이 보일때마다 콜백함수 실행
const callback = (entries) => {
    if(entries[0].isIntersecting && !isEnd){ // 스크롤을 내릴때인지 && 최종데이터인지
      console.log('관측되었습니다.');
      limitNum.current += 10
      dispatch(AxiosSnsPost(snsPostURL(limitNum.current)));
    }
  };

// 데이터를 받아오고 나서 target 관측 설정. 받아오기 전에 observe를 사용시 오류.
useEffect(()=> {
    if(target.current){
      observer.observe(target.current)
    }
  },[snsPostData])

// limitNum에 따라 제한 수가 유동적으로 변하는 url
const snsPostURL = (num) => {
    const url = `https://url~~~/post/${id}/userpost/?limit=${num}`;
    return url;
  };

snsPostData.map((post, index) => {
   // post의 마지막 요소만 target 설정.
   return snsPostData.length - 1 === index ? 
     <SnsPostWrap ref={target} key={post.id}>
        <MainSnsPost data={post} />
	 </SnsPostWrap>
		: 
     <SnsPostWrap key={post.id}>
        <MainSnsPost data={post} />
     </SnsPostWrap>
});

처음 데이터를 불러오고 나면 마지막 요소에만 ref 속성을 추가하여 타겟으로 지정해주었다. 스크롤을 내리다가 이 요소가 보일시 콜백함수가 실행되는데 entries의 isIntersecting 속성은 관찰 대상이 루트 요소와 교차 상태인지 아닌지 불리언 값 반환한다. 스크롤을 내려서 화면에 나타나면 true, 더이상 api 요청을 하지 않아도 되는지(마지막 데이터인지) 확인해서 조건에 부합할때만 dispatch를 날려주었다.

react-Intersection-Observer 라이브러리

const [ref, inView] = useInView();

  useEffect(() => {
    console.log('실행');
    if (snsPostData.length !== 0 && inView && !isEnd) {
      limitNum.current += 10;
      dispatch(AxiosSnsPost(snsPostURL(limitNum.current)));
    }
  }, [inView]);

두 번째 방법은 react-Intersection-Observer 라이브러리를 사용하는 것이였다. 사용법은 좀 더 간단하다. useInView 훅을 이용하여 ref와 inView를 가져온다. 관찰 대상에 ref를 설정해주고 화면에 관찰대상이 보이면 inView는 true 보이지 않으면 false를 반환한다.
useEffect를 활용해서 inView에 따라 게시물 제한수를 증가시키고 dispatch를 날려주었다.

어려웠던 점

const initialState = {
  snspost: [],
  status: 'idle',
  endpoint : false
};

.addCase(AxiosSnsPost.fulfilled, (state, action) => {
        //   console.log('이전길이확인',state.snspost.length);
        //   console.log('현재길이확인',action.payload.length);
        if(action.payload.length === state.snspost.length) state.endpoint = true
        else state.endpoint = false
        state.status = 'success';
        state.snspost = action.payload;
      })

사실 무한스크롤을 구현하는데 어려운 점이 꽤 있었다. 가장 애먹은 부분은 크게 2가지인데
첫 번째로 서버로부터 응답받는 데이터가 마지막인지 판단하여 더이상 불러올 데이터가 없으면 api요청을 막아주는 것이였다. 이것을 처리하지 않으면 불러오는 데이터가 그대로라서 마지막 타겟 요소가 계속 화면에 보이고, 콜백함수가 작동해서 api를 계속해서 요청하게 된다. 무한루프에 빠지는 것이다. 더이상 불러올 데이터가 있는지 없는지 어떻게 판단할까 고민하다가 redux-thunk를 사용해서 불러오는 게시물 데이터의 길이가 이전 데이터의 길이와 같은지 판단해서 true, false를 반환해주었다. 새로 불러온 데이터의 길이가 이전 데이터의 길이와 같으면 최종 데이터라고 판단하여 api요청을 막아주는 방법으로 해결하였다.

두 번째로는 콜백함수 내 api요청을 날릴때 limit 수를 +10씩 증가시켜주었다. 이때 useRef로 limit 수를 설정한 이유는 변수처럼 사용할 수 있지만 불필요한 렌더링을 일으키지 않고, 렌더링이 일어나도 초기화되지 않는다는 점을 고려하여 사용하였다. 따라서 콜백함수 실행시 10개의 추가된 게시물이 요청될 것으로 기대하였으나 이전 데이터 값과 같은 게시물을 요청받았다. 원인은 요청 url의 limit 수가 변하지 않았다는 것이였다.

수정 전 문제코드

const limitNum = useRef(10);
const snsPostURL = `https:~~~~/post/${id}/userpost/?limit=${limitNum.current}`;

const callback = (entries) => {
     if(entries[0].isIntersecting && !isEnd){
       limitNum.current += 10
       dispatch(AxiosSnsPost(snsPostURL));
     }
   };

limitNum.current +10을 해줌으로써 snsPostURL의 limit 수가 변하고 api요청이 될 것을 기대했으나 snsPostURL는 선언 당시 10의 수를 가진 것으로 고정이 되는 것이다. 실제 useRef의 수가 변하더라도 snsPostURL을 선언했을 당시에는 10을 넣어서 선언했기 때문에 렌더링이 일어나지 않는 이상 값은 변하지 않는다. 따라서 api요청시 이전의 데이터 수를 똑같이 보내게 된다. 해결방법으로는 api요청을 보내기 전에 증가한 useRef값을 가진 새로운 snsPostURL을 선언하여 보내주는 것으로 해결하였다.

수정 후

const snsPostURL = (num) => {
    const url = `~~~/post/${id}/userpost/?limit=${num}`;
    return url;
  };

const callback = (entries) => {
     if(entries[0].isIntersecting && !isEnd){
       limitNum.current += 10
       dispatch(AxiosSnsPost(snsPostURL(limitNum.current)));
     }
   };
profile
나만의 오답노트

0개의 댓글