Redux 연동하기
버전이 다르기 때문에 소스가 강좌와 다를 수 있다.
버전
next: 13.0.4
antd: 5.0.1
소스 : https://github.dev/braverokmc79/node-bird-sns
35. immer 도입하기
$ npm i immer
변경전
reducers/post.js
import shortId from 'shortid'; ~ //이전 상태를 액션을 통해 다음 상태로 만들어내는 함수(불변성은 지키면서) const reducer = (state = initialState, action) => { switch (action.type) { //글작성 case ADD_POST_REQUEST: return { ...state, addPostLoading: true, addPostDone: false, addPostError: null }; case ADD_POST_SUCCESS: return { ...state, mainPosts: [dummyPost(action.data), ...state.mainPosts], addPostLoading: false, addPostDone: true } case ADD_POST_FAILURE: return { ...state, addPostLoading: false, addPostError: action.error } //글삭제 case REMOVE_POST_REQUEST: return { ...state, removePostLoading: true, removePostDone: false, removePostError: null }; case REMOVE_POST_SUCCESS: return { ...state, mainPosts: state.mainPosts.filter((v) => v.id !== action.data), removePostLoading: false, removePostDone: true } case REMOVE_POST_FAILURE: return { ...state, removePostLoading: false, removePostError: action.error } //댓글 작성 case ADD_COMMENT_REQUEST: return { ...state, addCommentLoading: true, addCommentDone: false, addCommentError: null }; case ADD_COMMENT_SUCCESS: { const postIndex = state.mainPosts.findIndex((v) => v.id === action.data.postId); const post = state.mainPosts[postIndex]; post.Comments = [dummyComment(action.data.content), ...post.Comments]; const mainPosts = [...state.mainPosts]; mainPosts[postIndex] = post; return { ...state, mainPosts, addCommentLoading: false, addCommentDone: true } } case ADD_COMMENT_FAILURE: return { ...state, addCommentLoading: false, addCommentError: action.error } default: return state; } } export default reducer;
변경후 =>
import shortId from 'shortid'; import produce from 'immer'; ~ //이전 상태를 액션을 통해 다음 상태로 만들어내는 함수(불변성은 지키면서) const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { //글작성 case ADD_POST_REQUEST: draft.addPostLoading = true; draft.addPostDone = false; draft.addPostError = null; break; case ADD_POST_SUCCESS: draft.addPostLoading = false; draft.addPostDone = true; draft.mainPosts.unshift(dummyPost(action.data)); break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = action.error; break; //글삭제 case REMOVE_POST_REQUEST: draft.removePostLoading = true; draft.removePostDone = false; draft.removePostError = null; break; case REMOVE_POST_SUCCESS: draft.mainPosts = draft.mainPosts.filter((v) => v.id !== action.data); draft.removePostLoading = false; draft.removePostDone = true; break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = action.error; break; //댓글 작성 case ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: { const post = draft.mainPosts.find((v) => v.id === action.data.postId); post.Comments.unshift(dummyComment(action.data.content)); draft.addCommentLoading = false; draft.addCommentDone = true; break; // const postIndex = state.mainPosts.findIndex((v) => v.id === action.data.postId); // const post = state.mainPosts[postIndex]; // post.Comments = [dummyComment(action.data.content), ...post.Comments]; // const mainPosts = [...state.mainPosts]; // mainPosts[postIndex] = post; // return { // ...state, // mainPosts, // addCommentLoading: false, // addCommentDone: true // } } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; default: break; } });
변경전
reducers/user.js
const reducer = (state = initialState, action) => { switch (action.type) { case LOG_IN_REQUEST: console.log("3. 리듀서 LOG_IN_REQUEST : ", action); return { ...state, logInLoading: true, logInDone: false, logInError: null } case LOG_IN_SUCCESS: console.log("4. 리듀서 LOG_IN_REQUEST : ", action); return { ...state, logInLoading: false, logInDone: true, me: dummyUser(action.data) } case LOG_IN_FAILURE: return { ...state, logInLoading: false, logInError: action.error, } //로그 아웃 case LOG_OUT_REQUEST: return { ...state, logOutLoading: true, logOutDone: false, logOutError: null, } case LOG_OUT_SUCCESS: return { ...state, logOutLoading: false, logOutDone: true, me: null } case LOG_OUT_FAILURE: return { ...state, logOutLoading: false, logOutError: action.error } //회원가입 case SIGN_UP_REQUEST: return { ...state, signUpLoading: true, signUpDone: false, signUpError: null, } case SIGN_UP_SUCCESS: return { ...state, signUpLoading: false, signUpDone: true, } case SIGN_UP_FAILURE: return { ...state, signUpLoading: false, signUpError: action.error } //닉네임변경 case CHANGE_NICKNAME_REQUEST: return { ...state, changeNicknameLoading: true, changeNicknameDone: false, changeNicknameError: null, } case CHANGE_NICKNAME_SUCCESS: return { ...state, changeNicknameLoading: false, changeNicknameDone: true, } case CHANGE_NICKNAME_FAILURE: return { ...state, changeNicknameLoading: false, changeNicknameError: action.error } //게시글 수 숫자 증가 case ADD_POST_TO_ME: return { ...state, me: { ...state.me, Posts: [{ id: action.data }, ...state.me.Posts] } }; //게시글 수 숫자 감소 case REMOVE_POST_OF_ME: return { ...state, me: { ...state.me, Posts: state.me.Posts.filter((v) => v.id !== action.data), } } default: return state; } }
변경후 =>
import produce from "immer"; const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { case LOG_IN_REQUEST: draft.logInLoading = true; draft.logInDone = false; draft.logInError = null; break; case LOG_IN_SUCCESS: draft.logInLoading = false; draft.logInDone = true; draft.me = dummyUser(action.data); break; case LOG_IN_FAILURE: draft.logInLoading = false; draft.logInError = action.error; break; //로그 아웃 case LOG_OUT_REQUEST: draft.logOutLoading = true; draft.logOutDone = false; draft.logOutError = null; break; case LOG_OUT_SUCCESS: draft.logOutLoading = false; draft.logOutDone = true; draft.me = null; break; case LOG_OUT_FAILURE: draft.logOutLoading = false; draft.logOutError = action.error; draft; //회원가입 case SIGN_UP_REQUEST: draft.signUpLoading = true; draft.signUpDone = false; draft.signUpError = null; break; case SIGN_UP_SUCCESS: draft.signUpLoading = false; draft.signUpDone = true; break; case SIGN_UP_FAILURE: draft.signUpLoading = false; draft.signUpError = action.error; break; //닉네임변경 case CHANGE_NICKNAME_REQUEST: draft.changeNicknameLoading = true; draft.changeNicknameDone = false; draft.changeNicknameError = null; break; case CHANGE_NICKNAME_SUCCESS: draft.changeNicknameLoading = false; draft.changeNicknameDone = true; break; case CHANGE_NICKNAME_FAILURE: draft.changeNicknameLoading = false; draft.changeNicknameError = action.error; break; //게시글 수 숫자 증가 case ADD_POST_TO_ME: draft.me.Posts.unshift({ id: action.data }); break; // return { // ...state, // me: { // ...state.me, // Posts: [{ id: action.data }, ...state.me.Posts] // } // }; //게시글 수 숫자 감소 case REMOVE_POST_OF_ME: draft.me.Posts = draft.me.Posts.filter((v) => v.id !== action.data); break; // return { // ...state, // me: { // ...state.me, // Posts: state.me.Posts.filter((v) => v.id !== action.data), // } // } default: break; } }); export default reducer;
36. faker로 실감나는 더미데이터 만들기
강의 :
npm i faker 는 오류
npm i @faker-js/faker
https://www.npmjs.com/package/@faker-js/faker
사용법
import { faker } from '@faker-js/faker'; // import { faker } from '@faker-js/faker/locale/de'; export const USERS: User[] = []; export function createRandomUser(): User { return { userId: faker.datatype.uuid(), username: faker.internet.userName(), email: faker.internet.email(), avatar: faker.image.avatar(), password: faker.internet.password(), birthdate: faker.date.birthdate(), registeredAt: faker.date.past(), }; } Array.from({ length: 10 }).forEach(() => { USERS.push(createRandomUser()); });
i 임의 증가값
<img class="card-img-top" src="https://picsum.photos/600/400?random=${i}" onerror="this.src='https://via.placeholder.com/300x200'" />
Unhandled Runtime Error
Error: Text content does not match server-rendered HTML.
오류시
다음 코드를 추가해야 한다.
faker.seed(123);
reducers/post.js
import shortid from 'shortid'; import produce from 'immer'; import { faker } from '@faker-js/faker'; faker.seed(123); ~ initialState.mainPosts = initialState.mainPosts.concat( Array(20).fill().map((v, i) => ({ id: shortid.generate(), User: { id: shortid.generate(), nickname: faker.internet.userName() }, content: faker.lorem.paragraph(), Images: [{ id: shortid.generate(), src: 'https://picsum.photos/600/400?random=' + i, onerror: "https://via.placeholder.com/600x400" }], Comments: [{ id: shortid.generate(), User: { nickname: faker.internet.userName() }, content: faker.lorem.paragraph() }] })) )
37. 무한스크롤
강의 :
reducers/post.js
import shortid from 'shortid'; import produce from 'immer'; import { faker } from '@faker-js/faker'; faker.seed(123); export const initialState = { mainPosts: [], imagePaths: [], hasMorePosts: true, loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null } export const generateDummyPost = (number) => Array(10).fill().map(() => ({ id: shortid.generate(), User: { id: shortid.generate(), nickname: faker.internet.userName() }, content: faker.lorem.paragraph(), Images: [{ id: shortid.generate(), src: 'https://picsum.photos/600/400?random=' + Math.floor(Math.random() * 1000) + 1, onerror: "https://via.placeholder.com/600x400" }], Comments: [{ id: shortid.generate(), User: { nickname: faker.internet.userName() }, content: faker.lorem.paragraph() }] })); // initialState.mainPosts = initialState.mainPosts.concat( // generateDummyPost(10) // ) export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST'; export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS'; export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE'; 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 REMOVE_POST_REQUEST = 'REMOVE_POST_REQUEST'; export const REMOVE_POST_SUCCESS = 'REMOVE_POST_SUCCESS'; export const REMOVE_POST_FAILURE = 'REMOVE_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'; export const addPost = (data) => ({ type: ADD_POST_REQUEST, data }); export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data }); const dummyPost = (data) => ({ id: data.id, content: data.content, User: { id: 1, nickname: '마카로닉스' }, Images: [], Comments: [] }); const dummyComment = (data) => ({ id: shortid.generate(), content: data, User: { id: 1, nickname: '마카로닉스' } }); //이전 상태를 액션을 통해 다음 상태로 만들어내는 함수(불변성은 지키면서) const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { //무한 스크롤 case LOAD_POSTS_REQUEST: draft.loadPostsLoading = true; draft.loadPostsDone = false; draft.loadPostsError = null; break; case LOAD_POSTS_SUCCESS: draft.loadPostsLoading = false; draft.loadPostsDone = true; draft.mainPosts = action.data.concat(draft.mainPosts); draft.hasMorePosts = draft.mainPosts.length < 50; break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; //글작성 case ADD_POST_REQUEST: draft.addPostLoading = true; draft.addPostDone = false; draft.addPostError = null; break; case ADD_POST_SUCCESS: draft.addPostLoading = false; draft.addPostDone = true; draft.mainPosts.unshift(dummyPost(action.data)); break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = action.error; break; //글삭제 case REMOVE_POST_REQUEST: draft.removePostLoading = true; draft.removePostDone = false; draft.removePostError = null; break; case REMOVE_POST_SUCCESS: draft.mainPosts = draft.mainPosts.filter((v) => v.id !== action.data); draft.removePostLoading = false; draft.removePostDone = true; break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = action.error; break; //댓글 작성 case ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: { const post = draft.mainPosts.find((v) => v.id === action.data.postId); post.Comments.unshift(dummyComment(action.data.content)); draft.addCommentLoading = false; draft.addCommentDone = true; break; // const postIndex = state.mainPosts.findIndex((v) => v.id === action.data.postId); // const post = state.mainPosts[postIndex]; // post.Comments = [dummyComment(action.data.content), ...post.Comments]; // const mainPosts = [...state.mainPosts]; // mainPosts[postIndex] = post; // return { // ...state, // mainPosts, // addCommentLoading: false, // addCommentDone: true // } } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; default: break; } }); export default reducer;
saga/post.js
~ //무한 스크롤 function loadPostsAPI(data) { return axios.post('/api/posts', data); } function* loadPosts(action) { try { yield delay(1000); yield put({ type: LOAD_POSTS_SUCCESS, data: generateDummyPost(10) }); } catch (err) { yield put({ type: LOAD_POSTS_FAILURE, error: err.response.data }); } } function* watchLoadPosts() { // yield takeLatest(LOAD_POSTS_REQUEST, loadPosts); yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts); } export default function* postSaga() { yield all([ fork(watchAddPost), fork(watchAddComment), fork(watchRemovePost), fork(watchLoadPosts) ]); }
pages/index.js
import React, { useEffect, useCallback } from 'react'; import AppLayout from './../components/AppLayout'; import { useSelector, useDispatch } from 'react-redux'; import PostCard from './../components/PostCard'; import PostForm from './../components/PostForm'; import { LOAD_POSTS_REQUEST } from './../reducers/post'; const Index = () => { const dispatch = useDispatch(); const { me } = useSelector((state) => state.user); const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post); useEffect(() => { dispatch({ type: LOAD_POSTS_REQUEST }) }, []); useEffect(() => { function onScroll() { if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { if (hasMorePosts && !loadPostsLoading) { dispatch({ type: LOAD_POSTS_REQUEST }) } } } window.addEventListener('scroll', onScroll); //항상 반환처리시 이벤트를 제거해야지 메모리상에 낭비를 줄일 수 있다. return () => { window.removeEventListener('scroll', onScroll); } }, [hasMorePosts, loadPostsLoading]); return ( <AppLayout> {me && <PostForm />} {mainPosts && mainPosts.map((post) => <PostCard key={post.id} post={post} />)} </AppLayout> ); }; export default Index;
38. 팔로우, 언팔로우 구현하기
강의 :
reducer/user.js
import produce from "immer"; export const initialState = { unfollowLoading: false,//언팔로우 시도중 unfollowDone: false, unfollowError: null, followLoading: false,//팔로우 시도중 followDone: false, followError: null, ~ export const FOLLOW_REQUEST = "FOLLOW_REQUEST"; export const FOLLOW_SUCCESS = "FOLLOW_SUCCESS"; export const FOLLOW_FAILURE = "FOLLOW_FAILURE"; export const UNFOLLOW_REQUEST = "UNFOLLOW_REQUEST"; export const UNFOLLOW_SUCCESS = "UNFOLLOW_SUCCESS"; export const UNFOLLOW_FAILURE = "UNFOLLOW_FAILURE"; const reducer = (state = initialState, action) => produce(state, (draft) => { switch (action.type) { //팔로우 case FOLLOW_REQUEST: draft.followLoading = true; draft.followDone = false; draft.followError = null; break; case FOLLOW_SUCCESS: draft.followLoading = false; draft.followDone = true; draft.me.Followings.push({ id: action.data }); break; case FOLLOW_FAILURE: draft.followLoading = false; draft.followError = action.error; break; //언팔로우 case UNFOLLOW_REQUEST: draft.unfollowLoading = true; draft.unfollowDone = false; draft.unfollowError = null; break; case UNFOLLOW_SUCCESS: draft.unfollowLoading = false; draft.unfollowDone = true; draft.me.Followings = draft.me.Followings.filter((v) => v.id !== action.data); break; case UNFOLLOW_FAILURE: draft.unfollowLoading = false; draft.unfollowError = action.error; break; ~
saga/user.js
import { all, fork, put, takeLatest, delay } from 'redux-saga/effects'; import axios from 'axios'; import { LOG_IN_REQUEST, LOG_IN_SUCCESS, LOG_IN_FAILURE, LOG_OUT_REQUEST, LOG_OUT_SUCCESS, LOG_OUT_FAILURE, SIGN_UP_REQUEST, SIGN_UP_SUCCESS, SIGN_UP_FAILURE, FOLLOW_REQUEST, FOLLOW_SUCCESS, FOLLOW_FAILURE, UNFOLLOW_REQUEST, UNFOLLOW_SUCCESS, UNFOLLOW_FAILURE, } from '../reducers/user'; //팔로우 function followAPI(data) { return axios.post('/api/follow', data); } function* follow(action) { try { //const result =yield call(followAPI); yield delay(1000); yield put({ type: FOLLOW_SUCCESS, data: action.data }); } catch (err) { yield put({ type: FOLLOW_FAILURE, error: err.response.data }); } } function* watchFollow() { yield takeLatest(FOLLOW_REQUEST, follow); } //언팔로우 function unfollowAPI(data) { return axios.post('/api/unfollow', data); } function* unfollow(action) { try { //const result =yield call(unfollowAPI); yield delay(1000); yield put({ type: UNFOLLOW_SUCCESS, data: action.data }); } catch (err) { yield put({ type: UNFOLLOW_FAILURE, error: err.response.data }); } } function* watchUnFollow() { yield takeLatest(UNFOLLOW_REQUEST, unfollow); } ` //all 하면 한방에 배열로 적은 함수들이 실행처리 된다. //fork , call 로 실행한다. all 은 fork 나 call 을 동시에 실행시키도록 한다. //call 은 동기 함수 호출 //fork 는 비동기 함수 호출 export default function* userSaga() { yield all([ fork(watchLogIn), fork(watchLogOut), fork(watchFollow), fork(watchUnFollow) ]) }
components/PostCard.js
~ <Card key={post.Image} cover={post.Images[0] && <PostImages images={post.Images} />} actions={[ <RetweetOutlined key="retweet" />, liked ? <HeartTwoTone key="heart" twoToneColor="#ebef96" onClick={onToggleLike} /> : <HeartOutlined key="heart" onClick={onToggleLike} />, <MessageOutlined key="comment" onClick={onToggleComment} />, <Popover content={content} title="" key="popover" style={{ textAlign: "center" }} > <EllipsisOutlined /> </Popover> ]} extra={id && <FollowButton post={post} />} > ~
components/FollowButton.js
import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { Button } from 'antd'; import styled from 'styled-components'; import { useSelector, useDispatch } from 'react-redux'; import { FOLLOW_REQUEST, UNFOLLOW_REQUEST } from '../reducers/user'; const MyFollowButton = styled.div` margin: 10px; `; const FollowButton = ({ post }) => { const dispatch = useDispatch(); const { me, followLoading, unfollowLoading } = useSelector((state) => state.user); const isFollowing = me?.Followings.find((v) => v.id === post.User.id); const onClickButton = useCallback(() => { if (isFollowing) { dispatch(({ type: UNFOLLOW_REQUEST, data: post.User.id })) } else { dispatch(({ type: FOLLOW_REQUEST, data: post.User.id })) } }, [isFollowing]); return ( <MyFollowButton> <Button loading={followLoading || unfollowLoading} onClick={onClickButton}> {isFollowing ? '언팔로우' : '팔로우'} </Button> </MyFollowButton> ); }; FollowButton.propTypes = { post: PropTypes.object.isRequired } export default FollowButton;
react-virtualized 를 활용한 렌더링 성능 최적화
댓글 ( 4)
댓글 남기기