관리 메뉴

거니의 velog

(16) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 3 본문

React_프론트엔드 프로젝트

(16) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 3

Unlimited00 2024. 2. 23. 18:07

(2) 포스트 목록 조회 API 연동하기

* postLIst 컴포넌트에서 실제 데이터를 보여 줄 수 있도록 API를 연동해 보자. 우리가 사용할 list API는 username, page, tag 값을 쿼리 값으로 넣어서 사용한다. API를 사용할 때 파라미터로 문자열들을 받아 와서 직접 조합해도 되지만, 여기서는 qs 라이브러리를 사용하여 쿼리 값을 생성할 것이다. 이 라이브러리를 사용하면 쿼리 값을 더 편리하게 생성하고 JSON으로 변환할 수 있다.

* yarn 으로 qs를 설치하자.

$ yarn add qs

* 그리고 lib/api/posts.js 파일에 다음 함수를 추가하자.

import qs from 'qs';
import client from './client';

export const writePost = ({ title, body, tags }) =>
  client.post('/api/posts', { title, body, tags });

export const readPost = (id) => client.get(`/api/posts/${id}`);

export const listPosts = ({ page, username, tag }) => {
  const queryString = qs.stringify({
    page,
    username,
    tag,
  });
  return client.get(`/api/posts?${queryString}`);
};

* listPosts API를 호출할 때 파라미터로 값을 넣어 주면 /api/posts?username=tester&page=2와 같이 주소를 만들어서 호출한다.

* 이제 위 요청의 상태를 관리하는 리덕스 모듈을 만들어 보자. modules 디렉터리에 posts.js 파일을 만들어서 다음 코드를 작성하자.

[modules/posts.js]

import { createAction, handleActions } from 'redux-actions';
import createRequestSaga, {
  createRequestActionTypes,
} from '../lib/createRequestSaga';
import * as postsAPI from '../lib/api/posts';
import { takeLatest } from 'redux-saga/effects';

const [LIST_POSTS, LIST_POSTS_SUCCESS, LIST_POSTS_FAILURE] =
  createRequestActionTypes('posts/LIST_POSTS');

export const listPosts = createAction(
  LIST_POSTS,
  ({ tag, username, page }) => ({ tag, username, page }),
);

const listPostsSaga = createRequestSaga(LIST_POSTS, postsAPI.listPosts);
export function* postsSaga() {
  yield takeLatest(LIST_POSTS, listPostsSaga);
}

const initialState = {
  posts: null,
  error: null,
};

const posts = handleActions(
  {
    [LIST_POSTS_SUCCESS]: (state, { payload: posts }) => ({
      ...state,
      posts,
    }),
    [LIST_POSTS_FAILURE]: (state, { payload: error }) => ({
      ...state,
      error,
    }),
  },
  initialState,
);

export default posts;

* 다 작성한 뒤에는 루트 리듀서와 루트 사가에 방금 만든 리듀서와 사가를 등록하자.

[modules/index.js]

import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import loading from './loading';
import user, { userSaga } from './user';
import write, { writeSaga } from './write';
import post, { postSaga } from './post';
import posts, { postsSaga } from './posts';

const rootReducer = combineReducers({
  auth,
  loading,
  user,
  write,
  post,
  posts,
});

export function* rootSaga() {
  yield all([authSaga(), userSaga(), writeSaga(), postSaga(), postsSaga()]);
}

export default rootReducer;

* 다음으로 containers 디렉터리 안에 posts 디렉터리를 만들고, 그 안에 PostListContainer 컴포넌트를 만든다. 이 컴포넌트는 주소에 있는 쿼리 파라미터를 추출하여 우리가 만들었던 listPosts API를 호출해 준다.

[containers/posts/PostListContainer.js]

import React, { useEffect } from 'react';
import qs from 'qs';
import { withRouter } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import PostList from '../../components/posts/PostList';
import { listPosts } from '../../modules/posts';

const PostListContainer = ({ location, match }) => {
  const dispatch = useDispatch();

  const { posts, error, loading, user } = useSelector(
    ({ posts, loading, user }) => ({
      posts: posts.posts,
      error: posts.error,
      loading: loading['posts/LIST_POSTS'],
      user: user.user,
    }),
  );

  useEffect(() => {
    const { username } = match.params;
    const { tag, page } = qs.parse(location.search, {
      ignoreQueryPrefix: true,
    });
    dispatch(listPosts({ tag, username, page }));
  }, [dispatch, location.search, match.params]);

  return (
    <PostList
      loading={loading}
      error={error}
      posts={posts}
      showWriteButton={user}
    />
  );
};

export default withRouter(PostListContainer);

* PostList 컴포넌트를 사용할 때 showWriteButton props를 현재 로그인 중인 사용자의 정보를 지니고 있는 user 객체로 설정해 주었다. 이렇게 하면 user 객체가 유효할 때, 즉 사용자가 로그인 중일 때만 포스트를 작성하는 버튼이 나타난다.

* 컨테이너 컴포넌트를 완성한 후, PostListPage 컴포넌트에서 PostList를 PostListContainer 로 대체시키자.

[pages/PostListPage.js]

import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';
import PostListContainer from '../containers/posts/PostListContainer';

const PostListPage = () => {
  return (
    <div>
      <HeaderContainer />
      <PostListContainer />
    </div>
  );
};

export default PostListPage;

* 그리고 PostList에서 받아온 props에 따라 결과물을 보여 주자.

[components/posts/PostList.js]

import React from 'react';
import styled from 'styled-components';
import Responsive from '../common/Responsive';
import Button from '../common/Button';
import palette from '../../lib/styles/palette';
import SubInfo from '../common/SubInfo';
import Tags from '../common/Tags';
import { Link } from 'react-router-dom';

(...)

const PostItem = ({ post }) => {
  const { publishedDate, user, tags, title, body, _id } = post;
  return (
    <PostItemBlock>
      <h2>
        <Link to={`/@${user.username}/${_id}`}>{title}</Link>
      </h2>
      <SubInfo
        username={user.username}
        publishedDate={new Date(publishedDate)}
      />
      <Tags tags={tags} />
      <p>{body}</p>
    </PostItemBlock>
  );
};

const PostList = ({ posts, loading, error, showWriteButton }) => {
  // 에러 발생 시
  if (error) {
    return <PostListBlock>에러가 발생했습니다.</PostListBlock>;
  }

  return (
    <PostListBlock>
      <WritePostButtonWrapper>
        {showWriteButton && (
          <Button cyan to="/write">
            새 글 작성하기
          </Button>
        )}
      </WritePostButtonWrapper>
      {/*  로딩 중 아니고, 포스트 배열이 존재할 때만 보여줌 */}
      {!loading && posts && (
        <div>
          {posts.map((post) => (
            <PostItem post={post} key={post._id} />
          ))}
        </div>
      )}
    </PostListBlock>
  );
};

export default PostList;

* 여기까지 작성하면 페이지에 다음과 같은 결과가 나타날 것이다.

PostListContainer 구현하기

* 내용이 나타나는 부분에 HTML 태그가 그대로 보인다. 이 태그를 없애는 작업은 서버 쪽에서 해 주어야 한다. 물론 클라이언트에서 처리하는 방법도 있지만, 현재는 포스트 리스팅을 할 때 body의 글자수를 200자로 제한하는 기능이 있다. 이 때문에 완성된 HTML이 아니라 HTML의 일부분만 전달되어 HTML 태그를 없애는 작업이 잘 이루어지지 않을 가능성이 있다.