이번에 진행할 새로운 토이 프로젝트는 해외 유명 블라인드 데이트앱 tinder의 클론 서비스이다. 리액트와 Express, 몽고db 기반으로 제작중이며, 리덕스 사가를 이용한 상태관리와 socket i.o를 이용한 실시간 채팅 기능이 주 이기 때문에 tindux로 이름을 지었다.
우선 가장 기본이 되는 로그인, 회원가입 기능은 예전에 MERN스택으로 만들어둔 보일러 플레이트를 기반으로 구축중이며, 기존엔 리덕스 thunk를 이용하여 간단하게 상태관리를 했었지만, 이번 토이 프로젝트는 리덕스 사가를 사용하기로 했으므로 기존의 thunk 코드를 전부 사가로 바꿔줬다.
로그인 및 회원가입 시 상태관리를 담당하던 기존의 thunk코드이다. 상당히 심플하게 구성 했었었다.
우선 디렉토리 구조에 변화를 줬다. 기존엔 액션이 모여있는 '_action'와 리듀서들이 모여 있는 '_reducers' 폴더로 구성 했었지만, 이번엔 리덕스 액션과 짝을 이룰 api요청 로직이 모여있는 api폴더와, 사가 함수들이 모여있는 modules 폴더로 구성했다.
모든 파일들과 로직 관련 내용들을 이 글에서 모두 다룰 순 없으므로, 사가를 이용한 로그인 관련 상태 관리 부분만 정리해 보려한다.
import * as UserApi from "../api/user";
import { call, put, takeEvery } from "redux-saga/effects";
const LOGIN_USER = "user/LOGIN_USER";
const LOGIN_USER_SUCCESS = "user/LOGIN_USER_SUCCESS";
const LOGIN_USER_FAILURE = "user/LOGIN_USER_FAILURE";
export const loginUser = (formData) => ({
type: LOGIN_USER,
payload: formData,
});
export function* loginUserSaga(action) {
try {
const loginRusult = yield call(UserApi.loginUserAsync, action.payload);
yield put({
type: LOGIN_USER_SUCCESS,
payload: loginRusult,
});
} catch (e) {
console.log(e);
yield put({
type: LOGIN_USER_FAILURE,
payload: e,
});
}
}
export function* userSaga() {
yield takeEvery(LOGIN_USER, loginUserSaga);
}
export default function userReduce(state = {}, action) {
switch (action.type) {
case LOGIN_USER:
return {
...state,
};
case LOGIN_USER_SUCCESS:
return {
...state,
loginSuccess: action.payload.loginSuccess,
token: action.payload.token,
};
case LOGIN_USER_FAILURE:
return {
...state,
loginSuccess: false,
error: action.payload.message,
};
default:
return state;
}
}
먼저 로그인 시 유저가 폼데이터를 입력하고 submit을 하면 users.js의 'loginUser'액션이 디스패치되면서 LOGIN_USER라는 타입과 formData가 payload로 전달되는데, 감시자 역활을 하는 userSaga에 의해 loginUserSaga라는 사가 제네레이터 함수로 진입 하게된다.
import axios from 'axios';
export async function loginUserAsync(formData) {
const response = await axios.post('/api/users/login', formData);
if (!response.data.loginSuccess) {
throw new Error('해당 이메일이 존재하지 않습니다.');
}
return response.data;
}
사가의 비동기 effect인 call을 이용해서 api/users.js의 loginUserAsync을 호출한다.
'call' effect를 사용했기 때문에, loginUserAsync의 리턴값이 반환된 이후 loginRusult에 할당되며, try, catch구문을 이용해서 _SUCCESS, _FAILURE 여부를 판단한다. 그후 로그인 컴포넌트 단에서 useSelector를 이용해서 다음 작업을 처리한다.
기존 로직에 사가를 끼얹는(?)도중에 골치 아팠던 상황이 있었었는데, Header컴포넌트에서 리덕스 스토에의 값을 useSelector를 통해 접근하려하자 자꾸 undefined이 출력됬었다. 문제는 스토어의 모든 값들의 접근에 대해 undefined이 출력됬었던것이 아니라, 중첩되있는 객체에서만 undefined이 출력됬었다. 대략적으론 초기 랜더링 시 데이터가 담기기 전에 해당 데이터를 사용하려해서 undefined이 출력된거 같긴한데, ES11의 Optional Chaining 문법을 통해 해결했다.
ES11의 ?. 연산자는 왼쪽 연산자 값이 null이나 , undefined 일 경우 실행을 멈추고 undefined를 return하는 연산자이다. 해당 문법의 장점은 존재하지 않을 수도 있는 값에 대하여 예외처리를 손쉽게 할 수 있다고 한다.
확실히 리덕스 사가를 사용해보니, 코드의 양이 늘긴했지만, 에러 처리나, 전체적인 데이터 흐름을 디테일하게 확인할 수 있다는 점이 좋았다. 또한 익숙해지기만하면 가독성도 꽤 좋게 느껴질거같다. 또한 현재 로직은 로그인 이후에 리덕스 스토어의 특정 데이터값 여부에 따라 조건부 렌더링을 돌리다보니, db와 통신 시간에 의해 화면의 조건부 렌더링 부분이 순간적으로 깜빡이며 변하는 문제가 있는데, 해당 문제는 브라우저의 localstorage를 이용하여 개선해볼 예정이다.