3-7. 메모 무한 스크롤링

이제 이전 데이터들을 불러올 차례입니다. 지금은 한번 불러올때 20개밖에 불러오지 않기 때문에 만약에 메모의 갯수가 20개 이상이 된다면, 이전에 작성한건 보질 못합니다.

이번 섹션에서는 사용자가 아래로 스크롤을 했을 때, 이전 메모들이 나타나도록 기능을 구현해보겠습니다.

구현을 시작하기에 앞서, 그 전에 데이터들이 충분히 있어야겠지요? Postman 을 통하여 사용해서 Send 버튼을 여러번 눌러서 데이터를 무수히 만들거나, 브라우저를 열은 후 메모 페이지에서 개발자도구 열어 콘솔에서 다음 코드를 입력하세요.

function createDummyMemo(i) {
    if(i>100) return;
    fetch('/memo', { 
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({title:'test'+i,body:'test'})
    });
    setTimeout(() => createDummyMemo(++i), 100)
    console.log(`${i}/100`);
}
createDummyMemo(0)

fetch 는 크롬 및 기타 모던 브라우저에 내장되어있는 HTTP 클라이언트입니다.

추가 로딩 API 함수 만들기.

데이터가 준비 됐다면, 추가 로딩 API 함수를 만드세요.

src/lib/web-api.js

// (...)
export const getPreviousMemo = (endCursor) => axios.get(`/memo/?_sort=id&_order=DESC&_limit=20&id_lte=${endCursor-1}`); // endCursor 기준 이전 작성된 메모를 불러온다

getPreviousmemo 라는 함수를 만들었습니다. 이 함수는 getRecentMemo 랑 조금은 비슷한데요. 신규 메모를 불러오는 API 에서는 특정 id 보다 큰 id 값을 가진 메모리스트를 불러오는 반면, 이 API 는 특정 id 보다 작은 id 값을 가진 메모리스트를 불러옵니다.

여기서는 이 id 기준을 cursor 대신에 endCursor 라고 부르겠습니다.

액션, 리듀서 작성하기

모듈에서 리덕스 관련 코드를 작성하세요. 이 부분을 구현하는건 신규 메모를 불러올때랑 매우 비슷합니다. 구조는 비슷한데 다만 concat 을 할 때에 순서가 다릅니다. 이전엔 새 데이터를 맨 앞에 붙여줬지만, 이번엔 새 데이터를 맨 뒤에 붙여줍니다.

src/modules/memo.js

// (...)
const GET_PREVIOUS_MEMO = 'memo/GET_PREVIOUS_MEMO';
// (...)
export const getPreviousMemo = createAction(GET_PREVIOUS_MEMO, WebAPI.getPreviousMemo); // endCursor
// (...)
export default handleActions({
    // (...)
    // 이전 메모 로딩
    ...pender({
        type: GET_PREVIOUS_MEMO,
        onSuccess: (state, action) => {
            // 데이터 리스트의 뒷부분에 새 데이터를 붙여준다
            const data = state.get('data');
            return state.set('data', data.concat(fromJS(action.payload.data)))
        }
    })
}, initialState);

이전에 했던거랑 매우 비슷하죠?

스크롤 이벤트 등록하기

App 컴포넌트에서, handleScroll 이라는 메소드를 만들고, addEventListener 를 통하여 윈도우가 스크롤 될 때 호출 할 이벤트리스너로 등록하세요. 우선, 스크롤이 바닥에 가까워졌는지 알기위해서 필요한 수치들을 콘솔에 기록만 해보도록 하겠습니다:

src/containers/App.js

// (...)
class App extends Component {

    async componentDidMount() {
        window.addEventListener('scroll', this.handleScroll);

        const { MemoActions } = this.props;
        // 초기 메모 로딩
        try {
            await MemoActions.getInitialMemo();
            this.getRecentMemo();
        } catch(e) {
            console.log(e);
        }
    }

    handleScroll = (e) => {
        const { clientHeight } = document.body;
        const { innerHeight } = window;

        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        console.log(clientHeight, innerHeight, scrollTop);
    }

    // (...)
}

// (...)

코드를 입력하셨다면, 페이지를 열어서 스크롤을 하면서 개발자 도구의 콘솔을 확인해보세요.

어떤 숫자들이 나요나요?

아래로 내려갈수록, scrollTop 의 값이 증가됩니다. 그리고 페이지의 바닥에 달했을때에는, 우측에 있는 두 값을 더하면 왼쪽에 있는 값이 되지요. (407 + 1870 = 2277)

그렇다면, clientHeight - innerHeight - scrollTop 의 값이 0에 가까울 수록, 페이지의 끝과 가까워졌단 소리가 되겠지요?

이번에는 수정하여 스크롤하여 화면이 페이지 바닥에서 100px 안팍 떨어져있어야만 콘솔에 프린트를 하도록 코드를 수정해보겠습니다.

src/containers/App.js - handleScroll

    handleScroll = (e) => {
        const { clientHeight } = document.body;
        const { innerHeight } = window;

        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

        if(clientHeight - innerHeight - scrollTop < 100) {
            console.log('페이지 끝에 가까워졌군요.');
        }
    }

네, 이제 페이지의 끝과 가까워져야만 콘솔에 프린트가 되는군요. 여기서 콘솔에 프린트하는 것 대신에, 이전 메모들을 로딩하면 되겠습니다!

하지만 무작정 로딩을 하면 안됩니다. 왜냐하면 이 이벤트 리스너는 스크롤을 할 때마다 호출을 하기 때문에 중복 요청이 될 가능성이 있습니다. 따라서, 중복로딩을 방지하는 간단한 규칙을 만들어줘야합니다.

일단은, 이전 메모들을 로딩하려면 endCursor 을 알아야하니, 리덕스에 연결하는 코드를 수정해줍시다.

src/containers/App.js - 하단

export default connect(
    (state) => ({
        cursor: state.memo.getIn(['data', 0, 'id']),
        endCursor: state.memo.getIn(['data', state.memo.get('data').size - 1, 'id'])
    }), 
    (dispatch) => ({
        MemoActions: bindActionCreators(memoActions, dispatch)
    })
)(App);

그 다음엔, App 컴포넌트에 endCursor 멤버변수를 null 로 설정하고, handleScroll getPreviousMemo 를 호출하세요. 만약에 endCursor 가 존재하지 않거나, 이전에 했던 요청과 동일할 경우에는 작업이 취소됩니다.

src/containers/App.js

// (...)
class App extends Component {
    endCursor = 0
    // (...)
    handleScroll = (e) => {
        const { clientHeight } = document.body;
        const { innerHeight } = window;

        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

        if(clientHeight - innerHeight - scrollTop < 100) {
            const { endCursor, MemoActions } = this.props;

            // endCursor 가 없거나, 이전에 했던 요청과 동일하다면 여기서 멈춘다.
            if(!endCursor || this.endCursor === endCursor) return;
            this.endCursor = endCursor;

            MemoActions.getPreviousMemo(endCursor);
        }
    }
    // (...)
}
// (...)

이제 페이지 하단으로 스크롤 해보세요. 이전 데이터들이 로딩되나요? 축하합니다! 메모앱에서 필요한 모든 주요기능들을 완성하셨어요.

앞으로 남은 작업은, 로딩 스피너를 보여주는 것과, 카드에 애니메이션을 적용하는 것 입니다.

results matching ""

    No results matching ""