관리 메뉴

거니의 velog

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

React_프론트엔드 프로젝트

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

Unlimited00 2024. 2. 23. 17:23

2. 포스트 목록 페이지 구현하기

* 이번에는 여러 개의 포스트를 보여 주는 포스트 목록 페이지를 구현해 본다.

(1) PostList UI 준비하기

* PostList라는 컴포넌트를 만든다. 이 컴포넌트에서는 포스트들을 배열로 받아 와서 렌더링해 준다. 사용자가 로그인 중이라면 페이지 상단 우측에 새 글 작성하기 버튼을 보여 준다.

* components/posts 디렉터리를 만들고, 그 안에 PostList 컴포넌트를 다음과 같이 작성해 주자.

[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';

const PostListBlock = styled(Responsive)`
  margin-top: 3rem;
`;

const WritePostButtonWrapper = styled.div`
  display: flex;
  justify-content: flex-end;
  margin-bottom: 3rem;
`;

const PostItemBlock = styled.div`
  padding-top: 3rem;
  padding-bottom: 3rem;
  /* 맨 위 포스트는 padding-top 없음 */
  &:first-child {
    padding-top: 0;
  }
  & + & {
    border-top: 1px solid ${palette.gray[2]};
  }

  h2 {
    font-size: 2rem;
    margin-bottom: 0;
    margin-top: 0;
    &:hover {
      color: ${palette.gray[6]};
    }
  }
  p {
    margin-top: 2rem;
  }
`;

const SubInfo = styled.div`
  /* margin-top: 1rem; */
  color: ${palette.gray[6]};

  /* span 사이에 가운뎃점 문자 보여 주기 */
  span + span::before {
    color: ${palette.gray[4]};
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    content: '\\B7'; /* 가운뎃점 문자 */
  }
`;

const Tags = styled.div`
  margin-top: 0.5rem;
  .tag {
    display: inline-block;
    color: ${palette.cyan[7]};
    text-decoration: none;
    margin-right: 0.5rem;
    &:hover {
      color: ${palette.cyan[6]};
    }
  }
`;

const PostItem = () => {
  return (
    <PostItemBlock>
      <h2>제목</h2>
      <SubInfo>
        <span>
          <b>username</b>
        </span>
        <span>{new Date().toLocaleDateString()}</span>
      </SubInfo>
      <Tags>
        <div className="tag">#태그1</div>
        <div className="tag">#태그2</div>
      </Tags>
      <p>포스트 내용의 일부분...</p>
    </PostItemBlock>
  );
};

const PostList = () => {
  return (
    <PostListBlock>
      <WritePostButtonWrapper>
        <Button cyan to="/write">
          새 글 작성하기
        </Button>
      </WritePostButtonWrapper>
      <div>
        <PostItem />
        <PostItem />
        <PostItem />
      </div>
    </PostListBlock>
  );
};

export default PostList;

* 이 컴포넌트에 사용된 SubInfo 컴포넌트와 Tags 컴포넌트는 PostViewer 에서 사용한 코드와 같다. 한 가지 차이점이라면, SubInfo 컴포넌트의 경우 margin-top이 없다는 것이다.

* 이렇게 똑같은 코드를 두 번 선언하는 대신, SubInfo 컴포넌트와 Tags 컴포넌트를 common 디렉터리에 따로 분리시켜서 재사용해 보자. 그리고 분리시킬 때 계정명이 나타나는 부분과 각 태그가 나타나는 부분에 Link를 사용하여 클릭 시 이동할 주소를 설정해 줄 것이다.

* 먼저 SubInfo 를 분리시킨다.

[components/common/SubInfo.js]

import React from 'react';
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
import palette from '../../lib/styles/palette';

const SubInfoBlock = styled.div`
  ${(props) =>
    props.hasMarginTop &&
    css`
      margin-top: 1rem;
    `}
  color: ${palette.gray[6]};

  /* span 사이에 가운뎃점 문자 보여주기*/
  span + span:before {
    color: ${palette.gray[4]};
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    content: '\\B7'; /* 가운뎃점 문자 */
  }
`;

const SubInfo = ({ username, publishedDate, hasMarginTop }) => {
  return (
    <SubInfoBlock hasMarginTop={hasMarginTop}>
      <span>
        <b>
          <Link to={`/@${username}`}>{username}</Link>
        </b>
      </span>
      <span>{new Date(publishedDate).toLocaleDateString()}</span>
    </SubInfoBlock>
  );
};

export default SubInfo;

* SubInfo 컴포넌트는 hasMarginTop 값이 true 이면 상단 여백을 주고, 그렇지 않으면 여백이 없다. 그리고 username과 publishedDate를 props로 받아 와서 보여 주도록 설정했다.

* 다음으로 Tags 컴포넌트를 만든다.

[components/comons/Tags.js]

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';

const TagsBlock = styled.div`
  margin-top: 0.5rem;
  .tag {
    display: inline-block;
    color: ${palette.cyan[7]};
    text-decoration: none;
    margin-right: 0.5rem;
    &:hover {
      color: ${palette.cyan[6]};
    }
  }
`;

const Tags = ({ tags }) => {
  return (
    <TagsBlock>
      {tags.map((tag) => (
        <Link className="tag" to={`/?tag=${tag}`} key={tag}>
          #{tag}
        </Link>
      ))}
    </TagsBlock>
  );
};

export default Tags;

* Tags 컴포넌트에서는 tags 값을 props로 받아 와서 태그 목록을 렌더링해 준다. 각 태그 항목을 Link 컴포넌트로 작성했으며, 클릭했을 때 이동 경로는 /?tag=태그 로 설정했다.

* SubInfo 컴포넌트와 Tags 컴포넌트를 다 만들었으면, PostList에서 기존 SubInfo와 Tags를 지우고 이번에 새로 만든 컴포넌트를 불러와서 사용해 보자.

[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';

const PostListBlock = styled(Responsive)`
  margin-top: 3rem;
`;

const WritePostButtonWrapper = styled.div`
  display: flex;
  justify-content: flex-end;
  margin-bottom: 3rem;
`;

const PostItemBlock = styled.div`
  padding-top: 3rem;
  padding-bottom: 3rem;
  /* 맨 위 포스트는 padding-top 없음 */
  &:first-child {
    padding-top: 0;
  }
  & + & {
    border-top: 1px solid ${palette.gray[2]};
  }

  h2 {
    font-size: 2rem;
    margin-bottom: 0;
    margin-top: 0;
    &:hover {
      color: ${palette.gray[6]};
    }
  }
  p {
    margin-top: 2rem;
  }
`;

const PostItem = () => {
  return (
    <PostItemBlock>
      <h2>제목</h2>
      <SubInfo username="username" publishedDate={new Date()} />
      <Tags tags={['태그1', '태그2', '태그3']} />
      <p>포스트 내용의 일부분...</p>
    </PostItemBlock>
  );
};

const PostList = () => {
  return (
    <PostListBlock>
      <WritePostButtonWrapper>
        <Button cyan to="/write">
          새 글 작성하기
        </Button>
      </WritePostButtonWrapper>
      <div>
        <PostItem />
        <PostItem />
        <PostItem />
      </div>
    </PostListBlock>
  );
};

export default PostList;

* 컴포넌트를 잘 수정한 뒤, PostListPage 컴포넌트에서 PostList 컴포넌트를 렌더링하여 작성한 컴포넌트가 잘 나타나는지 확인해 보자.

[pages/PostListPage.js]

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

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

export default PostListPage;

- http://localhost:3000/

PostList 컴포넌트 UI

* PostList 컴포넌트가 잘 나타나는가?

* 이번에는 PostItem 컴포넌트를 만들 때 SubInfo와 Tags 컴포넌트를 재사용할 수 있도록 분리했었는데, 이 컴포넌트들을 이전에 만든 PostVIewer 에서도 재사용해 볼 것이다.

[components/post/PostViewer.js]

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Responsive from '../common/Responsive';
import SubInfo from '../common/SubInfo';
import Tags from '../common/Tags';

const PostViewerBlock = styled(Responsive)`
  margin-top: 4rem;
`;

const PostHead = styled.div`
  border-bottom: 1px solid ${palette.gray[2]};
  padding-bottom: 3rem;
  margin-bottom: 3rem;
  h1 {
    font-size: 3rem;
    line-height: 1.5;
    margin: 0;
  }
`;

const PostContent = styled.div`
  font-size: 1.3125rem;
  color: ${palette.gray[8]};
`;

const PostViewer = ({ post, error, loading }) => {
  // 에러 발생 시
  if (error) {
    if (error.response && error.response.status === 404) {
      return <PostViewerBlock>존재하지 않는 포스트입니다.</PostViewerBlock>;
    }
    return <PostViewerBlock>오류 발생!</PostViewerBlock>;
  }

  // 로딩중이거나, 아직 포스트 데이터가 없을 시
  if (loading || !post) {
    return null;
  }

  const { title, body, user, publishedDate, tags } = post;

  return (
    <PostViewerBlock>
      <PostHead>
        <h1>{title}</h1>
        <SubInfo
          username={user.username}
          publishedDate={publishedDate}
          hasMarginTop
        />
        <Tags tags={tags} />
      </PostHead>
      <PostContent dangerouslySetInnerHTML={{ __html: body }} />
    </PostViewerBlock>
  );
};

export default PostViewer;

* 여러 곳에서 재사용할 수 있는 컴포넌트는 이렇게 따로 분리하여 사용하면, 코드의 양도 줄일 수 있을 뿐 아니라 유지 보수성도 높일 수 있어서 좋다.