일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- git
- 노드
- 하프가드
- graphQL
- Express
- 리액트
- 개발
- 드릴
- 디자인패턴
- development
- web
- 클로즈가드
- REACT
- 주짓수
- 프로그래밍
- 영화감상
- 개발자
- 엄티로드
- 영화
- 솔로드릴
- 주짓떼라
- Redux
- 자바스크립트
- 영화리뷰
- JavaScript
- nodejs
- 파이썬
- Node
- 주짓떼로
- 웹개발
- Today
- Total
As i wish
[React SNS] React SNS 만들기 - 4 - 2 (redux saga 적용) 본문
안녕하세요. 엄티입니다.
계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.
프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.
참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.
앞서 포스팅에 이어서 redux-saga 를 적용해보겠습니다.
먼저 saga 를 프로젝트에 적용해보겠습니다.
미들웨어에 적용해주면 되는데, 일전에 chrome에서 redux 를 디버깅 하기 위해 미들웨어를 적용한 적이 있습니다.
그와 비슷합니다.
$ npm i redux-saga axios
redux-saga 를 사용하기 위해 모듈을 설치해주고요 axios 는 서버와 통신 (REST API 사용)을 하기 위함입니다.
그 다음 pages/_app.js
import React from 'react';
import Head from 'next/head';
import PropTypes from 'prop-types';
import withRedux from 'next-redux-wrapper'; // With redux in next
import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { Provider } from 'react-redux';
import reducers from '../reducers';
import rootSaga from '../sagas';
import AppLayout from '../components/AppLayout';
const NodeBird = ({ Component, store }) => {
// Using Provider link react with redux
return (
<Provider store={store}>
<Head>
<title>NodeBird</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.20.5/antd.css"/>
</Head>
<AppLayout>
<Component />
</AppLayout>
</Provider>
);
};
NodeBird.propTypes = {
Component: PropTypes.elementType.isRequired,
store: PropTypes.object.isRequired,
};
const configureStore = (initialState, options) => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: compose(
applyMiddleware(...middlewares),
!options.isServer && typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined' ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f,
);
const store = createStore(reducers, initialState, enhancer);
sagaMiddleware.run(rootSaga);
return store;
};
export default withRedux(configureStore)(NodeBird);
이런식으로 store 에 middleware 를 적용해 줍니다.
그 다음 saga 를 만들어야 겠죠?
reducers 랑 비슷합니다. index 를 만들고 그 안에 post, user 에 관련된 saga 들을 생성해줍니다.
sagas/index.js
import { all, fork } from 'redux-saga/effects';
import user from './user';
import post from './post';
export default function* rootSaga() {
yield all([
fork(user),
fork(post),
]);
}
sagas/user.js
import { all, fork, takeEvery, call, put, delay } from 'redux-saga/effects';
import axios from 'axios';
import * as actions from '../reducers/user';
function loginApi() {
return axios.post('/login');
}
function* login({ payload }) {
try {
const { loginData } = payload;
console.log('Login data', loginData);
// yield call(loginApi);
yield delay(2000);
yield put({
type: actions.LOG_IN_SUCCESS,
});
} catch (error) {
console.error('Login error. ', error);
yield put({
type: actions.LOG_IN_FAILURE,
error,
});
}
}
function logoutApi() {
return axios.post('/logout');
}
function* logout() {
try {
// yield call(logoutApi);
yield delay(2000);
yield put({
type: actions.LOG_OUT_SUCCESS,
});
} catch (error) {
console.error('Logout error. ', error);
yield put({
type: actions.LOG_OUT_FAILURE,
payload: {
error,
},
});
}
}
function signUpApi() {
return axios.post('/signup');
}
function* signUp({ payload }) {
try {
const { signUpData } = payload;
console.log('Sign up data', signUpData);
// yield call(signUpApi);
yield delay(2000);
yield put({
type: actions.SIGN_UP_SUCCESS,
});
} catch (error) {
console.error(error);
yield put({
type: actions.SIGN_UP_FAILURE,
error,
});
}
}
function* watchLogin() {
yield takeEvery(actions.LOG_IN_REQUEST, login);
yield takeEvery(actions.LOG_OUT_REQUEST, logout);
}
function* watchSignUp() {
yield takeEvery(actions.SIGN_UP_REQUEST, signUp);
}
export default function* userSaga() {
yield all([
fork(watchLogin),
fork(watchSignUp),
]);
}
먼저 로그인 및 회원가입 로직에 필요한 사가들을 만들었는데요. 로그인만 먼저 보시겠습니다.
왜냐하면 비동기 처리하는 로직은 다 비슷비슷하기 때문이죠. userSaga 가 all 를 이용하여 watchLogin 을 등록하고 watchLogin 이 takeEvery 를 이용하여 action 을 dispatch 합니다. LOGIN_IN_REQUEST action 이 dispatch 되면 login 이라는 함수를 실행하겠죠?
login은 payload 를 통하여 action 을 dispatch 할 때에 parameter 를 받습니다.
그 후 delay 를 주죠. 이는 원래를 call 을 이용하여 loginApi 를 비동기로 부르는 부분을 간이적으로 구현한거죠.
그 다음 성공/실패 여부에 따라 action 을 dispatch 해주게 됩니다.
어렵지 않죠...?
이에 따라 바뀐 reducers 에 모습도 확인해 보겠습니다.
reducers/user.js
const mockUser = {
nickname: 'MockUserRedux',
Post: [],
Followings: [],
Followers: [],
id: 1,
};
export const initialState = {
isLoggedIn: false, // 로그인 여부
isLoggingOut: false, // 로그아웃 시도중
isLoggingIn: false, // 로그인 시도중
logInErrorReason: '', // 로그인 실패 사유
isSignedUp: false, // 회원가입 성공
isSigningUp: false, // 회원가입 시도중
signUpErrorReason: '', // 회원가입 실패 사유
me: null, // 내 정보
followingList: [], // 팔로잉 리스트
followerList: [], // 팔로워 리스트
userInfo: null, // 남의 정보
};
// Action types
export const SIGN_UP_REQUEST = 'SIGN_UP_REQUEST';
export const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS';
export const SIGN_UP_FAILURE = 'SIGN_UP_FAILURE';
export const LOG_IN_REQUEST = 'LOG_IN_REQUEST';
export const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS';
export const LOG_IN_FAILURE = 'LOG_IN_FAILURE';
export const LOG_OUT_REQUEST = 'LOG_OUT_REQUEST';
export const LOG_OUT_SUCCESS = 'LOG_OUT_SUCCESS';
export const LOG_OUT_FAILURE = 'LOG_OUT_FAILURE';
// Actions creators
export const signUpRequestAction = data => ({
type: SIGN_UP_REQUEST,
payload: {
signUpData: data,
},
});
export const loginRequestAction = data => ({
type: LOG_IN_REQUEST,
payload: {
loginData: data,
},
});
export const logoutRequestAction = () => ({
type: LOG_OUT_REQUEST,
});
// reducers
export default (state = initialState, action) => {
const { type, payload, error } = action;
switch (type) {
case SIGN_UP_REQUEST: {
return {
...state,
isSigningUp: true,
isSignedUp: false,
signUpErrorReason: '',
};
}
case SIGN_UP_SUCCESS: {
return {
...state,
isSigningUp: false,
isSignedUp: true,
};
}
case SIGN_UP_FAILURE: {
return {
...state,
isSigningUp: false,
signUpErrorReason: error,
};
}
case LOG_IN_REQUEST:
return {
...state,
isLoggingIn: true,
logInErrorReason: '',
};
case LOG_IN_SUCCESS:
return {
...state,
isLoggingIn: false,
isLoggedIn: true,
me: mockUser,
isLoading: false,
};
case LOG_IN_FAILURE:
return {
...state,
isLoggingIn: false,
isLoggedIn: false,
logInErrorReason: error,
me: null,
};
case LOG_OUT_REQUEST:
return {
...state,
isLoggingOut: true,
};
case LOG_OUT_SUCCESS:
return {
...state,
isLoggedIn: false,
isLoggingOut: false,
me: {},
};
case LOG_OUT_FAILURE:
return {
...state,
isLoggingOut: false,
};
default:
return state;
}
};
비동기 action 들은 다음과 같이 ACTION_REQUEST, ACTION_SUCCESS, ACTION_FAILURE 로 적어주면 구분하기 편하겠죠? 이것은 자유 입니다. 여기서 한가지 주의할 점이 있는데 reducers 에도 LOG_IN_REQUEST 가 있다는 점이죠.
즉, reducer와 saga 는 독립적으로 존재한다는 사실을 알 수 있는데요. 실제로 LOG_IN_REQUEST action 을 dispatch 해도 saga에서도 reducer에서도 둘다 잡히는것을 볼 수 있습니다.
이를 통하여 reducer 는 reducer대로 saga는 saga 대로 자신의 일을 처리할 수가 있죠.
components/LoginForm.js
import React, { useState, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Link from 'next/link';
import { Form, Input, Button } from 'antd';
import { loginRequestAction } from '../reducers/user';
// Custom hook
export const useInput = (initValue = null) => {
const [value, setter] = useState(initValue);
const handler = useCallback((e) => {
setter(e.target.value);
}, []);
return [value, handler];
};
const LoginForm = () => {
const dispatch = useDispatch();
const { isLoggingIn } = useSelector(state => state.user);
const [id, onChangeId] = useInput('');
const [pass, onChangePass] = useInput('');
const onSubmitForm = useCallback((e) => {
e.preventDefault();
dispatch(loginRequestAction({
id,
pass,
}));
}, [id, pass]);
return (
<>
<Form onSubmit={onSubmitForm} style={{ padding: '10px' }}>
<div>
<label htmlFor="user-id">ID</label>
<br />
<Input name="user-id" required value={id} onChange={onChangeId} />
</div>
<div>
<label htmlFor="user-pass">Password</label>
<br />
<Input name="user-pass" type="password" required value={pass} onChange={onChangePass} />
</div>
<div style={{ marginTop: '10px' }}>
<Button type="primary" htmlType="submit" loading={isLoggingIn}>LogIn</Button>
<Link href="/signup"><Button>SignUp</Button></Link>
</div>
</Form>
</>
);
};
export default LoginForm;
loginForm 도 변경해야하는데요 위처럼 reducers 에서 action creator 를 가져온 뒤 이를 dispatch 만 해주면 됩니다.
그럼 action creator 에 정의한 대로 saga 는 saga 대로 reducer 는 reducer 대로 각자의 일을 하게 됩니다.
위 프로젝트 같은 경우는 login 을 눌렀을 때(LOG_IN_REQUEST 를 dispatch 하였을 때) 에 reducer 는 isLoggingIn 을 true 로 만들어줘서 버튼에 로딩 효과를 주고 saga는 실제 서버에 요청을 하고 요청에 응답에 따라 action 을 dispatch 하게끔 구현하였습니다.
이와 마찬가지로 logout, signup 다 비슷하게 구현 됩니다.
components/UserProfile.js
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Avatar, Button } from 'antd';
import { logoutRequestAction } from '../reducers/user';
const UserProfile = () => {
const dispatch = useDispatch();
const { me, isLoggingOut } = useSelector(state => state.user);
const onLogout = useCallback(() => {
dispatch(logoutRequestAction());
}, []);
return (
<Card
actions={[
<div key="twit">
짹짹
<br />
{me.Post.length}
</div>,
<div key="following">
팔로잉
<br />
{me.Followings.length}
</div>,
<div key="follower">
팔로워
<br />
{me.Followers.length}
</div>,
]}
>
<Card.Meta
avatar={<Avatar>{me.nickname[0]}</Avatar>}
title={me.nickname}
/>
<Button onClick={onLogout} loading={isLoggingOut}>LogOut</Button>
</Card>
);
};
export default UserProfile;
pages/signup.js
import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Form, Input, Checkbox, Button } from 'antd';
import Router from 'next/router';
import { signUpRequestAction } from '../reducers/user';
import { useInput } from '../components/LoginForm';
const Signup = () => {
const dispatch = useDispatch();
const { isSigningUp, me } = useSelector(state => state.user);
// const [id, setId] = useState(''); // Using custom hook in bottom
// const [nick, setNick] = useState(''); // Using custom hook in bottom
const [id, onChangeId] = useInput('');
const [nick, onChangedNick] = useInput('');
const [pass, setPass] = useState('');
const [passChk, setPassChk] = useState('');
const [term, setTerm] = useState('');
const [passError, setPassError] = useState(false);
const [termError, setTermError] = useState(false);
useEffect(() => {
if (me) {
alert('Move to mainpage because logged in.');
Router.push('/');
}
}, [me && me.id]);
const onSubmit = useCallback((e) => {
e.preventDefault();
if (pass !== passChk) {
return setPassError(true);
}
if (!term) {
return setTermError(true);
}
return dispatch(signUpRequestAction({
id,
pass,
nick,
}));
}, [pass, passChk, term]);
// const onChangeId = (e) => { // Using custom hook in bottom
// setId(e.target.value);
// };
// const onChangedNick = (e) => {
// setNick(e.target.value);
// };
const onChangePass = useCallback((e) => {
setPass(e.target.value);
}, []);
const onChangePassChk = useCallback((e) => {
setPassError(e.target.value !== pass);
setPassChk(e.target.value);
}, [pass]);
const onChangeTerm = (e) => {
setTermError(false);
setTerm(e.target.checked);
};
return (
<>
<Form onSubmit={onSubmit} style={{ padding: 10 }}>
<div>
<label htmlFor="user-id">ID</label>
<br />
<Input name="user-id" required value={id} onChange={onChangeId} />
</div>
<div>
<label htmlFor="user-nick">Nickname</label>
<br />
<Input name="user-nick" required value={nick} onChange={onChangedNick} />
</div>
<div>
<label htmlFor="user-pass">Password</label>
<br />
<Input name="user-pass" type="password" required value={pass} onChange={onChangePass} />
</div>
<div>
<label htmlFor="user-pass-chk">Password Check</label>
<br />
<Input name="user-pass-chk" type="password" required value={passChk} onChange={onChangePassChk} />
{passError && <div style={{ color: 'red' }}>Please check password</div>}
</div>
<div>
<Checkbox name="user-term" value={term} onChange={onChangeTerm}>
Are you agree this term?
</Checkbox>
{termError && <div style={{ color: 'red' }}>You must agrre term</div>}
</div>
<div>
<Button type="primary" htmlType="submit" loading={isSigningUp}>Register</Button>
</div>
</Form>
</>
);
};
export default Signup;
Add login, logout, signup Redux-saga
두 번째로 post 부분에 redux-saga를 적용해볼건데요. user 부분과 별다른게 없습니다.
sagas/post.js
import { all, call, fork, put, delay, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import * as actions from '../reducers/post';
function addPostApi() {
return axios.post('/postcard');
}
function* addPost({ payload }) {
try {
const { addPostData } = payload;
console.log('Add post data. ', addPostData);
// yield call(addPostApi);
yield delay(2000);
yield put({
type: actions.ADD_POST_SUCCESS,
});
} catch (error) {
console.error('Add post error. ', error);
yield put({
type: actions.ADD_POST_FAILURE,
error,
});
}
}
function addCommentApi() {
return axios.post('/addcomment');
}
function* addComment({ payload }) {
try {
const { addCommentData } = payload;
console.log('Add comment data.', addCommentData);
// yield call(addCommentApi);
yield delay(2000);
yield put({
type: actions.ADD_COMMENT_SUCCESS,
payload: {
postId: addCommentData.postId,
},
});
} catch (error) {
console.error('Add comment error. ', error);
yield put({
type: actions.ADD_COMMENT_FAILURE,
error,
});
}
}
function* watchAddPost() {
yield takeLatest(actions.ADD_POST_REQUEST, addPost);
}
function* watchAddComment() {
yield takeLatest(actions.ADD_COMMENT_REQUEST, addComment);
}
export default function* postSaga() {
yield all([
fork(watchAddPost),
fork(watchAddComment)
]);
user 부분과 비슷하게 REQUEST, SUCCESS/FAILURE 순으로 이루어집니다.
reducers/post.js
export const initialState = {
mainPosts: [{
id: 1,
User: {
id: 1,
nickname: 'MockUser',
},
content: 'First content',
img: 'https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726',
Comments: [],
}], // 화면에 보일 포스트들
imagePaths: [], // 미리보기 이미지 경로
addPostErrorReason: '', // 포스트 업로드 실패 사유
isAddingPost: false, // 포스트 업로드 중
postAdded: false, // 포스트 업로드 성공
isAddingComment: false,
addCommentErrorReason: '',
commentAdded: false,
};
const dummyPost = {
id: 2,
User: {
id: 1,
nickname: 'MockUser',
},
content: 'Dummy POST',
Comments: [],
};
const dummyComment = {
id: 1,
User: {
id: 1,
nickname: 'MockUser',
},
createdAt: new Date(),
content: 'Dummy REPLY',
};
// Action types
export const ADD_POST_REQUEST = 'ADD_POST_REQUEST';
export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS';
export const ADD_POST_FAILURE = 'ADD_POST_FAILURE';
export const ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST';
export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS';
export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE';
// Action creators
export const addPostRequest = data => ({
type: ADD_POST_REQUEST,
payload: {
addPostData: data,
},
});
export const addCommentRequest = data => ({
type: ADD_COMMENT_REQUEST,
payload: {
addCommentData: data,
},
});
// reducers
export default (state = initialState, action) => {
const { type, payload, error } = action;
switch (type) {
case ADD_POST_REQUEST:
return {
...state,
isAddingPost: true,
postAdded: false,
addPostErrorReason: '',
};
case ADD_POST_SUCCESS:
return {
...state,
isAddingPost: false,
postAdded: true,
mainPosts: [dummyPost, ...state.mainPosts],
};
case ADD_POST_FAILURE:
return {
...state,
isAddingPost: false,
addPostErrorReason: error,
};
case ADD_COMMENT_REQUEST:
return {
...state,
isAddingComment: true,
commentAdded: false,
addCommentErrorReason: '',
};
case ADD_COMMENT_SUCCESS:
const postIndex = state.mainPosts.findIndex(v => v.id === payload.postId);
const post = state.mainPosts[postIndex];
const Comments = [...post.Comments, dummyComment];
const mainPosts = [...state.mainPosts];
mainPosts[postIndex] = { ...post, Comments };
return {
...state,
isAddingComment: false,
commentAdded: true,
mainPosts,
};
case ADD_COMMENT_FAILURE:
return {
...state,
isAddingComment: false,
addCommentErrorReason: error,
};
default:
return state;
}
};
component 부분도 바꾸어 주어야겠죠?
components/PostForm.js
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Form, Input, Button } from 'antd';
import * as actions from '../reducers/post';
const PostForm = () => {
const dispatch = useDispatch();
const { imagePaths, isAddingPost, postAdded } = useSelector(state => state.post);
const [text, setText] = useState('');
useEffect(() => {
if (postAdded) {
setText('');
}
}, [postAdded]);
const onSubmitForm = (e) => {
e.preventDefault();
dispatch(actions.addPostRequest({
text,
}));
};
const onChangeText = (e) => {
setText(e.target.value);
};
return (
<Form style={{ margin: '10px 0 20px' }} encType="multipart/form-data" onSubmit={onSubmitForm}>
<Input.TextArea maxLength={140} placeholder="How was your day?" value={text} onChange={onChangeText} />
<div>
<input type="file" multiple hidden />
<Button>Upload Image</Button>
<Button type="primary" htmlType="submit" loading={isAddingPost}>TWIT</Button>
</div>
<div>
{
imagePaths.map(v => (
<div key={v}>
<img src={`http://localhost:3000/${v}`} alt={v} />
<div>
<Button>Remove</Button>
</div>
</div>
))
}
</div>
</Form>
);
};
export default PostForm;
마지막으로 댓글 부분을 추가하였는데요.
이또한 redux-saga를 적용했으니 함께 보면 좋을것 같습니다.
이미 앞서 언급한 코드인 saga/post.js, reducers/post.js 에서는 댓글부분이 적용되있으니 확인하면 될것 같습니다.
components 만 만들고 이에 대하여 action creator 만 dispatch 해주면 될것 같습니다.
components/CommentForm.js
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { Form, Input, Button } from 'antd';
import * as actions from '../reducers/post';
const CommentForm = ({ postId }) => {
const dispatch = useDispatch();
const { me } = useSelector(state => state.user);
const { isAddingComment, commentAdded } = useSelector(state => state.post);
const [text, setText] = useState('');
useEffect(() => {
if (commentAdded) {
setText('');
}
}, [commentAdded]);
const onSubmitForm = (e) => {
e.preventDefault();
if (!me) {
return alert('Please login');
}
return dispatch(actions.addCommentRequest({
postId,
text,
}));
};
const onChangeText = (e) => {
setText(e.target.value);
};
return (
<Form onSubmit={onSubmitForm}>
<Form.Item>
<Input.TextArea rows={4} value={text} onChange={onChangeText} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={isAddingComment}>Comment</Button>
</Form>
);
};
CommentForm.propTypes = {
postId: PropTypes.number.isRequired,
};
export default CommentForm;
components/CommentList.js
import React from 'react';
import PropTypes from 'prop-types';
import { Avatar, List, Comment } from 'antd';
const CommentList = ({ commentList }) => (
<List
header={`${commentList ? commentList.length : 0} 댓글`}
itemLayout="horizontal"
dataSource={commentList || []}
renderItem={item => (
<li>
<Comment
author={item.User.nickname}
avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
content={item.content}
/>
</li>
)}
/>
);
CommentList.propTypes = {
commentList: PropTypes.array.isRequired,
};
export default CommentList;
components/PostCard.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Card, Avatar, Icon, Button } from 'antd';
import CommentForm from './CommentForm';
import CommentList from './CommentList';
const PostCard = ({ post }) => {
const [commentOpened, setCommentOpened] = useState(false);
const onToggleComment = () => {
setCommentOpened(prev => !prev);
};
return (
<div>
<Card
key={+post.createdAt}
cover={post.img && <img alt="example" src={post.img} />}
actions={[
<Icon type="retweet" key="retweet" />,
<Icon type="heart" key="heart" />,
<Icon type="message" key="message" onClick={onToggleComment} />,
<Icon type="ellipsis" key="ellipsis" />,
]}
extra={<Button>팔로우</Button>}
>
<Card.Meta
avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
title={post.User.nickname}
description={post.content}
/>
</Card>
{
commentOpened && (
<>
<CommentForm postId={post.id} />
<CommentList commentList={post.Comments} />
</>
)
}
</div>
);
};
PostCard.propTypes = {
post: PropTypes.shape({
User: PropTypes.object,
content: PropTypes.string,
img: PropTypes.string,
createdAt: PropTypes.object,
id: PropTypes.number,
Comments: PropTypes.array,
}).isRequired,
};
export default PostCard;
한가지 주의할 점은 reducers/post.js 부분에
case ADD_COMMENT_SUCCESS:
const postIndex = state.mainPosts.findIndex(v => v.id === payload.postId);
const post = state.mainPosts[postIndex];
const Comments = [...post.Comments, dummyComment];
const mainPosts = [...state.mainPosts];
mainPosts[postIndex] = { ...post, Comments };
return {
...state,
isAddingComment: false,
commentAdded: true,
mainPosts,
};
요 부분인데 불변성을 위해서 다음과 같이 적용했습니다.
마지막으로 저의 깃허브 계정을 통하여 빠진 부분이나 에러나는 코드를 확인하면 될것 같습니다.
여기까지 redux-saga 를 적용한 모습이 되겠습니다.
'React JS' 카테고리의 다른 글
[React] Redux-saga 적용하기 (0) | 2019.12.06 |
---|---|
[React SNS] React SNS 만들기 - 5 (BackEnd server - Web server 만들기) (0) | 2019.08.13 |
[React SNS] React SNS 만들기 - 4 - 1 (redux saga 란, airbnbStyle applied) (0) | 2019.08.12 |
[React SNS] React SNS 만들기 - 3 (0) | 2019.08.06 |
[React SNS] React SNS 만들기 - 2 (0) | 2019.07.30 |