함수를 실행할 때 특정구간에 멈춰두거나, 원하는 시점에 다시 돌아가게 할 수도 있고, 결과값을 여러번 받아올 수도 있습니다.
function* generatorFunction() {
console.log('첫번째 next()');
yield 1;
console.log('두번째 next()');
yield 2;
console.log('세번째 next()');
yield 3;
return 4;
const generator = generatorFunction()
console.log(generator.next()) // 첫번째 next() { value : 1, done : false }
console.log(generator.next()) // 두번째 next() { value : 2, done : false }
console.log(generator.next()) // 세번째 next() { value : 3, done : false }
console.log(generator.next()) // { value: 4, done: true }
function* generatorFunction() : function에 * 표시를 하여 Generator 함수를 작성할 수 있습니다.
const generator = generatorFunction() : 제네레이터 함수를 호출한다고 바로 함수 안에 코드가 실행되지는 않습니다.
generator.next()를 호출해야지만 함수를 실행할 수 있고, yield 한 값을 return하고 코드의 실행을 멈추고 다시 호출되었을때 return한 yield값 이후의 코드부터 실행됩니다.
done : false 값은 다음에 실행할 코드가 남아있다는 뜻이고 true는 함수의 코드가 모두 종료되었음을 의미합니다.

| 함수명 | 내용 | 예시 | 해석 |
|---|---|---|---|
| put | 특정 액션을 Dispatch 한다 | put({type: 'INCREMENT'}) | INCREMENT 액션을 Dispatch 한다 |
| takeEvery | 요청이 들어오는 모든 액션에 대해 특정 작업을 한다 | takeEvery(INCREASE_ASYNC, increaseSaga) | 모든 INCREASE_ASYNC 액션에 대해 increaseSaga 함수를 실행한다 |
| takeLatest | 기존에 진행 중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업만 수행한다. | takeLatest(DECREASE_ASYNC, decreaseSaga) | DECREASE_ASYNC 액션에 대해서 기존에 진행중인 작업은 취소 처리하고 마지막으로 실행된 액션에 대해 decreaseSaga 함수를 실행한다 |
| call | 주어진 함수를 실행한다. | call(delay, 1000) | delay(1000)함수를 call함수를 사용해서 이렇게 쓸 수도 있다. |
| all | 제너레이터 함수를 배열의 형태로 인자로 넣어주면, 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 resolve 될때까지 기다린다. | yield all([testSaga1(), testSaga2()]) | testSaga1()과 testSaga2()가 동시에 실행되고, 모두 resolve될 때까지 기다린다 |
- Redux-saga는 Action을 모니터링하고 있다가, 특정 Action이 발생하면 이에 따라 특정 작업을 하는 방식입니다.
- 특정 Action이 발생했을 때 이에 따라 다른 Action을 Dispatch 시키거나 Javascript 코드를 실행 할 수 있습니다.
router.post('/login', async (req, res) => {
const done = (error, user, info) => {
// 오류 검증
if (error || !user) return res.status(500).json({ error, user, info });
const { password, createdAt, updatedAt, ...payload } = user.dataValues;
const accessToken = jwt.sign(payload);
res.json({
token: accessToken,
});
};
passport.authenticate('local', { session: false }, done)(req, res);
});
router.post('/me', passport.authenticate('jwt', { session: false }), async (req, res) => {
res.json({
result: true,
user: req.user,
});
});
프론트 서버에서 요청이 오면 로그인 기능을 처리할 로직입니다.
const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware];
const enhancer =
process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middleware)) // 배포모드
: composeWithDevTools(applyMiddleware(...middleware)); // 개발모드
const store = createStore(rootReducer, enhancer);
sagaMiddleware.run(rootSaga);
const persistor = persistStore(store);
const Store = ({ children }) => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
</Provider>
);
};
export default Store;
const sagaMiddleware = createSagaMiddleware();: Saga를 Redux Store에 연결하기 위해서는 미들웨어를 사용해야 하기때문에 Saga 미들웨어를 생성합니다.const middleware = [sagaMiddleware];: middleware에 생성한 sagaMiddleware를 담아줍니다.sagaMiddleware.run(rootSaga);: 모든 saga들이 action을 watching 해야하기 때문에 전체 관리를 해주는 rootSaga를 실행시켜줍니다.
const useForm = (defaultValue, onSubmit, validate) => {
const [values, setValues] = useState(defaultValue);
const [submit, setSubmit] = useState(false);
const [errors, setErrors] = useState({});
const onChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
setSubmit(true);
setErrors(validate(values)); // validate
};
useEffect(() => {
const init = async () => {
if (submit) {
if (Object.keys(errors).length === 0) {
onSubmit(values);
}
setSubmit(false);
}
};
init();
}, [errors]);
return {
...Object.keys(defaultValue).reduce((acc, v) => {
acc[v] = {
value: values[v],
onChange,
};
return acc;
}, {}),
handleSubmit,
errors,
submit,
};
};
export default useForm;
- 기본적인 상태를 생성하고 onChange 이벤트가 발생될 때마다 상태를 바꿔줄 수 있도록 onChange 함수를 만들어주었습니다.
- onSubmit 이벤트가 발생될 때 Submit을 true로 바꾸어주고 errors를 validate( )로 이메일 형식과 비밀번호를 검사한 결과값을 담아줍니다.
- errors가 변경될 때마다 init 함수가 실행되어 Submit이 true이고 errors에 아무것도 없는, 즉 폼체크에서 에러가 나지 않았을때 onSumbit함수를 실행시키고 Submit을 false로 바꿔줍니다.
- defaultValue의 key값을 가져와서 reduce( )로 객체형태로 변환시킬것이고 객체의 key값은 Object.keys(defaultValue)의 리턴값이 차례대로 들어가고 value값은 해당 key값에 해당하는 상태 value를 넣어줍니다.
const Login = () => {
const dispatch = useDispatch(); // dispatch : action을 발동
const navigate = useNavigate(); // useNavigate : 페이지 이동 처리
const user = useSelector((state) => state.user); // useSelector : state 조회
const inititalstate = { email: '', password: '' };
const onSubmit = (payload) => {
dispatch(user_login_request({ ...payload }));
};
// Custom Hook
const { email, password, handleSubmit, errors, submit } = useForm(inititalstate, onSubmit, validate);
useEffect(() => {
if (user.isLogin === true) {
navigate('/');
alert('로그인 성공');
}
}, [user.isLogin, navigate]);
return (
<>
<AuthLayout>
<form onSubmit={handleSubmit}>
<h3>로그인</h3>
<AuthInputBox type="text" name="email" {...email} placeholder="아이디를 입력해주세요." />
{errors.email && <span>{errors.email}</span>}
<br />
<AuthInputBox type="password" name="password" {...password} placeholder="패스워드를 입력해주세요." />
{errors.password && <span>{errors.password}</span>}
<br />
<StyledButton fullWidth type="submit" disabled={submit}>
로그인
</StyledButton>
</form>
<Footer>
<Link to="/register">회원가입</Link>
</Footer>
</AuthLayout>
</>
);
};
const onSubmit = (payload) => { dispatch(user_login_request({ ...payload })); };로그인을 시도했을때 폼체크 부분에서 errors가 없다면 실행될 함수입니다.
dispatch로 action을 발동시키며 인자값으로 payload를 전달해줍니다.
payload는 input에 입력한 값들이 담깁니다.
useEffect(() => { if (user.isLogin === true) { navigate('/'); alert('로그인 성공'); } }, [user.isLogin, navigate]);user.isLogin을 체크하여 성공적으로 로그인이 되었을때 실행될 Hook입니다.
export default function* rootSaga() {
yield all([watchCounterUp(), watchList(), userSaga()]);
}
모든 saga들이 action을 watching 해야하기 때문에 전체 관리할 rootSaga를 만들어줍니다.
같은 디렉토리에서 댓글과 Counter를 테스트해보았어서 사용하였습니다.
async function loginAPI(payload) {
const result = await axios.post('http://localhost:3500/user/login', payload);
const { token } = result.data;
const response = await axios.post('http://localhost:3500/user/me', null, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response;
}
function* login(action) {
try {
const result = yield call(loginAPI, action.payload);
yield put({
type: 'USER/LOGIN_SUCCESS',
payload: result.data,
});
} catch (e) {
yield put({
type: 'USER/LOGIN_FAILURE',
error: e.response.data,
});
}
}
export default function* userSaga() {
yield takeLatest(user_login_request.toString(), login);
}
export default function* userSaga() { yield takeLatest(user_login_request.toString(), login); }
user_login_request.toString()액션이 실행되면 login 함수를 실행합니다.
const persist = {
key: 'user',
storage,
whitelist: ['user'], // 전체 상태값 중에 user만 localstorage를 사용하겠다.
};
const rootReducer = combineReducers({ counter, comment, user });
export default persistReducer(persist, rootReducer);
const rootReducer = combineReducers({ counter, comment, user });combineReducer로 reducer들을 관리하기 쉽도록 한데 묶어 rootReducer에 담아서 사용하였습니다.
const initialState = {
me: {
id: null,
email: null,
nickname: null,
provider: null,
},
ifLogin: false,
error: null,
loadding: false,
};
const USER_LOGIN = {
REQUEST: 'USER/LOGIN_REQREST',
SUCCESS: 'USER/LOGIN_SUCCESS',
FAILURE: 'USER/LOGIN_FAILURE',
};
export const user_login_request = createAction(USER_LOGIN.REQUEST);
export const user_login_success = createAction(USER_LOGIN.SUCCESS);
export const user_login_failure = createAction(USER_LOGIN.FAILURE);
const user = (state = initialState, action) => {
switch (action.type) {
case USER_LOGIN.REQUEST:
return { ...state, loadding: true, isLogin: false, error: null };
case USER_LOGIN.SUCCESS:
return { ...state, loadding: false, isLogin: true, me: { ...action.payload.user }, error: null };
case USER_LOGIN.FAILURE:
return { ...state, loadding: false, isLogin: false, error: action };
default:
return state;
}
};
export default user;
각 action별로 처리할 reducer의 로직입니다. 전역에서 관리할 초기값 또한 이곳에서 작성해주었습니다.
- Login.jsx에서 input에 회원가입한 email과 해당 아이디의 패스워드를 입력하고 폼체크 부분에서 에러가 없을경우 onSubmit을 통하여 action / user_login_request가 발동되고 인자값으로 payload를 받습니다.
- userSaga.js에서 user_login_request.toString() action을 캐치하여 takeLatest로 마지막 실행된 작업만 수행하게 하여 로그인 버튼을 많이 클릭하더라도 마지막 1번만 실행되도록 설정해주었고 , 2번째 인자값에 login 함수가 실행됩니다.
- login 함수가 실행되면 제일 먼저 call( )함수가 실행되어 loginAPI 함수를 실행시키고, 실행시킬 함수에 인자값으로 action.payload를 전달해줍니다.
- loginAPI 함수가 실행되면 axios로 요청을 보내는데 이때 body부분에 입력한 input의 내용인 email과 password가 담겨있는 payload를 같이 보내주고 응답으로 access_token을 받고 받은 토큰으로 다시 user/me에 요청을 보내는데 이때는 option부분에 header에 token을
Authorization: `Bearer ${token}이런식으로 같이 요청을 보냅니다. 백서버에서 응답으로 보낸 user에 대한 정보를 repsonse로 받고 loginAPI함수는 종료됩니다.
- 다시 login 함수로 와서 성공적으로 모든 요청과 응답을 받아왔을경우 put( )으로 dispatch를 실행시켜 action을 발동시키고 payload에 응답으로 받은 user 정보를 전달해줍니다.
- 이제 미들웨어가 종료되고 reducer로 넘어가 type이 USER_LOGIN.SUCCESS일 경우
me : { ...action.payload.user }로 현재 로그인한 user의 정보를 추가해줍니다.
만약 type이 USER_LOGIN.FAILURE이면 isLogin을 그대로 false로 두어 login이 처리되지 않도록하고 상태 error를 변경해주었습니다.