드.디.어
4주프로젝트의 모달창을 리팩토링했다. 😆
아직 모달창을 띄우는 모든 컴포넌트를 리팩토링한건 아니구, 리덕스에 redux-saga
를 세팅하고 채팅창 나가기버튼과 회원탈퇴 컴포넌트에 적용해놨다.
아주 잘된다. 속이 시원하다.
리팩토링하면서 공부하고 배운 과정들을 기록해보겠다!!
🧐 리팩토링 왜 한거야?
리팩토링의 목적부터 먼저 말하면 아래와 같다.
모달창을 OPEN할 때 callback함수를 dispatch해서, 모달창의 예/아니오 버튼에 따라 다른 callback 함수를 실행시키자!!
사실, 프로젝트를 진행하면서도 위를 구현해야 했다. 그러나 당시에는 데드라인이 정해져있었기때문에 추가로 redux-middleware
를 공부해서 적용할 만한 시간이 없었다.
따라서 방법이 없을까 궁리하다가 아래와 같이 구현했다.
변경전 코드 (회원탈퇴)
withdrawal.tsx
dispatch(
openModal({
type: 'danger',
text: '정말 탈퇴하시겠습니까? 😢',
callbackName: 'kakaoWithdrawal',
callbackData: { config, withdrawalURL },
}),
);
modal.tsx
const okCallback = () => {
if (callbackName) {
// OK클릭시 실행할 콜백들 작성
if (callbackName === 'kakaoWithdrawal') {
kakaoWithdrawal();
}
...
}
const kakaoWithdrawal = () => {
const {config,withdrawalURL} = callbackData;
...
(회원탈퇴를 위한 코드)
...
}
미들웨어 없이는 action에 함수를 담아 보낼 수 없기때문에 callbackName
이라는 string과 callbackData
라는 callback함수를 실행시키는데 필요한 data를 객체로 담아 dispatch
했다.
이러한 방법은 어찌어찌 기능구현은 됐지만 아래와 같은 문제점들이 있었다.
1. 무거워진 modal 컴포넌트: 콜백함수들이 모두 modal.tsx
에 들어가면서 modal 컴포넌트가 굉장히 무거워졌다. 또한 콜백함수들이 콜백함수가 필요한 컴포넌트에 있는게 아니라 modal 컴포넌트에 모여서, 코드의 가독성 또한 해치게되었다.
2. 코드작성의 번거로움: dispatch
할 때마다 openModal
의 payload에 4~5가지의 값을 넣다보니 type을 설정하는 것도 굉장히 번거로웠고, modal component에 callbackName
별로 분기를 계속 해줘야했다.
3. 한계: 계속 사용하다보니 기능적으로도 한계가 있었다. callbackData
에 값을 명시적으로 넣을 수 없는, 예를 들어 상태와 관련된 함수들은 callback으로 넘길 수 없었다.
이러한 문제들로 모달창의 리팩토링은 반드시 필요했다!!!
😅 리팩토링 접근 과정
함수를 action에 담아 dispatch
해야 했기때문에 redux-middleware
를 사용해야 했다.
일단 해보면서 배우자는 생각으로 redux-toolkit
에 내장되어있는 redux-thunk
를 먼저 사용해서 해결해보려 했다.
redux-thunk와 redux-saga
redux-toolkit
의 createAsyncThunk
를 사용해서 dispatch된 함수를 실행시키고 이를 slice의 extraReducers
에서 pending
, fulfilled
, rejected
상태에 따라 다른 로직을 실행시킬 수 있음을 배웠다.
서버에 요청을 하는 경우 이러한 thunk
를 사용하면 Loading spinner를 띄우거나 에러핸들링을 하는데 굉장히 편리할 것 같다는 생각을 했다.
그러나 전달된 함수를 저장했다가 특정 action이 dispatch 되었을 때만 조건적으로 실행시키는 데에는 redux-thunk
보다는 redux-saga
가 적절하다는 것을 아래 내용으로 알 수 있었다.
thunk는 절대로 action에 응답을 줄수 없다.
반면 saga는 store를 구독하고 특정 작업이 디스패치될때 saga가 실행되도록 할 수 있다.
따라서 redux-saga
를 사용하기 위해 간단히 generator 문법에 대해 공부한 뒤 적용해보기 시작했다.
😎 리팩토링 결과
reducer (redux-toolkit사용)
1) reducer/modalSlice.ts
export const modalSlice = createSlice({
name: 'modal',
initialState,
reducers: {
openModal: (state, action: PayloadAction<OpenPayload>) => {
const newState = { ...action.payload, open: true };
return newState;
},
closeModal: () => {
return initialState;
},
// clickConfirm이 dispatch되면 openModal의 callback함수가 실행된다.
clickConfirm: () => {
return initialState;
},
},
});
export const { openModal, closeModal, clickConfirm } = modalSlice.actions;
export default modalSlice.reducer;
2) reducer/modalSaga.ts
import { takeLatest, race, take } from 'redux-saga/effects';
import { clickConfirm, openModal, closeModal } from './modalSlice';
export function* handleConfirm(action: ReturnType<typeof openModal>) {
const { confirm } = yield race({ confirm: take(clickConfirm), cancle: take(closeModal) });
if (confirm) {
action.payload.onConfirm?.();
} else {
action.payload.onCancle?.();
}
}
export default function* watchOpen() {
yield takeLatest(openModal.type, handleConfirm);
}
3) reducer/index.ts
... (생략) ...
const sagaMiddleware = createSagaMiddleware();
function* rootSaga() {
yield all([watchOpen()]);
}
const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware],
});
sagaMiddleware.run(rootSaga);
1) modalSlice.ts
clickConfirm
action을 추가해줬다.closeModal
action과 로직이 동일하지만 예/아니오에 따른 다른 함수가 실행되도록 action을 분리해줬다.
2) modalSaga.ts
handleConfirm
함수는clickConfirm
,closeModal
액션이 실행됐을 때, 값을confirm
,cancle
변수에 할당하고 (cancle변수는 else로 처리하면돼서 구조분해할당에서 생략함) 이에따라 payload에서 받은onConfirm
또는onCancle
함수를 실행시키는 함수다.watchOpen
함수는openModal
action이 들어오면 위의handleConfirm
을 실행시킨다.- 위에서 쓰인
redux-saga
의 effects들을 간략히 설명하면 아래와 같다.takeLatest: 가장 마지막에 실행된 액션에 대해서만 핸들러를 실행한다. 실행된 모든 액션에대해서 핸들러를 실행시키고 싶다면 takeEvery effects를 사용하면 된다.
race: effects들을 마치 경주하듯이 동시에 실행시켜놓고 먼저 완료되는 effect가 있으면 다른 effects들을 종료시킨다.
take: 매개변수로 전달된 액션이 올때까지 블락된 상태로 기다린다.
3) index.ts
redux-toolkit
을 사용하고 있었기때문에 store에 미들웨어로sagaMiddleware
를 추가함으로써 간단히 세팅을 마쳤다.
Components
1) withdrawal.tsx
dispatch(
openModal({
type: 'danger',
text: '정말 탈퇴하시겠습니까? 😢',
onConfirm: confirmCallback,
}),
);
2) modal.tsx
const okCallback = () => dispatch(clickConfirm());
const cancleCallback = () => dispatch(closeModal());
1) withdrawal.tsx
- 이제 해당 컴포넌트에서 callback함수를 직접 payload로 담을 수 있다!!
예/아니오 버튼을 눌렀을 때, onConfirm/onCancle함수가 실행되는 것이다.👍
2) madal.tsx
- modal 컴포넌트의 이벤트핸들러가 위의 두줄이 끝이다..!
기존에는 온갖 if문으로 callbackName에 따라 분기하고, callback을 직접 modal 컴포넌트 안에서 선언해줬어야 했는데 말이다.
너무 이쁘다.😍
🤗 리팩토링 후기
뿌듯하다.
만약 "redux-middleware
에 대해 공부합시다." 라는 생각으로 redux-middleware
를 공부했다면 꽤나 지루한 시간이 되었을 지도 모르겠다.
그러나 내가 그 필요성을 느끼고 문제를 해결하기위해 부딪히면서 공부하는 과정은 정말 재밌는 것 같다.
이걸로 redux-thunk
와 redux-saga
에 대해서 충분히 공부했다고는 절대 말 못하겠지만, 이 둘을 처음 공부하는 시작으로는 꽤나 괜찮은 시작이었다는 생각이 들었다.
나머지 컴포넌트들의 모달창들도 하나하나 수정해나가야겠다. 끝!
🙏 참고
'개인공부 > TIL(Today I Learned)' 카테고리의 다른 글
saga 쓰기 전에 ES6 Generator 공부하기 (0) | 2021.07.07 |
---|---|
TIL 93일차_Mongoose 사용하기 (0) | 2021.04.21 |
TIL 92일차_Null Check (0) | 2021.04.20 |
TIL 91일차_axios에서의 오류처리 (0) | 2021.04.19 |
TIL 90일차_jwt 사용하기 (0) | 2021.04.18 |