본문 바로가기
React

2021.09.24 React Post Read

by 해맑은 코린이 2021. 9. 25.

2021.09.24 React Post Detail_정리노트

 

으으.. 대장정의 끝이 보이는군 오늘은 detail, list 페이지만들기! 오늘도 역시 UI 준비해서 API 연동하는 흐름 ㅇㅇ!

길..겠지..? ㅠ 반복이지만 그래도 하나하나 찬찬히 다시보기

 

 

일단 디테일 페이지 UI 부터

 

components/post/PostViewer.js ( 새로 생성 )

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

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 SubInfo = styled.div`
  margin-top: 1rem;
  color: ${palette.gray[6]};
  /* span 사이에 가운데점 문자 보여주기 */
  span + span:before {
    color: ${palette.gray[5]};
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    /* 가운데점 문자 */
    content: '\\B7';
  }
`;

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

const PostContent = styled.div`
  font-size: 1.3125rem;
  color: ${palette.gray[8]};
`;
const PostViewer = () => {
  return (
    <PostViewerBlock>
      <PostHead>
        <h1>제목</h1>

        <SubInfo>
          <span>
            <b>tester</b>
          </span>
          {/* 사용자의 문화권에 맞는 시간 표현법으로 현재 시간을 리턴 */}
          <span>{new Date().toLocaleDateString()}</span>
        </SubInfo>
        <Tags>
          <div className="tag">#태그1</div>
          <div className="tag">#태그2</div>
          <div className="tag">#태그2</div>
        </Tags>
      </PostHead>
      <PostContent
    //   리액트에서는 HTML 을 그대로 렌더링하는 형태로 작성하면 일반텍스트 형태로 나타나므로, HTML 적용할 때는  dangerouslySetInnerHTML 이라는 props 설정
    // innerHTML 을 DOM 에서 사용하기위한 대체방법. 사이트간에 스크립팅 공격에 쉽게 노출될 수 있기 때문에 앞에 dangerous 키워드가 붙은 것.
    // 그래서 위험함을 상기시키기 위해 키워드와 함께 __html 키로 객체를 전달함
        dangerouslySetInnerHTML={{ __html: '<p>HTML <b>내용</b>입니다.</p>' }}
      />
    </PostViewerBlock>
  );
};

export default PostViewer;

 

포스트 제목, 작성자 계정명, 작성된 시간, 태그 , 제목 ,내용 을 보여주는 UI 뚝딱!

여기서 하나 짚고 넘어갈 점은 dangerouslySetInnerHTML 부분. 

innderHTML 과 똑같이 html 을 삽입하지만, innderHTML의 경우 DOM 노드가 수정되었을 때 수정여부를 알 수 있는 방법이 없다. 그래서 dangerouslySetInnerHTML 을 사용해서 가상 DOM 과 실제 DOM을 비교하여 변경된 것이 있다면 리렌더링 될 수 있도록 해야한다.

 

책에서는 그냥 일반텍스트 형태로 나타난다해서 따로 찾아보니 좀 더 자세한 설명이있었다. 

 

그럼 예시를 넣었으니까 PostPage 에 다가 넣어주고 주소에 test 로 url 을 입력해서 UI 확인해보장

 

src/pages/PostPage

// read,detail
import React from 'react';
import PostViewer from '../components/post/PostViewer';
import HeaderContainer from '../containers/common/HeaderContainer';

const PostPage = () => {
  return (
    <>
      <HeaderContainer />
      <PostViewer />
    </>
  );
};

export default PostPage;

 

헤더컨테이너랑 같이 넣어주어서 네브바도 같이 뜨게 했다! 나중에 쟤도 컨테이너로 바꾸겠지...끌끌

 

쨋든 실행 화면!

 

실행 화면

http://localhost:3000/@tester/sampleid

 

음음 이쁘구만 홀홀 글러면 이제 API 로 가즈아....

 

먼저 api 호출 함수 ㄱ!

 

lib/api/posts

import client from "./client";

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

// post read id 값 가져오기 위해서 backtic 사용
export const readPost = id => client.get(`api/posts/${id}`)

 

 

그러고.... 리덕스 모듈...작성..고고..굉굉...

 

src/modules/post.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 [READ_POST, READ_POST_SUCCESS, READ_POST_FAILURE] =
  createRequestActionTypes('post/READ_POST');

const UNLOAD_POST = 'post/UNLOAD_POST'; // 포스트 페이지에서 벗어날 때 데이터 비우기

// 액션 생성 함수
export const readPost = createAction(READ_POST, (id) => id);
export const unloadPost = createAction(UNLOAD_POST);

// 사가 생성
const readPostSaga = createRequestSaga(READ_POST, postsAPI.readPost);
export function* postSaga() {
  yield takeLatest(READ_POST, readPostSaga);
}

//초기 상태 설정

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

// 리듀서 함수

const post = handleActions(
  {
    [READ_POST_SUCCESS]: (state, { payload: post }) => ({
      ...state,
      post,
    }),
    [READ_POST_FAILURE]: (state, { payload: error }) => ({
      ...state,
      error,
    }),
    // page 를 벗어났을 때 리덕스의 상태 데이터 비움. 왜 ?? >> 안 비우면 특정 포스트를 읽고 뒤로 돌아가서 다시 다른 포스트를 읽을 때 이전 포스트의 데이터가 잠시 깜빡였다가 나타나는 현상이 발생
    [UNLOAD_POST]: () => initialState,
  },
  initialState,
);

export default post;

 

모듈 작성한 뒤 루트 리듀서, 루트 사가 등록!

 

src/modules/index

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

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

export function* rootSaga(){
    // all 은 배열안에 있는 모든 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 이행될 때까지 기다림.
    // Promise.all 과 비슷하다고 해서 찾아보니 모든 것들이 이행될 때까지 기다리고 하나라도 에러가 나면 모든 Promise는 무시가 되고, catch 문 실행한다니 이와 비슷할듯
    // writeSaga 추가
    yield all([authSaga(),userSaga(),writeSaga(),postSaga()]);
}

export default rootReducer;

 

후.... 이제 컨테이너 작성....꿔꿔....

 

src/containers/post/PostViewerContainer.js ( 새로 생성 )

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router';
import { readPost, unloadPost } from '../../lib/api/posts';
import PostViewer from '../../components/post/PostViewer';

const PostViewerContainer = ({ match }) => {
  //  match 객체를 사용하면 해당 객체의 params 값을 참조할 수 있음
  // 요 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어있음
  const { postId } = match.params;
  const dispatch = useDispatch();
  const { post, error, loading } = useSelector(({ post, loading }) => ({
    post: post.post,
    error: post.error,
    loading: loading['post/READ_POST'],
  }));

  useEffect(() => {
    dispatch(readPost(postId));
    // 언마운트 될 때 리덕스에서 포트스 데이터 없애기
    return () => {
      dispatch(unloadPost());
    };
  }, [dispatch, postId]);
  return <PostViewer post={post} loading={loading} error={error} />;
};

// url 파라미터로 받아온 id 값을 조회하는 match 객체에 접근하기 위해 withRouter 사용
export default withRouter(PostViewerContainer) ;

좋았숴!!! 이제 넘겨준 props 자리깔아주러 PostViewer 로..! 가기전에...!

 

src/pages/PostPage

// read,detail
import React from 'react';
import HeaderContainer from '../containers/common/HeaderContainer';
import PostViewerContainer from '../containers/post/PostViewerContainer';

const PostPage = () => {
  return (
    <>
      <HeaderContainer />
      {/* 교체 */}
      <PostViewerContainer />
    </>
  );
};

export default PostPage;

컨테이너로 교체 ㅎ 

 

src/components/post/PostViewer

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

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 SubInfo = styled.div`
  margin-top: 1rem;
  color: ${palette.gray[6]};
  /* span 사이에 가운데점 문자 보여주기 */
  span + span:before {
    color: ${palette.gray[5]};
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    /* 가운데점 문자 */
    content: '\\B7';
  }
`;

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

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

const PostViewer = ({ post, error, loading }) => {
  // error 발생시
  if (error) {
    // 404 not found 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>
          <span>
            <p>{user.userame}</p>
          </span>
          {/* 사용자의 문화권에 맞는 시간 표현법으로 현재 시간을 리턴 */}
          <span>{new Date(publishedDate).toLocaleDateString()}</span>
        </SubInfo>
        <Tags>
          {tags.map((tag) => (
            <div className="tag">#{tag} </div>
          ))}
        </Tags>
      </PostHead>
      <PostContent
        //   리액트에서는 HTML 을 그대로 렌더링하는 형태로 작성하면 일반텍스트 형태로 나타나므로, HTML 적용할 때는  dangerouslySetInnerHTML 이라는 props 설정
        // innerHTML 을 DOM 에서 사용하기위한 대체방법. 사이트간에 스크립팅 공격에 쉽게 노출될 수 있기 때문에 앞에 dangerous 키워드가 붙은 것.
        // 그래서 위험함을 상기시키기 위해 키워드와 함께 __html 키로 객체를 전달함
        dangerouslySetInnerHTML={{ __html: body }}
      />
    </PostViewerBlock>
  );
};

export default PostViewer;

여기서 content:'\\B7' 부분은 

 

요고임요고 :before 로 span 태그 전에 새로운 content 삽입.

그리고 유니코드로 CSS 에서 중간점을 넣을 수 있다!

 

 

 

후.. 완성 이제 http://localhost:3000/write 로 가서 글 작성하면 디테일 페이지로 갈테지

 

실행 화면

localhost:3000/@username/postId

 

중간에 계속 404 떠서 한참 헤맸..... 하 꼼꼼히 잘 봅시다.... 굿굿 쨋든 디테일 페이지는 잘 뜨니까 이제 list 페이지... 해야지끌끌...

 


 

components/posts/PostList.js ( 새로 생성. 디렉토리 이름 위에는 post 고 얘는 list 이기 때문에 posts임 )

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;
  /* 맨 처음 요소 스타일링 */
  &:first-child {
    padding-top: 0;
  }
  & + & {
    border-top: 1px solid ${palette.gray[2]};
  }

  h2 {
    font-size: 2rem;
    margin-bottom: 0;
    margin-top: 0;
    /* h2:hover 와 같은 의미 */
    &:hover {
      color: ${palette.gray[6]};
    }
  }
  p {
    margin-top: 2rem;
  }
`;

const SubInfo = styled.div`
  color: ${palette.gray[6]};

  span + span:before {
    color: ${palette.gray[4]};
    padding-left: 0.25rem;
    padding-right: 0.25rem;
    /* span */
    content: '\\B7';
  }
`;

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

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

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

export default PostList;

한 파일에 컴포넌트를 2개 생성해주었고, SubInfo, Tags 컴포넌트는 PostViewer 에서 사용한 코드와 같음. , SubInfo marin-top 빼고는 동일! 그래서 똑같은 컴포넌트를 두 번 생성하지말고 분리시켜서 재사용할거임. 분리시키고 나서는 계정명이 나타나는 부분, 각 태그가 나타나는 부분에 Link 를 사용해서 클릭시에 이동할 주소 설정도 해봅시단.

 

 

src/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) =>
//   margin-top props css
    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'; /* 가운뎃점 문자 */
  }
`;

// margin-top, username, publishedDate props
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;

 

src/components/common/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.violet[7]};
    text-decoration: none;
    margin-right: 0.5rem;
    &:hover {
      color: ${palette.violet[2]};
    }
  }
`;

const Tags = ({ tags }) => {
  return (
    <TagsBlock>
        {/* tags props 각 태그 항목 Link 경로는 ?tag={tag} 로! */}
      {tags.map(tag => (
        <Link className="tag" to={`/?tag=${tag}`} key={tag}>
          #{tag}
        </Link>
      ))}
    </TagsBlock>
  );
};

export default Tags;

 

이제 얘네들 불러와서 PostList랑 PostViewer 에 각각 사용

 

src/components/post/PostViewer

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Responsive from '../common/Responsive';
// SubInfo,Tags import
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 SubInfo = styled.div`
//   margin-top: 1rem;
//   color: ${palette.gray[6]};
//   /* span 사이에 가운데점 문자 보여주기 */
//   span + span:before {
//     color: ${palette.gray[5]};
//     padding-left: 0.25rem;
//     padding-right: 0.25rem;
//     /* 가운데점 문자 */
//     content: '\\B7';
//   }
// `;

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

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

const PostViewer = ({ post, error, loading }) => {
  // error 발생시
  if (error) {
    // 404 not found 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>
          <span>
            <p>{user.userame}</p>
          </span>
          {/* 사용자의 문화권에 맞는 시간 표현법으로 현재 시간을 리턴 */}
          {/* <span>{new Date(publishedDate).toLocaleDateString()}</span>
        </SubInfo> */}
         <SubInfo
          username={user.username}
          publishedDate={publishedDate}
          // margin-top props
          hasMarginTop
        />
        {/* <Tags>
          {tags.map((tag) => (
            <div className="tag">#{tag} </div>
          ))}
        </Tags> */}
        <Tags tags={tags} />
      </PostHead>
      <PostContent
        //   리액트에서는 HTML 을 그대로 렌더링하는 형태로 작성하면 일반텍스트 형태로 나타나므로, HTML 적용할 때는  dangerouslySetInnerHTML 이라는 props 설정
        // innerHTML 을 DOM 에서 사용하기위한 대체방법. 사이트간에 스크립팅 공격에 쉽게 노출될 수 있기 때문에 앞에 dangerous 키워드가 붙은 것.
        // 그래서 위험함을 상기시키기 위해 키워드와 함께 __html 키로 객체를 전달함
        dangerouslySetInnerHTML={{ __html: body }}
      />
    </PostViewerBlock>
  );
};

export default PostViewer;

 

주석과 비교하면 재사용하면서 넘겨줄 props 를 꼼꼼히 확인해서 연결!

 

 

src/components/posts/PostList

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';
// SubInfo,Tags import
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;
  /* 맨 처음 요소 스타일링 */
  &:first-child {
    padding-top: 0;
  }
  & + & {
    border-top: 1px solid ${palette.gray[2]};
  }

  h2 {
    font-size: 2rem;
    margin-bottom: 0;
    margin-top: 0;
    /* h2:hover 와 같은 의미 */
    &:hover {
      color: ${palette.gray[6]};
    }
  }
  p {
    margin-top: 2rem;
  }
`;

// const SubInfo = styled.div`
//   color: ${palette.gray[6]};

//   span + span:before {
//     color: ${palette.gray[4]};
//     padding-left: 0.25rem;
//     padding-right: 0.25rem;
//     /* span */
//     content: '\\B7';
//   }
// `;

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

const PostItem = () => {
  return (
    <PostItemBlock>
      <h2>제목</h2>
      {/* sample code */}
      <SubInfo username="username sample" publishedDate={new Date()} />
      <Tags tags={['tag1', 'tag2', 'tag3']} />
      <p>포스트 내용의 일부분쓰</p>
    </PostItemBlock>
  );
};

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

export default PostList;

 

얘는 지금 리덕스와 연결된게 아니기 때문에 샘플코드 렌더링 후 일단 UI 잘 뜨는지 확인!

 

src/pages/PostListPage.js

// list
import React from 'react';
import PostList from '../components/posts/PostList';
import HeaderContainer from '../containers/common/HeaderContainer';
const PostListPage = () => {
    return (
        <div>
            <HeaderContainer />
            {/* list component add */}
           <PostList />
        </div>
    );
};

export default PostListPage;

 

 

실행 화면 

http://localhost:3000/

 

굿 detail page 도 위에 글처럼 잘 떠야함

 

 

 

이제 API  연동을 할건데 list api 에서는 username,page,tag 값을 쿼리 값으로 넣어서 사용하도록 로직이 짜여있어서 API 를 사용할 때 파라미터로 문자열들을 받아와서 직접 조합하기보다는 qs 라이브러리 설치로 쿼리값 생성하도록 구현!

 

$ yarn add qs

 

그러고 api 요청 함수 만들기

 

src/lib/api/posts

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

// post read id 값 가져오기 위해서 backtic 사용
export const readPost = (id) => client.get(`/api/posts/${id}`);

// post list api with qs
export const listPosts = ({ page, username, tag }) => {
  const queryString = qs.stringify({
      page,
      username,
      tag,
  });
//   예시 >>> '/api/posts?username=tester&page=2'
  return client.get(`/api/posts?${queryString}`)
};

 

리덕스..모듈꼬!

 

src/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;

 

아오 post 랑 거ㅓㅓ의 똑같아서 추가로 받는 데이터가 다른거 빼곤 형태 똑같아서 복붙했다가 이름 넘무헷갈려서 걍 적음ㅋㅋㅋㅋㅋㅋㅋ 후... 사가 , 리듀서 생성 후엔 ? 루트 사가, 루트 리듀서 ㄱ 

 

 

src/modules/index

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


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

export function* rootSaga() {
  // all 은 배열안에 있는 모든 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 이행될 때까지 기다림.
  // Promise.all 과 비슷하다고 해서 찾아보니 모든 것들이 이행될 때까지 기다리고 하나라도 에러가 나면 모든 Promise는 무시가 되고, catch 문 실행한다니 이와 비슷할듯
  // postsSaga 추가
  yield all([authSaga(), userSaga(), writeSaga(), postSaga(),postsSaga()]);
}

export default rootReducer;

 

와구와구 이제 좀 많구먼

 

컨테이너... 가야지...이제.... 약간 흐르륵흐르륵(?) 과정이 좀 자연스러워짐... 물론 하나하나 다 이해하고 원리 이해할 때까지 프로젝트 해봐야겠디만...

 

src/containers/posts/PostListContainer.js (새로 생성) 

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

// withRouter 로 location 객체 접근 가능
const PostListContainer = ({ location }) => {
  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(() => {
    //   location.search ? 뒤의 쿼리스트링을 값으로 하는 DOMstring.
    const { tag, username, page } = qs.parse(location.search, {
      ignoreQueryPrefix: true,
    });
    dispatch(listPosts({ tag, username, page }));
  }, [dispatch, location.search]);

  return (
    <PostList
      loading={loading}
      error={error}
      posts={posts}
    //   user 객체가 유효할 때 (user 객체는 현재 로그인 중인 사용자의 정보를 가지고 있음 .) 
      showWriteButton={user}
    />
  );
};

export default withRouter(PostListContainer);

https://developer.mozilla.org/ko/docs/Web/API/Location

 

Location - Web API | MDN

Location 인터페이스는 객체가 연결된 장소(URL)를 표현합니다.

developer.mozilla.org

locataion 객체 설명은 요기!! withRouter 로 접근해서 location.search 쿼리 형식으로 접근할 수 있는데,

ignoreQueryPrefix=true 를 해줘야 ? 없는 온전한 형태의 객체로 받을 수 있음.

 

이제 컨테이너 바꿔서 page 에서 렌더링

 

src/pages/PostListPage

// list
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;

 

 

자 이제 props 받아온거 읏챠챠 받자~~~~

 

src/components/posts/PostList

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';
// SubInfo,Tags import
import SubInfo from '../common/SubInfo';
import Tags from '../common/Tags';
//Link import
import { Link } from 'react-router-dom';

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;
  /* 맨 처음 요소 스타일링 */
  &:first-child {
    padding-top: 0;
  }
  & + & {
    border-top: 1px solid ${palette.gray[2]};
  }

  h2 {
    font-size: 2rem;
    margin-bottom: 0;
    margin-top: 0;
    /* h2:hover 와 같은 의미 */
    &:hover {
      color: ${palette.gray[6]};
    }
  }
  p {
    margin-top: 2rem;
  }
`;

const PostItem = ({ post }) => {
  const { publishedDate, user, tags, title, body, _id } = post;
  return (
    <PostItemBlock>
      <h2>
      // @username/post_id 값을 받아서 detail page 로 이동
        <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 && (
          // user 객체가 있으면 해당 버튼 보여주기
          <Button gray to="/write">
            새글 작성하기
          </Button>
        )}
      </WritePostButtonWrapper>
      {/* 로딩중이 아니고, 포스트 배열이 존재할 때마 해당 UI 띄우기 */}
      {!loading && posts && (
        <div>
          {posts.map((post) => (
            <PostItem post={post} key={post._id} />
          ))}
        </div>
      )}
    </PostListBlock>
  );
};

export default PostList;

 

으어...이제 실행화면..!

 

실행 화면

 

 

여기서 로그아웃하면 ?

 

 

굿 user 객체가 없으니까 버튼안뜨고!!! 

 

http://localhost:3000/@username/post_id

 

제목 클릭하면 detail 까지!!!!! goooood!!!!

 

 

 


느헉..... 같은 패턴이지만 그 안에 하나하나 원리 이해하고 쓸 수 있도록...내공과 짬을 단ㄹ련하자.... 흑흑 이제 update, delete 만 남았다 대장정 얼마 남지 않았으 오늘의 포스팅...끄트으으읕...ㅎㅅㅎ!!!!

댓글