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
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 객체가 없으니까 버튼안뜨고!!!
제목 클릭하면 detail 까지!!!!! goooood!!!!
느헉..... 같은 패턴이지만 그 안에 하나하나 원리 이해하고 쓸 수 있도록...내공과 짬을 단ㄹ련하자.... 흑흑 이제 update, delete 만 남았다 대장정 얼마 남지 않았으 오늘의 포스팅...끄트으으읕...ㅎㅅㅎ!!!!
'React' 카테고리의 다른 글
2022.05.11 TypeScript 로 React 세팅하기 (2) | 2022.05.11 |
---|---|
2021.09.28 React Update,Delete (2) | 2021.09.29 |
2021.09.24 React Post Create (0) | 2021.09.24 |
2021.09.22 React 로그인, 회원가입 (3) (1) | 2021.09.22 |
2021.09.21 React 로그인, 회원가입(2) (0) | 2021.09.21 |
댓글