all fork call put 요런 것들에 대해서 알아보자. 얘네들을 saga의 이펙트라고 부른다.
그래서 보통 rootSaga() 하나 만들어 놓고 거기에 비동기 액션들을 하나씩 넣어준다. thunk처럼 async action creator라고 표현하기엔 좀 그렇고 한번 비교를 해보자.
// sagas폴더에 index.js
import { all, fork, call, put } from "redux-saga/effects";
export default function* rootSaga() {
yield all([
fork(watchLogin),
]);
}
rootSaga()안에 all은 배열을 받는데 배열을 받으면 그 배열안에 들어있는 것들을 한방에 다 실행을 해준다.
그러면 아래 watchLogin watchLogout watchAddPost 3개가 다 실행이 될 것이다. fork는 함수를 실행한다는 뜻이다.
import { all, fork, call, put } from "redux-saga/effects";
function* watchLogin() {
yield take('LOG_IN');
}
function* watchLogout() {
yield take("LOG_OUT");
}
function* watchAddPost() {
yield take("ADD_POST");
}
export default function* rootSaga() {
yield all([
fork(watchLogIn),
fork(watchLogOut),
fork(watchAddPost),
]);
}
근데 이것은 call이랑 다르다. fork 대신에 call로 할 수도 있는데 명확한 차이점을 알고 넘어가자.
fork나 call로 generator 함수를 실행을 해준다고 일단 알고있자. all은 그런애들을 동시에 실행할 수 있게 해준다.
watchLogIn이 실행됬다고 쳐보자. take(LOG_IN) 즉 LOG_IN이 될 때까지 기다리겠다 이 뜻이다.
그리고 LOG_IN이 실행되면 logIn 이것을 실행하게 하자. 여기서 주의할 점 logInAPI는 generator가 아니다. *붙이면 에러가 난다고 보면된다. 아래처럼 logInAPI에 코드를 넣어서 실제로 서버에 요청을 보내게 된다.
그러면 logInAPI 예를 실행한 것이고 결과값을 받을 수가 있다. 즉 function logInAPI에서 서버로 로그인하는 요청을 보냈다하면 function* logIn()에서 요청의 결과를 받는 것이다.
import { all, fork, call, take, put } from "redux-saga/effects";
function logInAPI() {
return axios.post("/api/login");
}
function* logIn() {
const result = yield call(logInAPI);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
}
}
function* watchLogin() {
yield take('LOG_IN', logIn);
}
이제 axios를 설치하자. 그리고 요청이라는 것이 항상 성공하는 것이 아니라 요청이 실패할 수 도 있다.
요청이 실패하는 경우에 대비하기 위해서 functin* logIn()을 try catch로 감싸주자.
여기서 성공 결과는 result.data에 있고 실패 결과는 err.response.data에 담겨있다.
npm i axios
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,
});
}
}
thunk에서는 async action creator를 직접 실행했지만 saga에서는 async action creator가 직접 실행되는 것이 아니라 eventListener같은 역할을 한다. LOG_IN이라는 action이 들어오면 logIn generator 함수를 실행하도록 한다.
즉 saga에서는 eventListener같은 느낌을 준다는 것이다.
즉 logIn이 실행되면 yield call을 해서 항상 effect 앞에는 yield call을 붙여주고 call을 해서 loginAPI을 실행하고 return값을 result에 받는다. 만약에 서버요청한게 실패를 한다 그러면 catch(err)쪽으로 가게된다. put은 머냐 put은 dispatch이다.
put을 dispatch라고 생각하면 아래 객체를 dispatch!! 오호 action 객체를 dispatch!! action 객체를 dispatch하는 거구나라고 할 수 있다.
// 이곳이 실행되게 한다.
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", logIn);
}
또 function* logIn()부분에다가 LOG_IN_REQUEST를 추가해주자.
function* logIn() {
try {
yield put({
type: "LOG_IN_REQUEST",
});
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,
});
}
}
그런데 여기서 보면 action이 많다고 느낄 수가 있다. LOG_IN / LOG_IN_REQUEST / LOG_IN_SUCCESS / LOG_IN_FAILURE 이렇게 action이 4개나 나오니까 액션이 너무 많다고 생각이 든다.
그러면 밑에 있는 LOG_IN 을 LOG_IN_REQUEST 로 바꿔보자. 어짜피 로그인이라는 걸 하나 로그인 요청을 하는 순간에 eventListener을 실행하나 마찬가지이다. 괜히 쓸데없이 action을 만들지 말자.
function* logIn() {
try {
yield put({
type: "LOG_IN_REQUEST",
});
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);
}
나머지 밑에것들도 다 바꿔주자.. 하나라도 action이 적은게 좋다 redux할 때는 action이 너무 많이 나온다는게 단점이기 때문이다.
function* watchLogin() {
yield take("LOG_IN_REQUEST", logIn);
}
function* watchLogout() {
yield take("LOG_OUT_REQUEST");
}
function* watchAddPost() {
yield take("ADD_POST_REQUEST");
}
call 대신에 fork도 아래 함수를 실행하는데 call이랑 fork의 차이가 무엇일까?
fork는 비동기 함수호출이다.
call은 동기 함수호출이다.
즉 call을 하면 logInAPI가 return할 때까지 기다려서 result에다가 넣어주는데
fork를 하면 비동기이기 때문에 요청 보내버리고 결과 기다리는거 상관없이 yield를 통해 바로 다음 구문인 type과 data가 실행이 된다.(non-blocking)
function* logIn() {
try {
yield put({
type: "LOG_IN_REQUEST",
});
// 아랫 부분
const result = yield fork(logInAPI);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
즉 fork를 쓰면 axios.post('/api/login')과 마찬가지이다.
try {
const result = yield fork(logInAPI);
axios.post('/api/login')
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
만약에 call이다 그러면 then을 하고나서 아래와 같이 한다. 둘의 차이점을 알자.
결과값을 받을 때까지 기다려주느냐(call) 아니면 다음줄로 넘어가느냐(fork)
try {
const result = yield call(logInAPI);
axios.post('/api/login')
.then(() => {
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
});
그래서 아래에서는 call을 쓰면 안된다. then을 안넣어줘서 결과값이 안나오기 때문에
// 결과값이 존재안한다.
try {
const result = yield call(logInAPI);
axios.post('/api/login')
yield put({
type: "LOG_IN_SUCCESS",
data: result.data
});
call을 쓰려면 아래와 같이 then을 넣어줘야 한다.
function* logIn() {
try {
const result = yield call(logInAPI);
axios.post('/api/login')
.then((result) => {
yield put({
type: "LOG_IN_SUCCESS",
data: result.data
});
})
즉 yield가 await와 비슷하다 async 함수인 경우에는 await을 써주는데 그거랑 비슷하다.
function* logIn() {
try {
const result = await call(logInAPI);
await put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
fork를 쓰게 되면 await을 빠뜨린 역할을 하게 해주는 것이다. 그래서 동기냐 비동기냐 blocking이냐 non-blocking이냐 이 두개의 차이라고 보면된다.
try {
const result = fork(logInAPI);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data
});
즉 eventListener 잔뜩 만들어줘서(정확히 eventListener는 아니고 비유를 그렇게 들었다.)
function* watchLogin() {
yield take("LOG_IN_REQUEST", logIn);
}
function* watchLogout() {
yield take("LOG_OUT_REQUEST");
}
function* watchAddPost() {
yield take("ADD_POST_REQUEST");
}
아래에다가 한방에 all로 등록을 해주고
export default function* rootSaga() {
yield all([fork(watchLogIn), fork(watchLogOut), fork(watchAddPost)]);
}
로그인 REQUEST 액션이 실행되면
function* watchLogin() {
yield take("LOG_IN_REQUEST", logIn);
}
function* 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 logOutAPI() {
return axios.post("/api/logOut");
}
function* logOut() {
try {
const result = yield call(logOutAPI);
yield put({
type: "LOG_OUT_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_OUT_FAILURE",
data: err.response.data,
});
}
}
function* watchLogout() {
yield take("LOG_OUT_REQUEST", logOut);
}
만약에 addPost가 필요하다. 같은패턴이다.
function addPostAPI() {
return axios.post("/api/post");
}
function* addPost() {
try {
const result = yield call(addPostAPI);
yield put({
type: "ADD_POST_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "ADD_POST_FAILURE",
data: err.response.data,
});
}
}
function* watchAddPost() {
yield take("ADD_POST_REQUEST", addPost);
}
generator의 원리와 이펙트들의 원리, 전체적인 흐름을 파악하여서
saga의 다양한 이펙트를 자유롭게 사용해보자.
all fork call take put delay debounce throttle takeLatest takeEvery takeLeading takeMaybe
그리고 로그인 할때 그냥 요청만 보내는 것이 아니라 실제 데이터를 넣어서 로그인을 해줘야 한다. 그 데이터는 어디서 받아올 수 있냐 로그인 리퀘스트 할 때 type : LOG_IN_REQUEST = 어떤 데이터(로그인과 관련된 어떤 데이터)
그러면 로그인 리퀘스트에 대한 액션 자체가 아래에 매개변수로 전달이 된다.
그러면 action.type하면 로그인 리퀘스트가 나올거고 action.data하면 로그인 데이터가 들어있을 것이다.
아래와같이 logInAPI에 action.data를 넣어주면 위에 있는 function logInAPI(data)로 들어가게 된다.
function logInAPI(data) {
return axios.post("/api/login", data);
}
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
call이나 fork가 동작방법이 특이한데 보통은 함수 호출할 때 아래와 같이 하는데
logInAPI(action.data);
call을 쓰면 이걸 펼춰져야 한다.
call(logInAPI, action.data);
만약 인수를 여러개 넘기고 싶다 그러면
call(logInAPI, action.data);
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data, 'a', 'b', 'c');
아래와 같이 데이터가 들어가게 된다.
// 첫번째 자리가 함수고 그 다음은 함수의 매개변수들로 들어가게된다.
function logInAPI(data, 'a', 'b', 'c') {
return axios.post("/api/login", data);
}
함수를 차라리 아래와 같이 호출하면 더 편한데
function* logIn(action) {
try {
const result = yield logInAPI(action.data);
굳이 아래처럼 하는 이유는 뭘까 왜 call이라는 effect를 쓸까
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield 같은 것도 안붙여줘도 되긴 하는데 왜 붙여줄까 이펙트들 앞에 yield를 붙여주는 이유중 하나가 saga는 테스트 할 때 엄청 편하기 때문이다. yield를 넣어줘서 generator를 쓰면 프로그램 동작원리를 테스트하기가 편해진다.
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
예를들어 아래코드를 하면
const l = logIn({ type: 'LOG_IN_REQUEST', data: { id: 'kjh950601@gmail.com' }})
l.next();
이 코드에서
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}
아래코드까지만 실행된다.
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
그 다음 next를 한번 더하면 아래까지 실행된다. 즉 generator를 써서 한줄 한줄 실행을 해보면서 실제로 돌려볼 수 있어서 매우 편하다.
l.next()
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
마지막으로 로그아웃은 데이터가 필요없고 addPost는 post에 대한 데이터가 필요할 것이다.
// data가 이곳으로 전달되서
function addPostAPI(data) {
// 여기로 간다
return axios.post("/api/post".data);
}
function* addPost() {
try {
// action에서 data를 꺼내서
const result = yield call(addPostAPI, action.data);
yield put({
type: "ADD_POST_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "ADD_POST_FAILURE",
data: err.response.data,
});
}
}
<출처 조현영: [리뉴얼] React로 NodeBird SNS 만들기>
[리뉴얼] React로 NodeBird SNS 만들기 - 인프런 | 강의
리액트 & 넥스트 & 리덕스 & 리덕스사가 & 익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다., 새로 만나는 제로초의 리액트 노드버
www.inflearn.com
'React > NodeBird(ZeroCho)' 카테고리의 다른 글
saga 쪼개고 reducer와 연결하기 (0) | 2021.10.29 |
---|---|
take 시리즈, throttle 알아보기 (0) | 2021.10.29 |
saga 설치 & generator 이해하기 (0) | 2021.10.28 |
redux-thunk 이해하기 (0) | 2021.10.28 |
정규표현식 맛보기 (0) | 2021.10.28 |