npm i redux-thunk
// store/configureStore.js
import thunkMiddleware from "react-thunk";
const configureStore = () => {
const middlewares = [thunkMiddleware];
...
// 미들웨어 생성
const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
// 아래의 middlewares배열에 추가해주면 됨
const configureStore = () => {
const middlewares = [thunkMiddleware, loggerMiddleware];
아까 설치한 redux-thunk는 삭제하고 redux-saga를 설치
npm i redux-saga
configureStore.js파일에 아래와 같이 코드를 변경해줌
// store/configureStore.js
import createSagaMiddleware from "@redux-saga/core";
import reducer from "../reducers";
import rootSaga from "../sagas";
const loggerMiddleware =
({ dispatch, getState }) => (next) => (action) => {
console.log(action);
return next(action);
};
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware, loggerMiddleware];
const enhancer =
process.env.NODE_ENV === "production"
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
store.sagaTask = sagaMiddleware.run(rootSaga);
return store;
};
// sagas/post.js
// addPost 작성예시
import { all, delay, put, takeLatest, fork, throttle } from "redux-saga/effects";
function addPostAPI(data) {
return axios.post("/api/post", data);
}
function* addPost(action) {
try {
yield delay(1000);
const id = action.data.id;
yield put({
type: ADD_POST_SUCCESS,
data: {
id,
content: action.data,
},
});
yield put({
type: ADD_POST_TO_ME,
data: id,
});
} catch (err) {
yield put({
type: ADD_POST_FAILURE,
data: err.response.data,
});
}
}
function* watchAddPost() {
yield takeLatest(ADD_POST_REQUEST, addPost);
}
export default function* postSaga() {
yield all([
fork(watchAddPost),
...
]);
}
const gen = function* () {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
yield 4;
}
const generator = gen()
gen().next()
// -> 1
// -> { value: undefined, done: false }
gen().next()
// -> 2
// -> { value: undefined, done: false }
gen().next()
// -> 3
// -> { value: 4, done: false }
gen().next()
// -> { value: undefined, done: true }
import { all, fork, take, call } from "redux-saga/effects";
import axios from "axios";
function logInAPI() {
return axios.post("/api/login");
}
function* logIn() {
try {
const result = yield call(logInAPI);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
// 이벤트리스너 같은 역할을 함
function* watchLogin() {
yield take("LOG_IN_REQUEST", logIn);
}
function* watchLogOut() {
yield take("LOG_OUT_REQUEST", logOut);
}
function* watchAddPost() {
yield take("ADD_POST_REQUEST", addPost);
}
export default function* rootSaga() {
yield all([fork(watchLogin), fork(watchLogOut), fork(watchAddPost)]);
}
function* watchLogin() {
while (true) {
yield take("LOG_IN_REQUEST", logIn);
}
}
function* watchLogin() {
yield takeEvery("LOG_IN_REQUEST", logIn);
}
function* watchAddPost() {
yield throttle("ADD_POST_REQUEST", addPost, 2000);
}
// sagas/user.js
import axios from "axios";
import { all, fork, takeEvery } from "redux-saga/effects";
function logInAPI() {
return axios.post("/api/login");
}
function* logIn() {
try {
// const result = yield call(logInAPI);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
function* logOut() {
try {
// const result = yield call(logInAPI);
yield delay(1000);
yield put({
type: "LOG_OUT_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_OUT_FAILURE",
data: err.response.data,
});
}
}
function* watchLogIn() {
yield takeEvery("LOG_IN_REQUEST", logIn);
}
function* watchLogOut() {
yield takeEvery("LOG_OUT_REQUEST", logOut);
}
export default function* userSaga() {
yield all([fork(watchLogIn), fork(watchLogOut)]);
}
// sagas/post.js
import axios from "axios";
import { all, delay, put, takeLatest, fork } from "axios";
function addPostAPI(data) {
return axios.post("api/post", data);
}
function* addPost(action) {
try {
yield delay(1000);
yield put({
type: "ADD_POST_SUCCESS",
});
} catch (err) {
yield put({
type: "ADD_POST_SUCCESS",
data: err.response.data,
});
}
}
function* watchAddPost() {
yield takeLatest("ADD_POST_REQUEST", addPost, 2000);
}
export default function* postSaga() {
yield all([fork(watchAddPost)]);
}
// sagas/index.js
import { all, fork } from "redux-saga/effects";
import postSaga from "./post";
import userSaga from "./user";
export default function* rootSaga() {
yield all([fork(postSaga), fork(userSaga)]);
}
아래를 설치한다
npm i -D babel-eslint
npm i -D eslint-config-airbnb
npm i -D eslint-plugin-import
npm i -D eslint-plugin-react-hooks
npm i -D eslint-plugin-jsx-a11y // 접근성
.eslintrc에 "parser": "babel-eslint"를 써주면 최신 문법을 써도 에러가 나지 않음
"extends": ["airbnb"]로 바꿔준다.
rules에 아래와 같이 추가해준다.
"rules": {
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/anchor-is-valid": "off",
"no-console": "off",
"no-underscore-dangle": "off",
"react/forbid-prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-one-expression-per-line": "off",
"object-curly-newline": "off",
"linebreak-style": "off"
}
(npm run dev를 했을 때 eslint에러가 난다면 ide를 껐다가 다시 켜는 것도 방법 중 하나)
npm i shortid
import shortId from "shortid";
const dummyPost = (data) => ({
id: shortId.generate(),
content: data,
User: {
id: 1,
nickname: "BonnieC",
},
Images: [],
Comments: [],
});
npm i -D faker@5
npm i -D @faker-js/faker // -> 위가 안될 경우, 이걸 설치할 것
// reducers/post.js
...
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: [dummyPost(action.data), ...state.mainPosts],
addPostLoading: false,
addPostDone: true,
};
...
리덕스 뿐만 아니라 react에서도 사용 가능, 데이터 불변성을 위해 사용하는 라이브러리이다.
npm i immer
immer사용을 위해 .eslintrc의 rules에 rule 하나를 추가해주자.
"no-param-reassign": "off"
import produce from 'immer';
const reducer = (state = initialState, action) => {
return produce(state, (draft) => {
draft
})
}
// reducers/post.js
const reducer = (state = initialState, action) => {
switch (action.type) {
...
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,
addCommentLoading: false,
addCommentDone: true,
};
case ADD_COMMENT_FAILURE:
return {
...state,
addCommentLoading: false,
addCommentError: action.error,
};
default:
return state;
}
};
// reducers/user.js
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),
},
};
// reducers/post.js
const reducer = (state = initialState, action) => {
return produce(state, (draft) => {
switch (action.type) {
....
case ADD_COMMENT_REQUEST:
draft.addCommentLoading = true;
draft.addCommentDone = false;
draft.addCommentError = null;
break;
case ADD_COMMENT_SUCCESS:
// 1. 게시글을 찾는다.
const post = draft.mainPosts.find((v) => v.id === action.data.postId);
// 2. 게시글에 새로운 코멘트를 하나 추가해주면 됨
post.Comments.unshift(dummyComment(action.data.content));
draft.addCommentLoading = false;
draft.addCommentDone = true;
break;
case ADD_COMMENT_FAILURE:
draft.addCommentLoading = false;
draft.addCommentError = action.error;
break;
default:
break;
}
});
};
// reducers/user.js
case ADD_POST_TO_ME:
draft.me.Posts.unshift({ id: action.data });
break;
case REMOVE_POST_OF_ME:
draft.me.Posts = draft.me.Posts.filter((v) => v.id !== action.data);
break;
initialState.mainPosts = initialState.mainPosts.concat(
Array(20)
.fill()
.map(() => ({
id: shortId.generate(),
User: {
id: shortId.generate(),
nickname: faker.name.findName(),
},
content: faker.lorem.paragraph(),
Images: [
{
src: faker.image.image(),
},
],
Comments: [
{
User: {
id: shortId.generate(),
nickname: faker.name.findName(),
},
content: faker.lorem.sentence(),
},
],
}))
);
// pages/index.js
useEffect(() => {
function onScroll() {
console.log(
window.scrollY,
document.documentElement.clientHeight,
document.documentElement.scrollHeight
);
}
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
});
// pages/index.js
// 스크롤이 바닥에서 300px 위쪽까지 도달하면 다음 10개 포스트 로딩을 준비
if (window.scrollY + document.documentElement.clientHeight >
document.documentElement.scrollHeight - 300) {
...
}
// sagas/post.js
function* watchLoadPosts() {
yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts);
}
// pages/index.js
useEffect(() => {
function onScroll() {
if (
window.scrollY + document.documentElement.clientHeight >
document.documentElement.scrollHeight - 300
) {
if (hasMorePost && !loadPostsLoading) {
dispatch({
type: LOAD_POSTS_REQUEST,
});
}
}
}
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, [mainPosts, hasMorePost, loadPostsLoading]);