10-7. prefetching 이 적용된 무한스크롤링 구현하기
자, 이제 prefetching 이 적용된 무한스크롤링을 구현 할 차례입니다. prefetching, 그렇게 복잡할것은 없습니다. 이 단어가 의미하는 그대로 '미리 불러오기' 작업이 중간에 진행되는 것 뿐입니다.
이 흐름을 정리하자면, 다음과 같습니다.
- 초기 렌더링 20개 불러오기 후 렌더링
- 렌더링이 끝나면 바로 그 다음 20개 불러오기, 하지만 아직 렌더링 하지 않음
- 대기.. 유저가 아래로 스크롤중
- 스크롤이 바닥과 가까워지면, 이미 불러왔었던것들을 렌더링해줌
- 바로, 다음 리스트를 미리 불러와서 담아둠
- 다시 스크롤이 바닥과 가까워지면 바로 렌더링
- 무한 반복...
흐름이 어느정도 이해가 가나요? 이해가 안간다면, 직접 구현해 보는것이 더 빠를지도 모릅니다.
우선 리덕스 모듈부터 작성을 해볼까요?
src/redux/modules/posts.js
import { createAction, handleActions } from 'redux-actions';
import { Map, List, fromJS } from 'immutable';
import * as PostsAPI from 'lib/api/posts';
import { pender } from 'redux-pender';
const LOAD_POST = 'posts/LOAD_POST'; // 포스트 리스트 초기 로딩
const PREFETCH_POST = 'posts/PREFETCH_POST'; // 포스트 미리 로딩
const SHOW_PREFETCHED_POST = 'posts/SHOW_PREFETCHED_POST'; // 미리 로딩된 포스트 화면에 보여주기
export const loadPost = createAction(LOAD_POST, PostsAPI.list);
export const prefetchPost = createAction(PREFETCH_POST, PostsAPI.next); // URL
export const showPrefetchedPost = createAction(SHOW_PREFETCHED_POST);
const initialState = Map({
next: '',
data: List(),
nextData: List()
});
export default handleActions({
...pender({
type: LOAD_POST,
onSuccess: (state, action) => {
const { next, data } = action.payload.data;
return state.set('next', next)
.set('data', fromJS(data));
}
}),
...pender({
type: PREFETCH_POST,
onSuccess: (state, action) => {
// nextData 에 결과를 담아둡니다.
const { next, data } = action.payload.data;
return state.set('next', next)
.set('nextData', fromJS(data));
}
}),
[SHOW_PREFETCHED_POST]: (state, action) => {
// data 의 뒷부분에 nextData 를 붙여주고,
// 기존의 nextData 는 비워줍니다.
const nextData = state.get('nextData');
return state.update('data', data => data.concat(nextData))
.set('nextData', List());
}
}, initialState);
그 다음엔, PostListContainer 에서 prefetching 로직을 작성하겠습니다.
src/components/PostListContainer.js
import React, { Component } from 'react';
import PostList from 'components/Shared/PostList';
import { connect } from 'react-redux';
import {bindActionCreators} from 'redux';
import * as postsActions from 'redux/modules/posts';
class PostListContainer extends Component {
prev = null
// 포스트 목록 초기로딩
load = async () => {
const { PostsActions } = this.props;
try {
await PostsActions.loadPost();
const { next } = this.props;
if(next) {
// 다음 불러올 포스트들이 있다면 미리 로딩을 해둔다
await PostsActions.prefetchPost(next);
}
} catch (e) {
console.log(e);
}
}
// 다음 목록 불러오기
loadNext = async () => {
const { PostsActions, next } = this.props;
PostsActions.showPrefetchedPost(); // 미리 불러왔던걸 보여준 다음에
if(next === this.prev || !next) return; // 이전에 했던 요청과 동일하면 요청하지 않는다.
this.prev = next;
// 다음 데이터 요청
try {
await PostsActions.prefetchPost(next);
} catch (e) {
console.log(e);
}
this.handleScroll(); // 한번 더 호출함으로써, 인터넷 느린 상황에 밀리는 현상 방지
}
// 스크롤 리스너
handleScroll = () => {
const { nextData } = this.props;
if(nextData.size === 0) return; // 미리 불러온 데이터 없으면 작업 중지
const { innerHeight } = window;
const { scrollHeight } = document.body;
// IE 에서는 body.scrollTop 대신에 document.documentElement.scrollTop 사용해야함
const scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
if(scrollHeight - innerHeight - scrollTop < 100) {
this.loadNext();
}
}
componentDidMount() {
// 컴포넌트가 마운트 됐을 때 호출 합니다.
this.load();
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
// 컴포넌트가 언마운트 될 때에는 스크롤 이벤트리스너를 제거합니다
window.removeEventListener('scroll', this.handleScroll);
}
render() {
const { data } = this.props;
return (
<PostList posts={data}/>
);
}
}
export default connect(
(state) => ({
next: state.posts.get('next'),
data: state.posts.get('data'),
nextData: state.posts.get('nextData')
}),
(dispatch) => ({
PostsActions: bindActionCreators(postsActions, dispatch)
})
)(PostListContainer);
여기까지 작업을 하고 나면, prefetching 이 적용된 무한스크롤링이 성공적으로 구현됩니다!