React - Redux-saga ( Generator , 주요 함수 , 로그인 기능구현 )

dev_swan·2022년 5월 10일

React

목록 보기
1/15
post-thumbnail

Redux-saga 배우기 전 - Generator 개념

  • 정의

함수를 실행할 때 특정구간에 멈춰두거나, 원하는 시점에 다시 돌아가게 할 수도 있고, 결과값을 여러번 받아올 수도 있습니다.

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는 함수의 코드가 모두 종료되었음을 의미합니다.


Redux-Saga 주요 함수 ( 출처 )

함수명내용예시해석
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

  • 정의
  • Redux-saga는 Action을 모니터링하고 있다가, 특정 Action이 발생하면 이에 따라 특정 작업을 하는 방식입니다.
  • 특정 Action이 발생했을 때 이에 따라 다른 Action을 Dispatch 시키거나 Javascript 코드를 실행 할 수 있습니다.

Redux-saga 로그인 기능 구현해보기 ( localStorage )

Back-end

  • login
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,
	});
});

프론트 서버에서 요청이 오면 로그인 기능을 처리할 로직입니다.


front-end

  • useStore.jsx
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를 실행시켜줍니다.

  • useForm.jsx ( Custom Hook )
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;
  1. 기본적인 상태를 생성하고 onChange 이벤트가 발생될 때마다 상태를 바꿔줄 수 있도록 onChange 함수를 만들어주었습니다.
  2. onSubmit 이벤트가 발생될 때 Submit을 true로 바꾸어주고 errors를 validate( )로 이메일 형식과 비밀번호를 검사한 결과값을 담아줍니다.
  3. errors가 변경될 때마다 init 함수가 실행되어 Submit이 true이고 errors에 아무것도 없는, 즉 폼체크에서 에러가 나지 않았을때 onSumbit함수를 실행시키고 Submit을 false로 바꿔줍니다.
  4. defaultValue의 key값을 가져와서 reduce( )로 객체형태로 변환시킬것이고 객체의 key값은 Object.keys(defaultValue)의 리턴값이 차례대로 들어가고 value값은 해당 key값에 해당하는 상태 value를 넣어줍니다.

  • Login.jsx
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입니다.


  • index.js ( /sagas )
export default function* rootSaga() {
	yield all([watchCounterUp(), watchList(), userSaga()]);
}

모든 saga들이 action을 watching 해야하기 때문에 전체 관리할 rootSaga를 만들어줍니다.
같은 디렉토리에서 댓글과 Counter를 테스트해보았어서 사용하였습니다.


  • userSaga.js
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 함수를 실행합니다.


  • index.js ( /reducer )
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에 담아서 사용하였습니다.


  • user.js
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의 로직입니다. 전역에서 관리할 초기값 또한 이곳에서 작성해주었습니다.


  • 로그인 기능 처리 순서
  1. Login.jsx에서 input에 회원가입한 email과 해당 아이디의 패스워드를 입력하고 폼체크 부분에서 에러가 없을경우 onSubmit을 통하여 action / user_login_request가 발동되고 인자값으로 payload를 받습니다.
  1. userSaga.js에서 user_login_request.toString() action을 캐치하여 takeLatest로 마지막 실행된 작업만 수행하게 하여 로그인 버튼을 많이 클릭하더라도 마지막 1번만 실행되도록 설정해주었고 , 2번째 인자값에 login 함수가 실행됩니다.
  1. login 함수가 실행되면 제일 먼저 call( )함수가 실행되어 loginAPI 함수를 실행시키고, 실행시킬 함수에 인자값으로 action.payload를 전달해줍니다.
  1. loginAPI 함수가 실행되면 axios로 요청을 보내는데 이때 body부분에 입력한 input의 내용인 email과 password가 담겨있는 payload를 같이 보내주고 응답으로 access_token을 받고 받은 토큰으로 다시 user/me에 요청을 보내는데 이때는 option부분에 header에 token을 Authorization: `Bearer ${token} 이런식으로 같이 요청을 보냅니다. 백서버에서 응답으로 보낸 user에 대한 정보를 repsonse로 받고 loginAPI함수는 종료됩니다.
  1. 다시 login 함수로 와서 성공적으로 모든 요청과 응답을 받아왔을경우 put( )으로 dispatch를 실행시켜 action을 발동시키고 payload에 응답으로 받은 user 정보를 전달해줍니다.
  1. 이제 미들웨어가 종료되고 reducer로 넘어가 type이 USER_LOGIN.SUCCESS일 경우
    me : { ...action.payload.user }로 현재 로그인한 user의 정보를 추가해줍니다.
    만약 type이 USER_LOGIN.FAILURE이면 isLogin을 그대로 false로 두어 login이 처리되지 않도록하고 상태 error를 변경해주었습니다.

0개의 댓글