우선 여기에 쓰인것처럼 파어어베이스 프로젝트를 연동해줬다.
그 후 redux-thunk를 사용해 비동기로 불러왔다.
보통 데이터베이스를 설계는 테이블 사이의 관계를 설정해주는걸 말한다.
근데 파이어베이스의 파이어스토어는 NoSQL 데이터베이스다.
테이블이 아니라 JSON 혹은 트리형식으로 데이터를 관리하고 비정규화를 통해 데이터를 모델링한다.
즉, 사용자의 정보와 같은 데이터들이 필요한 모든곳에 들어가있다고 보면 된다.
SQL에서 는 비정규화를 통해 이런 중복들을 최대한 줄이는데 왜 NoSQL은 이런방식을 쓸까?
장점이 있기 때문이다. 이런식으로 작동하면 쿼리 수행을 위한 I/O가 줄어든다.
이로인해 성능이 매우 향상되지만 데이터 사이즈가 늘어나는게 단점이다.
이번 프로젝트는 아주 간단하기 때문에 구조는 아래와 같이 설계했다.
receipts: {
id?: number;
category: string; // 카테고리
transactionBranch: string; //거래처
timeDate: TDateTime;
income?: number; // 수입
spending?: number; //지출
memo?: string; // 메모
paymentMethod?: string; // 지불방법
depositMethod?: string; //입금방법
isExcept?: boolean; // 제외여부
}
users: {
budget: number;
character: number;
email: string;
name: string;
uid: string
}
redux-toolkit의 createAsyncTunk를 사용해 redux-thunk를 간단하게 만들었다.
// asyncCreateReceipt.ts
const asyncCreateReceipt = createAsyncThunk(
'ReceiptSlice/asyncCreateReceipt',
async (receipt: TReceipt, api) => {
const firstDate = (api.getState() as RootState).accountBook.firstDate;
const email = (api.getState() as RootState).user.email;
const uid = (api.getState() as RootState).user.uid;
const isLogin = (api.getState() as RootState).user.isLogin;
// 로그인된 상태가 아닌데 생성하려고 시도하는경우
if (!isLogin) {
api.rejectWithValue({ status: 400, data: '로그인이 필요합니다.' });
}
// 로그인된 상태라면
const date = getDate(receipt.timeDate);
const timeStamp = Timestamp.now().seconds;
const data = {
...receipt,
uid: uid,
id: timeStamp,
};
try {
// 데이터를 씀
await setDoc(doc(db, 'receipts', timeStamp.toString()), data);
const response = await getDoc(doc(db, 'receipts', timeStamp.toString()));
if (response.exists()) {
// 방금 추가한게 이전 처음 날짜보다 앞선날자면 첫날짜를 업데이트해줌
if (getDate(firstDate) > getDate(receipt.timeDate)) {
const docuFirstDate = doc(db, email, 'date');
// 서버값도 업데이트
setDoc(docuFirstDate, {
firstDate: date,
});
// 클라이언트값도 업데이트
api.dispatch(changeFirstDateAction(receipt.timeDate));
}
return api.fulfillWithValue({ status: 201, data });
}
} catch (e) {
return api.rejectWithValue({ status: 400, data: '생성 실패' });
}
},
);
const asyncCreateReceiptPending: CaseReducer = (state, action) => {
state.loadingState.create.loading = true;
state.loadingState.create.success = false;
state.loadingState.create.error = false;
state.loadingState.create.errorMsg = null;
};
const asyncCreateReceiptFulfilled: CaseReducer = (state, action) => {
state.loadingState.create.loading = false;
state.loadingState.create.success = true;
state.receipts.push(action.payload.data);
};
const asyncCreateReceiptRejected: CaseReducer = (state, action) => {
state.loadingState.create.loading = false;
state.loadingState.create.error = true;
state.loadingState.create.errorMsg = action.payload.data;
};
export default asyncCreateReceipt;
export {
asyncCreateReceiptPending,
asyncCreateReceiptFulfilled,
asyncCreateReceiptRejected,
};
asyncCreateReceipt
액션을 디스패치 하면 진행 경과에 따라 그 다음 액션함수가 디스패치된다.
진행도중: asyncCreateReceiptPending
성공: asyncCreateReceiptFulfilled
실패: asyncCreateReceiptRejected
이렇게 작성한 redux-thunk는 reducer안의 extraReducers
에 이런식으로 넣어줬다..
const accountBookSlice = createSlice({
name: 'accountBookSlice',
initialState: initialAccountBookState,
extraReducers(builder) {
builder.addCase(asyncCreateReceipt.pending, asyncCreateReceiptPending);
builder.addCase(asyncCreateReceipt.fulfilled, asyncCreateReceiptFulfilled);
builder.addCase(asyncCreateReceipt.rejected, asyncCreateReceiptRejected);
}
});
receipt 관련해선 삭제, 로딩, 업데이트도 있는데 모두 이와같은 방식으로 작성해줬다.
해당 소스코드는 아래에서 확인할 수 있다.
1. asyncDeleteReceipt.ts
2. asyncReadReceipt.ts
3. asyncUpdateReceipt.ts
4. accountBookReducer
유저도 receipt의 틀에서 크게 벗어나지 않았다.
만약 성공하면 Fulfilled 함수를 디스패치 해주는 방식이다.
실패하면 reject를!
export interface TUserIdPassword {
email: string;
password: string;
}
const asyncCreateUser = createAsyncThunk(
'userSlice/asyncCreateUser',
async (user: TUserIdPassword, api) => {
const isDuplicate = await checkDuplicateId(user.email);
const { isLogin } = (api.getState() as RootState).user;
if (isDuplicate) {
return api.rejectWithValue({
status: 409,
data: '중복되는 이메일입니다.',
});
} else if (isLogin) {
return api.rejectWithValue({
stats: 400,
data: '이미 로그인 되어있습니다.',
});
}
await setPersistence(auth, browserLocalPersistence);
const res = await createUserWithEmailAndPassword(
auth,
user.email,
user.password,
);
// 계정 생성에 성공하면
return res.user
? {
status: 200,
data: {
uid: res.user.uid,
email: res.user.email,
},
}
: {
status: 409,
data: '중복되는 메일입니다.',
};
},
);
const asyncCreateUserPending: CaseReducer = (state, action) => {
state.loadingState.createUser.loading = true;
state.loadingState.createUser.success = false;
state.loadingState.createUser.error = false;
state.loadingState.createUser.errorMsg = null;
};
const asyncCreateUserFulfilled: CaseReducer = (state, action) => {
state.loadingState.createUser.loading = false;
state.loadingState.createUser.success = true;
state.email = action.payload.data.email as string;
state.uid = action.payload.data.uid as string;
state.isLogin = true;
};
const asyncCreateUserRejected: CaseReducer = (state, action) => {
state.loadingState.createUser.loading = false;
state.loadingState.createUser.error = true;
state.loadingState.createUser.errorMsg = action.payload.data;
};
export default asyncCreateUser;
export {
asyncCreateUserPending,
asyncCreateUserFulfilled,
asyncCreateUserRejected,
};
유저 관련해서 함수들도 네가지 종류가 더있다.
1. asyncLoginUser.ts
2. asyncLogoutUser.ts
3. asyncReadUserInfo.ts
4. asyncUpdateUserInfo.ts
5. userReducer
우선 user다 user의 경우 로그인 할시 그 값들을 store
에 저장해줬다.
현재 백엔드 서버가 없어서 JWT인증은 사용하지 않았고 그냥 바로 값들을 저장해줬다.
그리고 자동로그인 구현을 위해 로그인 정보가 있다면 값을 불러오고, 로그인 상태를 유지해줬다.
receipt 관련된 함수중 read함수도 이때 사용한다.
이번 프로젝트는 한달이라는 긴 시간동안 진행했는데 이렇게 긴 시간이 필요한건지 모르겠다.
아마 기간을 타이트하게 잡으면 2주란 시간안에도 충분히 가능할거라고 생각은 한다.
redux-thunk도 저렇게 쓰는게 맞는지 더 찾아봐야 할것같다.
그리고 지난 1주일 정도는 정말 일정이 비질 않아서 코딩에 충분한 시간을 할애하지 못했는데
앞으로 시간이 많이 남지 않았으니 더 열심히 해봐야겠다.
처음에는 블로그에 글을 2개씩도 써갔는데 점점 귀찮아져서 점점점 쓰는 속도가 늦어진다.
다시 마음을 가다듬고 초심으로 돌아가 열심히 공부해보자!
아 마지막으로 지금까지 완성된 부분을 올리괴 마친다.