본문 바로가기

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개만 불러와지는 것을 볼 수 있다.

     


    참고

    인프런 강의 - 노드버드