saga의 단점은 코드줄이 매우 길어진다는 것이다.
logIn만 해도 그렇다. action 하나당 3개씩 세트이기 때문에 add comment follow like unlike unfollow 이런 것들이 action 3개씩 나온다. 그래서 이제 saga를 한번 나눠보자..
function logInAPI(data) {
return axios.post("/api/login", data);
}
function* logIn(action) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
function* watchLogin() {
yield takeLatest("LOG_IN_REQUEST", logIn);
}
reducer은 합칠 때 combine reducer로 합쳤는데 그런거 까지는 필요가 없다. 그리고 index.js에 있는 것 user에 관련된 것들을 user.js에 그대로 옮겨준다.
// sagas 폴더안에 user.js
import { all, fork } from "redux-saga/effects";
// sagas 폴더 안에 index.js에 있는 코드를 user.js 안에다가 옮겨 준다.
function logInAPI(data) {
return axios.post("/api/login", data);
}
function* logIn(action) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
function logOutAPI() {
return axios.post("/api/logOut");
}
function* logOut() {
try {
// const result = yield call(logOutAPI);
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 takeLatest("LOG_IN_REQUEST", logIn);
}
function* watchLogout() {
yield takeLatest("LOG_OUT_REQUEST", logOut);
}
export default function* userSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut),
]),
}
post에 관련된 것들은 post.js에 옮겨준다.
// sagas 폴더안에 post.js
import axios from "axios";
import { delay, put, takeLatest, all, fork } from "redux-saga/effects";
function addPostAPI(data) {
return axios.post("/api/post".data);
}
function* addPost(action) {
try {
// const result = yield call(addPostAPI, action.data);
yield delay(1000);
yield put({
type: "ADD_POST_SUCCESS",
});
} 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)]);
}
index.js는 아래와 같이 코드를 작성하여 post와 user.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),
]);
}
이제 sagas의 user.js에 있는 LOG_IN_REQUEST / LOG_OUT_REQUEST에 맞춰서 reducer랑 실제 컴포넌트랑 한번 바꿔보자.
// reducers 폴더안에 user.js파일
export const initialState = {
isLoggingIn: false, // 로그인 시도중
isLoggedIn: false,
isLoggingOut: false, // 로그아웃 시도중
me: null,
signUpData: {},
loginData: {},
};
export const loginRequestAction = (data) => {
return {
type: "LOG_IN_REQUEST",
data,
};
};
export const loginSuccessAction = (data) => {
return {
type: "LOG_IN_SUCCESS",
data,
};
};
export const loginFailureAction = (data) => {
return {
type: "LOG_IN_FAILURE",
data,
};
};
export const logoutRequestAction = () => {
return {
type: "LOG_OUT_REQUEST",
};
};
export const logoutSuccessAction = () => {
return {
type: "LOG_OUT_SUCCESS",
};
};
export const logoutFailureAction = () => {
return {
type: "LOG_OUT_FAILURE",
};
};
const reducer = (state = initialState, action) => {
switch (action.type) {
// 요청 들어왔을 때
case "LOG_IN_REQUEST":
return {
...state,
isLoggingIn: true,
};
// 요청 성공했을 때
case "LOG_IN_SUCCESS":
return {
...state,
isLoggingIn: false,
isLoggedIn: true,
me: action.data,
};
// 요청 실패했을 때
case "LOG_IN_FAILURE":
return {
...state,
isLoggingIn: false,
isLoggedIn: false,
};
case "LOG_OUT_REQUEST":
return {
...state,
isLoggingOut: true,
};
case "LOG_OUT_SUCCESS":
return {
...state,
isLoggingOut: false,
isLoggedIn: false,
me: null,
};
case "LOG_OUT_FAILURE":
return {
...state,
isLoggingOut: false,
};
default:
return state;
}
};
export default reducer;
LoginForm.js의 아래 코드를 false에서 isLoggingIn으로 바꾸자. 그러면 버튼이 로딩중으로 바뀐다. 나머지 코드들도 보자.
// LoginForm.js
import { useDispatch, useSelector } from "react-redux";
import { loginRequestAction } from "../reducers/user";
const LoginForm = () => {
const dispatch = useDispatch();
const { isLoggingIn } = useSelector((state) => state.user);
const [id, onChangeId] = useInput("");
const [password, onChangePassword] = useInput("");
const onSubmitForm = useCallback(() => {
console.log(id, password);
dispatch(loginRequestAction({ id, password }));
}, [id, password]);
<Button type="primary" htmlType="submit" loading={isLoggingIn}>로그인</Button>
그리고 UserProfile에서도 logoutRequestAction으로 해놓는다.
// UserProfile.js
import { logoutRequestAction } from "../reducers/user";
const UserProfile = () => {
const dispatch = useDispatch();
const { me, isLoggingOut } = useSelector((state) => state.user);
const onLogOut = useCallback(() => {
dispatch(logoutRequestAction());
}, []);
<Button onClick={onLogOut} loadging={isLoggingOut}>
로그아웃
</Button>
</Card>
);
};
그리고 sagas 폴더안에 user.js의 LOG_IN_SUCCESS 코드에다가 data를 넣어준다.
// sagas폴더의 user.js
// logIn 리퀘스트에서 보낸 데이터를 바로 success로 보낸다.
function* logIn(action) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: action.data,
});
그리고 reducers폴더의 user.js에 있는 이 부분이 필요가 없는게 우리가 호출하는 것이 아니라 saga가 알아서 호출을 해준다.
// reducers의 user.js 아래 코드는 필요가 없다.
export const loginSuccessAction = (data) => {
return {
type: "LOG_IN_SUCCESS",
data,
};
};
export const loginFailureAction = (data) => {
return {
type: "LOG_IN_FAILURE",
data,
};
};
export const logoutSuccessAction = () => {
return {
type: "LOG_OUT_SUCCESS",
};
};
export const logoutFailureAction = () => {
return {
type: "LOG_OUT_FAILURE",
};
};
// sagas폴더의 user.js 파일
function* logIn(action) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: action.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
이제 전체적으로 흐름을 한 번 파악해보자. LoginForm에서 사용자가 로그인을 할 것이다. 아이디 비밀번호를 적고 로그인을 눌렀다고 치자. 로그인을 누르면 loginRequestAction이 실행이 될 것이다.
<Input name="user-id" value={id} onChange={onChangeId} required />
<Input
name="user-password"
type="password"
value={password}
onChange={onChangePassword}
required
/>
<Button type="primary" htmlType="submit" loading={isLoggingIn}>
로그인
</Button>
dispatch(loginRequestAction({ id, password }));
그 다음에는 어디가 실행되냐면 sagas폴더의 user.js에서 아래에 걸리게 될 것이다. 이벤트 역할을 하는 LOG_IN_REQUEST 부분 이거 하면서 logIn이 실행이 된다.
function* watchLogin() {
yield takeLatest("LOG_IN_REQUEST", logIn);
}
function* logIn(action) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: action.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
그와 동시에 실행되는 것이 또 하나 있는데 reducers폴더의 user.js파일에서 switch부분의 LOG_IN이 실행이 된다.
switch (action.type) {
case "LOG_IN_REQUEST":
return {
...state,
isLoggingIn: true,
};
즉 sagas폴더의 user.js 부분과 reducers폴더의 user.js부분이 동시에 실행이 된다. 동시라고 해도 실제로 완전히 동시는 아니고 둘이 순서는 있다. 각 user.js파일에다가 console.log을 찍어보자.
// sagas - user.js
function* logIn(action) {
try {
// 콘솔찍기
console.log("saga logIn");
// const result = yield call(logInAPI, action.data);
// 1초뒤에 login success가 된다. login success가 dispatch되면 다시 reducer에서
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: action.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
// reducers - user.js
const reducer = (state = initialState, action) => {
switch (action.type) {
case "LOG_IN_REQUEST":
console.log("reducer logIn");
return {
...state,
isLoggingIn: true,
};
// 이 부분이 실행되게 된다.
case "LOG_IN_SUCCESS":
return {
...state,
isLoggingIn: false,
isLoggedIn: true, // me에 데이터가 들어가면 isLoggedIn이 true가 된다.
// 실행되는 순간 me에 데이터가 들어가게 된다.
me: { ...action.data, nickname: "jay" },
};
그러면 AppLayout에서 loginForm이 아니라 UserProfile로 바뀌게 된다.
대략 순서가 request 먼저되고 request될 때 reducer랑 saga랑 동시에 실행되고 saga에서 1초뒤에 success를 하는데
success를 하면 isLoggedIn이 true가 되니까 logInForm에서 UserProfile로 바뀌게 된다. 즉 로그인하고 1초뒤에 UserProfile로 바뀌겠구나 라고 생각하자.
<Row gutter={8}>
<Col xs={24} md={6}>
// LoginForm에서 UserProfile로 바뀌겠구나
{isLoggedIn ? <UserProfile /> : <LoginForm />}
</Col>
<Col xs={24} md={12}>
{children}
</Col>
<Col xs={24} md={6}>
<a
href="https://k-j-hyeon.github.io/portfolio/"
target="_blank"
rel="noreferrer noopener"
>
Made by Jay With Vanilla JavaScript
</a>
</Col>
</Row>
npm run dev를 해서 돌려보았는데 지금 문제점이 로그인 버튼이 로딩동작에서 1초뒤에 success 단계로 넘어가야 하는데 넘어가지 않고 계속해서 로딩창이 뜨고 있다. 해결을 해야한다.
상태 정리하기 파트와 eslint 점검 , 게시글, 댓글 saga 작성 게시글 삭제 saga 작성 immer 도입, 무한 스크롤링 적용 팔로우 언팔로우 구현하는거는 차차 정리를 하자....
<출처 조현영: [리뉴얼] React로 NodeBird SNS 만들기>
[리뉴얼] React로 NodeBird SNS 만들기 - 인프런 | 강의
리액트 & 넥스트 & 리덕스 & 리덕스사가 & 익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다., 새로 만나는 제로초의 리액트 노드버
www.inflearn.com
'React > NodeBird(ZeroCho)' 카테고리의 다른 글
eslint로 프로젝트 점검하기 (0) | 2021.10.31 |
---|---|
take 시리즈, throttle 알아보기 (0) | 2021.10.29 |
saga 이펙트 알아보기 (0) | 2021.10.29 |
saga 설치 & generator 이해하기 (0) | 2021.10.28 |
redux-thunk 이해하기 (0) | 2021.10.28 |