인피니트 스크롤링

sykim·2020년 2월 15일
0
post-thumbnail

인피니트 스크롤링 구현하기

1. 리액트에 스크롤 이벤트 걸기

    const onScroll = useCallback(() => {
        if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
            // 액션 실행
        }
    }, []);
    
    useEffect(() => {
        window.addEventListener('scroll', onScroll);
        return () => {
        // scroll 이벤트를 걸었으면 반드시 useEffect return 부분에서 이벤트 제거하기!
            window.removeEventListener('scroll', onScroll);
        };
    }, [mainPosts.length]);

window.scrollY : 현재 scroll 값
document.documentElement.clientHeight : 윈도우 현재 창 높이값
document.documentElement.scrollHeight : 전체 화면 높이값
즉, 위 코드는 전체 화면 높이값에서 -300이 되는 시점에 포스트들을 불러오는 액션을 실행한다.

2. lastId 프론트와 백엔드 라우터에 세팅하기

// 액션 실행
                const lastId = mainPosts[mainPosts.length - 1].id;
                    dispatch({
                        type: LOAD_TAG_POSTS_REQUEST,
                        lastId,
                        data : tag,
                    });

인피니트스크롤링에서 중요한 개념은 lastId 값으로 다음에 불러올 포스트들을 호출하는 부분에 있다.

router.get('/:tag', async (req, res, next) => { // GET /api/tag/:tag
    try {
        let where = {};
        if (parseInt(req.query.lastId, 10)) {
            where = {
              id : {
                    [db.Sequelize.Op.lt] : parseInt(req.query.lastId, 10), 
                    // Id가 req.query.lastId보다 작은 게시글들 limit만 불러오기
                }
            };
        }
        const posts = await db.Post.findAll({
            where,
            include: [{
              model: db.Tag,
              where: { name: decodeURIComponent(req.params.tag) },
            }, {
                model: db.Image,
            }],
            order: [['created_at', 'DESC']],
            limit: parseInt(req.query.limit, 10),
        });
        return res.json(posts);
    } catch (e) {
        console.error(e);
        next(e);
    }
});

리덕스사가에서 전달한 lastId를 받아 시퀄라이즈 문법으로 해당 값보다 작은 id를 가진 포스트들을 전부 찾는다. 이때 해당 값보다 작은 id의 포스트들을 찾는 이유는 현재 포스트들의 나열이 내림차순(최신순)이기 때문이다.

function loadTagPostsAPI(tag, lastId = 0, limit = 10) {
    return axios.get(`/tag/${encodeURIComponent(tag)}?lastId=${lastId}&limit=${limit}`); // server:GET /api/tag/:tag
}
function* loadTagPosts(action) {
    try {
        const result = yield call(loadTagPostsAPI, action.data, action.lastId);
        yield put({
            type : LOAD_TAG_POSTS_SUCCESS,
            data : result.data,
        })
    } catch(e) {
        console.log(e)
        yield put({
            type : LOAD_TAG_POSTS_FAILURE,
            error : e.response && e.response.data,
        })
    }
}

리덕스 사가의 코드는 위와 같다. dispatch 함수에서 tag 값, lastId 값, limit 값을 백엔드 서버 api로 전달하고 그 결과 값(포스트들)을 성공 액션 데이터에 넣어줌으로서 해당하는 범위의 포스트들이 프론트단에 불러와진다.

        case LOAD_TAG_POSTS_REQUEST : {
            return {
                ...state,
                mainPosts : action.lastId === 0 ? [] : state.mainPosts,
            }
        }
        case LOAD_TAG_POSTS_SUCCESS : {
            return {
                ...state,
                mainPosts : action.lastId === 0 ? action.data : state.mainPosts.concat(action.data),
            }
        }

해당 리듀서의 코드는 위와 같다. 포스트 요청을 할 때도 [] 빈배열이 아닌, lastId 값에 따라 분기가 되는데 lastId 값이 0 경우는 처음 포스트들을 불러올 때 사용된다.
즉, lastId 값이 0이 아닌 경우는 이전 포스트들이 이미 있고 (스크롤한 만큼의 포스트들) 그 다음 스크롤 시 성공 리듀서에 기재한 것과 같이 이전 포스트들과 새로운 데이터를 concat 메서드로 이어줌으로서 구현된다.

3. 모든 포스트 로드시 스크롤 스탑시키기

    const {mainPosts, hasMoreTagPost} = useSelector(state => state.post);
			...
            if (hasMoreTagPost) {
                const lastId = mainPosts[mainPosts.length - 1].id;
                dispatch({
                  type: LOAD_TAG_POSTS_REQUEST,
                  lastId,
                  data : tag,
                });   
            }
            ...

리듀서에 hasMoreTagPost라는 state 값을 false로 놓고

export const initialState = {
	...
	hasMoreTagPost : false,
};
...

case LOAD_TAG_POSTS_REQUEST : {
            return {
                ...state,
                mainPosts : action.lastId === 0 ? [] : state.mainPosts,
                hasMoreTagPost : action.lastId ? state.hasMoreTagPost : true,
            }
        }
        case LOAD_TAG_POSTS_SUCCESS : {
            return {
                ...state,
                mainPosts : action.lastId === 0 ? action.data : state.mainPosts.concat(action.data),
                hasMoreTagPost : action.data.length === 10,
                thumbImagePath : [],
            }
        }

hasMoreTagPost state가 true인 경우 스크롤 요청을 하는 방식인데
그 true의 조건은 불러올 데이터의 length가 10개 (limit 값과 맞춰줘야함)인 경우 state가 true가 된다.

4-1. 스크롤 특성상 리덕스 요청이 무수히 많아지는 경우 방법 1. 쓰로틀링

function* watchloadPosts() {
    yield throttle(2000, LOAD_POSTS_REQUEST, loadPosts);
}

리덕스 사가에서 요청을 할 때 takeLatest 메서드가 아닌 throttle을 사용하는 이유는 takeLatest는 동일한 요청 중 마지막 하나를 반영하는 메서드이지만 리덕스 사가와 요청을 실제로 받고 있는 리덕스는 엄연히 다른 개념이기에 throttle 메서드를 사용해 2초의 텀을 둠으로서 _REQUEST 액션을 줄이는 방법이 있다.

4-2. 스크롤로 인한 리덕스 요청을 줄이는 방법 2. state 상태값을 부여해 조건문 안에서만 리덕스 요청하기

    const countRef = useRef([]);

    const onScroll = useCallback(() => {
        if (window.scrollY + document.documentElement.clientHeight >...) {
            if (hasMoreTagPost) {
                const lastId = mainPosts[mainPosts.length - 1].id;
                // 이 부분
                if (!countRef.current.includes(lastId)) {
                    dispatch({
                        type: LOAD_TAG_POSTS_REQUEST,
                        lastId,
                        data : tag,
                    });
                    countRef.current.push(lastId);
                } 
            } else {
                countRef.current = [];
            }
        }
    }, [hasMoreTagPost, mainPosts.length, countRef ]);

useRef의 값은 바뀌어도 화면이 리렌더링이 되지 않는다는 것이 포인트이다.
즉, 스크롤을 할 때마다 변하는 lastId 값들을 배열에 넣어주고 기억해서 진짜 호출해야할 lastId의 경우에만 액션을 실행하는 것이다.

profile
블로그 이전했습니다

0개의 댓글