본문 바로가기

Dot Programming/React ∙ Next.js

[React] infinite scrolling 구현하기 (redux-saga) | react-virtualized

React, redux로 게시글 무한 스크롤링을 구현

1. 서버와 연결하지 않고 프론트에서 더미 데이터로 테스트를 하는 경우 아래와 같이 redux에 더미데이터를 생성한다. (faker 라이브러리 사용)

// 서버에서 20개씩 데이터를 불러오는 경우 시뮬레이션 
export const generateDummyPost = (number) => Array(number).fill().map(() => ({
    id: shortId.generate(),
    User: {
        id: shortId.generate(),
        nickname: faker.name.findName()
    },
    content: faker.lorem.paragraph(),
    Images: [{
        src: faker.image.image(),
    }],
    Comments: [{
        User: {
            id: shortId.generate(),
            nickname: faker.name.findName()
        },
        content: faker.lorem.sentence(),
    }],
}));

 

2. saga를 통해 action을 받아 작업을 처리해준다.

function* loadPost(action){
    try{
        // const result = yield call(loadPostAPI);
        const id = shortid.generate();
        yield delay(1000);
        yield put({
            type: LOAD_POST_SUCCESS,
            data: generateDummyPost(10),
        });
    }
    catch(err){
        yield put({
            type: LOAD_POST_FAILURE,
            data: err.response.data,
        })
    }
}

function* watchLoadPost(){
    yield takeLatest(LOAD_POST_REQUEST, loadPost);
}

export default function* postSaga(){
    yield all([
        fork(watchAddPost),
        fork(watchLoadPost),
        fork(watchRemovePost),
        fork(watchAddComment),
    ]);
}

 

 

3. scrollY, clientHeight, scrollHeight 값을 통해 스크롤이 끝까지 갔을 때 자연스럽게 로딩해주는 것을 구현

(에러 발생)

const Home = () => {
    const { me } = useSelector(state => state.user);
    const { mainPosts, hasMorePost } = useSelector(state => state.post);

    const dispatch = useDispatch();
    useEffect(() =>{
        dispatch({
            type: LOAD_POST_REQUEST,
        })
    },[]);

    useEffect(() => {
        function onScroll(){
            // console.log(window.scrollY, document.documentElement.clientHeight, document.documentElement.scrollHeight); 
            //  마지막 scrollY + clientHeight = scrollHeight
            
            //  스크롤 다 내리면 새로운 게시글 로딩
            if (window.scrollY + document.documentElement.clientHeight  > document.documentElement.scrollHeight -300){
                if(hasMorePost){
                    dispatch({
                        type: LOAD_POST_REQUEST,
                    })
                }
            }
        }
        window.addEventListener('scroll', onScroll);

        return () => {
            window.removeEventListener('scroll', onScroll);
        };
    }, [hasMorePost]);

 

 

시행 착오 1)

스크롤이 마지막 도달하기 300px이전에 미리 로딩해서 게시글을 불러오기 위해 조건문을 equal에서 등호로 바꿔보았다.

 

window.scrollY + document.documentElement.clientHeight  > document.documentElement.scrollHeight -300

 

하지만 scroll이 미세하게 측정되기 때문에 요청이 1초에 수십 개가 발생한다.

 

시행 착오 2)

그래서 redux-saga effect 중 throttle을 통해 5초에 한번의 요청만 받게 설정해보았다.

function* watchLoadPost(){
    yield throttle(5000, LOAD_POST_REQUEST, loadPost);
}

그러나 5초에 한번만 할 뿐 기존 요청을 취소 시켜주지 않기 때문에 5초 뒤에 또 요청이 성공하게 된다.

 

시행 착오3)

요청을 1번만 보낼 수 있는 방법이 없을까?

reducer에서 immer를 사용하여 설정해놓은 상태를 사용하면 된다.

case LOAD_POST_REQUEST:
    draft.loadPostLoading = true;
    draft.loadPostDone = false;
    draft.loadPostError = null;
break;
 case LOAD_POST_SUCCESS:
    draft.loadPostLoading = false;
    draft.loadPostDone = true;
    draft.mainPosts = action.data.concat(draft.mainPosts);
    draft.hasMorePost = draft.mainPosts.length < 50; //  게시글이 50개 이상이면 false (50개씩만 불러오기)
break;
case LOAD_POST_FAILURE:
    draft.loadPostLoading = false;
    draft.loadPostError = action.error;
break;

 

 

컴포넌트 useEffect 두 번째 인자에 loadPostLoading을 추가하고 if문에  hasMorePost && !loadPostLoading라고 수정하여  loading이 false일때만 요청이 가게끔 설정했다.

 const { me } = useSelector(state => state.user);
    const { mainPosts, hasMorePost, loadPostLoading } = useSelector(state => state.post);

    const dispatch = useDispatch();
    useEffect(() =>{
        dispatch({
            type: LOAD_POST_REQUEST,
        })
    },[]);

 useEffect(() => {
        function onScroll(){
            // console.log(window.scrollY, document.documentElement.clientHeight, document.documentElement.scrollHeight); 
            //  마지막 scrollY + clientHeight = scrollHeight
            
            //  스크롤 다 내리면 새로운 게시글 로딩
            if (window.scrollY + document.documentElement.clientHeight  > document.documentElement.scrollHeight -300){
               if(hasMorePost && !loadPostLoading){
                    dispatch({
                        type: LOAD_POST_REQUEST,
                    })
                }
            }
        }
        window.addEventListener('scroll', onScroll);

        return () => {
            window.removeEventListener('scroll', onScroll);
        };
    } , [hasMorePost, loadPostLoading]);

일단은 요청이 몇 번 일어나든 Success 1번만 되면 된다.

+ 추가로

그리고 throttle은 다시 takeLatest로 수정해주었다

(요청이 2번 가는 경우도 있는데 이게 최종본이 아니라 나중에 lastId방식을 적용하면서 한번 더 디벨롭할 예정)

 

 

React-virtualized

infite scrolling을 하면 유저가 계속해서 게시글을 수 천개, 수 만개 이렇게 불러오면 브라우저 메모리가 터져버릴 수도 있다.

(컴퓨터는 메모리가 커서 괜찮은데 모바일의 경우 메모리가 작아서 터질 수 있음)

 

이럴 때 사용하는 것이 react-virtualized이다. (인스타에서 사용)

원리
수천개의 로딩된 게시글 중 보여지는 한 번에 보여지는 게시글 3~4개만 갖고있고 나머지는 메모리에 갖고있는 방식이다.

 

인스타 부분 캡처

인스타를 보면 아무리 스크롤을 해도 최대 8개만 불러와지는 것을 볼 수 있다.

 


참고

인프런 강의 - 노드버드