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이 되는 시점에 포스트들을 불러오는 액션을 실행한다.
// 액션 실행
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 메서드로 이어줌으로서 구현된다.
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가 된다.
function* watchloadPosts() {
yield throttle(2000, LOAD_POSTS_REQUEST, loadPosts);
}
리덕스 사가에서 요청을 할 때 takeLatest 메서드가 아닌 throttle
을 사용하는 이유는 takeLatest는 동일한 요청 중 마지막 하나를 반영하는 메서드이지만 리덕스 사가와 요청을 실제로 받고 있는 리덕스는 엄연히 다른 개념이기에 throttle
메서드를 사용해 2초의 텀을 둠으로서 _REQUEST 액션을 줄이는 방법이 있다.
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
의 경우에만 액션을 실행하는 것이다.