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)
댓글 남기기