3-5. 메모 불러오기
이제 우리가 작성 한 메모들을 화면에서 보여줄 차례 입니다. 우리가 서버에서 메모를 불러올 때엔, 총 3가지 종류로 불러올건데요, 그 종류들은 다음과 같습니다.
- 초기 로딩: 페이지에 처음 들어왔을때 하는 메모 로딩입니다. 가장 처음 보여줄 20개를 보여줍니다.
- 신규 로딩: 초기로딩이 마친 후, 그 이후에 새로 작성된 메모들을 불러옵니다. 이 로딩은 5초마다 반복합니다. 또한, 우리 메모를 작성하고 나서도 한번 호출됩니다.
- 추가 로딩: 스크롤을 내렸을 때, 이전 메모들을 더 불러올때 사용됩니다.
이번 섹션에서는, 이 세가지 로딩 API 중 1번과 2번만 구현하고, 3번의 경우엔 3-7 섹션 무한스크롤링 부분에서 구현하도록 하겠습니다.
초기 로딩 API 함수 만들기
초기 로딩을 할 때에는, 지금까지 작성된 메모들을 역순으로, 최대 20개까지만 불러오도록 설정하겠습니다.
json-server 에서 제공하는 기능들을 사용하여 이 조건으로 요청 한다면 주소는 다음과 같습니다:
/memo/?_sort=id&_order=DESC&_limit=20
id 를 역순으로 정렬한 후 _limit
을 통하여 20개만 불러옵니다. 그 이후의 데이터들은 나중에 추가로딩을 통하여 불러올것입니다.
자, 그러면 이 API 함수를 만들어보겠습니다.
web-api.js
에 다음 함수를 추가하세요.
src/lib/web-api.js
// (...)
export const getInitialMemo = () => axios.get('/memo/?_sort=id&_order=DESC&_limit=20'); // 역순으로 최근 작성된 포스트 20개를 불러온다.
리덕스 모듈에서 API 호출하는 액션 만들기
이제 이 API 를 호출하는 액션 GET_INITIAL_MEMO
를 만들겠습니다.
src/modules/memo.js
import { createAction, handleActions } from 'redux-actions';
import { Map, List, fromJS } from 'immutable';
import { pender } from 'redux-pender';
import * as WebAPI from 'lib/web-api';
// 액션 타입
const CREATE_MEMO = 'memo/CREATE_MEMO';
const GET_INITIAL_MEMO = 'memo/GET_INITIAL_MEMO';
// 액션 생성자
export const createMemo = createAction(CREATE_MEMO, WebAPI.createMemo) // { title, body }
export const getInitialMemo = createAction(GET_INITIAL_MEMO, WebAPI.getInitialMemo);
const initialState = Map({
data: List()
});
export default handleActions({
// 초기 메모 로딩
...pender({
type: GET_INITIAL_MEMO,
onSuccess: (state, action) => state.set('data', fromJS(action.payload.data))
})
}, initialState);
pender 를 통하여 API 요청이 성공 하였을 때, 결과 내용을 data
안에 넣도록 코드를 작성하였습니다. 이때 반환 되는 값이 배열 형태이니, Immutable 데이터로 변환 하기 위하여 fromJS
가 사용되었습니다.
App 컴포넌트에서 초기 메모 불러오기
방금 만든 액션을 App 컴포넌트에서 실행해보도록 하겠습니다.
액션을 바인딩 한 다음에, componentDidMount
에서 액션을 실행하세요.
src/containers/App.js
import React, { Component } from 'react';
import Header from 'components/Header';
import Layout from 'components/Layout';
import WriteMemo from './WriteMemo';
import * as memoActions from 'modules/memo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
class App extends Component {
componentDidMount() {
const { MemoActions } = this.props;
// 초기 메모 로딩
MemoActions.getInitialMemo();
}
render() {
return (
<Layout>
<Header/>
<Layout.Main>
<WriteMemo/>
</Layout.Main>
</Layout>
);
}
}
export default connect(
(state) => ({}), // 현재는 비어있는 객체를 반환합니다
(dispatch) => ({
MemoActions: bindActionCreators(memoActions, dispatch)
})
)(App);
이제 브라우저를 열어서 Redux 개발자 도구를 확인해보세요. 요청이 잘 처리 되었나요?
이제 불러온 데이터를 렌더링하겠습니다.
메모 목록 보여주기
메모를 보여주는것에 관련된 컴포넌트는 src/components/MemoList
디렉토리에 만들도록 하겠습니다.
앞으로 우리가 만들 컴포넌트는 다음과 같습니다:
- Memo: 메모 한개의 데이터를 전달받아 렌더링합니다.
- MemoList: 메모 여러개가 들어있는 배열을 전달받아 여러개의 Memo 컴포넌트를 렌더링 합니다.
Memo 컴포넌트 만들기
Memo 컴포넌트는, 화면 크기에 따라 다른 사이즈의 정사각형 크기로 화면에 나타납니다.
화면에 따라 정사각형으로 보여주기 위한 과정에서, 내용을 감싸기 위한 컴포넌트가 3개 만들어집니다.
이 컴포넌트는 추후 클릭되었을떄 전체 메모를 보여주기위하여 onOpen
을 props 로 전달받고, memo
를 Immutable Map 형태로 전달받습니다.
src/MemoList/Memo.js
import React, {Component} from 'react';
import styled from 'styled-components';
import oc from 'open-color';
import PropTypes from 'prop-types';
import { media } from 'lib/style-utils';
import ImmutablePropTypes from 'react-immutable-proptypes';
// 화면 크기에 따라 일정 비율로 가로 사이즈를 설정합니다
const Sizer = styled.div`
display: inline-block;
width: 25%;
padding: 0.5rem;
${media.desktop`
width: 33.3333%;
`}
${
media.mobile`
width: 50%;
padding: 0.25rem;
`
}
`;
// 정사각형을 만들어줍니다. (padding-top 은 값을 % 로 설정하였을 때 부모 엘리먼트의 width 의 비율로 적용됩니다.)
const Square = styled.div`
padding-top: 100%;
position: relative;
background: white;
cursor: pointer;
transition: all 0.3s cubic-bezier(.25,.8,.25,1);
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
&:hover {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
`;
// 실제 내용이 들어가는 부분입니다.
const Contents = styled.div`
position: absolute;
top: 1rem;
left: 1rem;
bottom: 1rem;
right: 1rem;
/* 텍스트가 길어지면 새 줄 생성; 박스 밖의 것은 숨김 */
white-space: pre-wrap;
overflow: hidden;
`;
const Title = styled.div`
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 1rem;
`;
const Body = styled.div`
font-size: 1.1rem;
font-weight: 300;
color: ${oc.gray[7]};
`;
class Memo extends Component {
static propTypes = {
memo: ImmutablePropTypes.mapContains({
id: PropTypes.number,
title: PropTypes.string,
body: PropTypes.body
}),
onOpen: PropTypes.func
}
render() {
const { title, body } = this.props.memo.toJS();
return (
<Sizer>
<Square>
<Contents>
{ title && <Title>{title}</Title>}
<Body>{body}</Body>
</Contents>
</Square>
</Sizer>
)
}
}
export default Memo;
MemoList 만들기
이제 여러개의 Memo 를 렌더링해주는 MemoList 컴포넌트를 만들어보겠습니다.
이 컴포넌트는 Immutable List 형태의 memos
와 메모를 열어주는 onOpen
메소드를 props 로 전달받습니다.
src/components/MemoList/MemoList.js
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { media } from 'lib/style-utils';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Memo from './Memo';
const Wrapper = styled.div`
display: block;
margin-top: 0.5rem;
font-size: 0px; /* inline-block 위아래 사이에 생기는 여백을 제거합니다 */
${media.mobile`
margin-top: 0.25rem;
`}
`;
const MemoList = ({memos, onOpen}) => {
const memoList = memos.map(
memo => (
<Memo
key={memo.get('id')}
memo={memo}
onOpen={onOpen}
/>
)
);
return (
<Wrapper>
{memoList}
</Wrapper>
);
};
MemoList.propTypes = {
memos: ImmutablePropTypes.listOf(
ImmutablePropTypes.mapContains({
id: PropTypes.number,
title: PropTypes.string,
body: PropTypes.body
})
),
onOpen: PropTypes.func
}
export default MemoList;
인덱스 파일 만들어주기
이제 곧 MemoList 컴포넌트를 리덕스에 연결시킬건데요, 그 때 컴포넌트를 불러올 때엔 아마 이렇게 불러와야하겠죠?
import MemoList from 'components/MemoList/MemoList';
MemoList 가 두번이나 입력되는게 좀 맘에 들지 않을 수도 있습니다. 그런 경우에는 디렉토리에 인덱스파일을 만들어서 MemoList 를 불러온다음에 바로 내보내면 됩니다.
src/components/MemoList/index.js
export { default } from './MemoList';
이렇게 하면,
import MemoList from 'components/MemoList';
이런식으로 불러올 수 있게 됩니다.
MemoListContainer 만들기
자, 이제 준비가 어느정도 되었으니 MemoList 를 위한 컨테이너 컴포넌트를 만들어봅시다.
src/containers/MemoListContainer.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MemoList from 'components/MemoList';
class MemoListContainer extends Component {
render() {
const { memos } = this.props;
return (
<MemoList
memos={memos}
/>
);
}
}
export default connect(
(state) => ({
memos: state.memo.get('data')
})
)(MemoListContainer);
이제 이 컴포넌트를 App 에서 렌더링하세요.
src/containers/App.js
// (...)
import MemoListContainer from './MemoListContainer';
class App extends Component {
// (...)
render() {
return (
<Layout>
<Header/>
<Layout.Main>
<WriteMemo/>
<MemoListContainer/>
</Layout.Main>
</Layout>
);
}
}
// (...)
이제 브라우저에서 우리가 작성한 메모들이 나타나는지 확인해보세요.
지금까지 우리가 쓴 메모가 잘 나타났나요? 하지만 아직은, 메모를 새로 작성했을 때 바로 반영이 되지 않습니다.
새 메모 불러오기
신규로딩 API 작성
이제 초기로딩을 마친 이후에 새로 작성된 메모들을 불러오는 신규로딩 API 를 호출해보도록 하겠습니다.
web-api.js
에 다음 함수를 추가하세요.
src/lib/web-api.js
export const getRecentMemo = (cursor) => axios.get(`/memo/?id_gte=${cursor+1}&_sort=id&_order=DESC&`); // cursor 기준 최근 작성된 메모를 불러온다.
이 함수는, cursor
(memo 의 id)를 파라미터로 받아와서 그 값보다 큰 id 를 가진 메모들을 불러옵니다. 또한, 초기로딩을 했을때와 동일하게, 역순으로 정렬되어있습니다.
cursor
는, 기준점이라는 의미로 이해하시면 되겠습니다.
신규로딩 액션 및 리듀서 작성
GET_RECENT_MEMO
액션이 디스패치 되었을때 방금 만든 API가 호출되도록 액션생성자를 만들어주고, 이 요청이 완료되었을때에, concat
함수를 통하여 기존 데이터 리스트의 앞 부분에 새 데이터를 붙여주세요.
src/modules/memo.js
import { createAction, handleActions } from 'redux-actions';
import { Map, List, fromJS } from 'immutable';
import { pender } from 'redux-pender';
import * as WebAPI from 'lib/web-api';
// 액션 타입
const CREATE_MEMO = 'memo/CREATE_MEMO';
const GET_INITIAL_MEMO = 'memo/GET_INITIAL_MEMO';
const GET_RECENT_MEMO = 'memo/GET_RECENT_MEMO';
// 액션 생성자
export const createMemo = createAction(CREATE_MEMO, WebAPI.createMemo) // { title, body }
export const getInitialMemo = createAction(GET_INITIAL_MEMO, WebAPI.getInitialMemo);
export const getRecentMemo = createAction(GET_RECENT_MEMO, WebAPI.getRecentMemo) // cursor
const initialState = Map({
data: List()
});
export default handleActions({
// 초기 메모 로딩
...pender({
type: GET_INITIAL_MEMO,
onSuccess: (state, action) => state.set('data', fromJS(action.payload.data))
}),
// 신규 메모 로딩
...pender({
type: GET_RECENT_MEMO,
onSuccess: (state, action) => {
// 데이터 리스트의 앞부분에 새 데이터를 붙여준다
const data = state.get('data');
return state.set('data', fromJS(action.payload.data).concat(data))
}
})
}, initialState);
WriteMemo 에서 새 메모 작성후 신규로딩 호출하기
이제 WriteMemo 컴포넌트에서 새 메모를 작성한 다음에 신규 로딩을 호출하도록 설정해보겠습니다. 먼저, 컴포넌트를 리덕스에 연결하는 부분에서, 메모 데이터의 첫번째 원소의 id 를 cursor 로 설정하세요. 그 다음에는 handleCreate
메소드 안에서 메모생성 API 가 실행되고 난 다음에, getRecentMemo
에 cursor
를 인자로 설정하여 실행하세요.
만약에 데이터가 아예 비어있다면 리스트가 비어있을테니 cursor의 값이 undefined
일 것입니다. 이럴 경우에는, 기본값을 0 으로 설정하도록 합니다. (그래야 메모가 존재하지 않는 상황에서도 새 데이터를 제대로 불러오겠지요?)
//(...)
class WriteMemo extends Component {
//(...)
handleCreate = async () => {
const { title, body, cursor, MemoActions, UIActions } = this.props;
try {
// 메모 생성 API 호출
await MemoActions.createMemo({
title, body
});
// 신규 메모를 불러옵니다
// cursor 가 존재하지 않는다면, 0을 cursor 로 설정합니다.
await MemoActions.getRecentMemo(cursor ? cursor : 0);
UIActions.resetInput();
// TODO: 최근 메모 불러오기
} catch(e) {
console.log(e); // 에러 발생
}
}
//(...)
}
export default connect(
(state) => ({
focused: state.ui.getIn(['write', 'focused']),
title: state.ui.getIn(['write', 'title']),
body: state.ui.getIn(['write', 'body']),
cursor: state.memo.getIn(['data', 0, 'id'])
}),
(dispatch) => ({
UIActions: bindActionCreators(uiActions, dispatch),
MemoActions: bindActionCreators(memoActions, dispatch)
})
)(enhanceWithClickOutside(WriteMemo));
자, 이제 메모를 작성해보세요. 화면에 바로 나타나나요?
5초마다 새 데이터 불러오기
이제 한가지 더 남았습니다. App 컴포넌트에서도 cursor 를 사용해서 5초마다 새 데이터를 불러오도록 코드를 작성해보겠습니다.
src/containers/App.js
import React, { Component } from 'react';
import Header from 'components/Header';
import Layout from 'components/Layout';
import WriteMemo from './WriteMemo';
import MemoListContainer from './MemoListContainer';
import * as memoActions from 'modules/memo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
class App extends Component {
async componentDidMount() {
const { MemoActions } = this.props;
// 초기 메모 로딩
try {
await MemoActions.getInitialMemo();
this.getRecentMemo();
} catch(e) {
console.log(e);
}
}
getRecentMemo = () => {
const { MemoActions, cursor } = this.props;
MemoActions.getRecentMemo(cursor ? cursor : 0);
// short-polling - 5초마다 새 데이터 불러오기 시도
setTimeout(() => {
this.getRecentMemo()
}, 1000 * 5)
}
render() {
return (
<Layout>
<Header/>
<Layout.Main>
<WriteMemo/>
<MemoListContainer/>
</Layout.Main>
</Layout>
);
}
}
export default connect(
(state) => ({
cursor: state.memo.getIn(['data', 0, 'id'])
}), // 현재는 비어있는 객체를 반환합니다
(dispatch) => ({
MemoActions: bindActionCreators(memoActions, dispatch)
})
)(App);
이렇게 하면, 자신의 창이 아닌 다른 창에서 (혹은 다른 사용자가) 작성을 해도 5초마다 새로운 데이터를 갱신하면서 새 데이터를 화면에 뿌려주게됩니다.
이제 메모를 보여주는 부분은 어느정도 끝났습니다. 아직 구현하지 않은 부분들도있는데요. 스크롤을 아래로 내렸을때 메모를 추가적으로 불러오고, 로딩을 할 때 유저에게 로딩중이란걸 인지하도록 빙글빙글 돌아가는 스피너를 보여주거나, 메모가 화면에 나타날때 애니메이션을 구현하는건 프로젝트가 더 완성되고 나서 마무리하면서 구현하도록하겠습니다.