공식 문서를 돌같이 보는 버릇을 고치자!
[RTK] 공식 문서만 보고 Redux Toolkit 적용해 보기(1)에 이어 작성하는 글이다.
todo
의 로직도 auth
와 큰 차이는 없다. 다만, CRUD 요청이 모두 비동기로 발생해 createAsyncThunk
를 4번 사용했다. extraReducers
에서도 콜백 함수의 builder
를 4번 호출했다. .addCase
로 연결하여 호출할 수도 있지만, 개인적으로 가독성이 떨어져 api 호출별로 builder.addCase
를 나눠 작성했다.
extraReducers: (builder) => {
// get todo list
builder
.addCase(fetchTodoList.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchTodoList.fulfilled, (state, action) => {
state.isLoading = false;
state.todoList = action.payload;
})
.addCase(fetchTodoList.rejected, (state) => {
state.isLoading = false;
});
// create todo
builder
.addCase(fetchCreateTodo.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchCreateTodo.fulfilled, (state, action) => {
state.isLoading = false;
state.todoList.todos = [
...state.todoList.todos,
action.payload.newToDoData,
];
})
.addCase(fetchCreateTodo.rejected, (state) => {
state.isLoading = false;
});
// update todo
builder
.addCase(fetchUpdateTodo.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchUpdateTodo.fulfilled, (state, action) => {
state.isLoading = false;
state.todoList.todos = state.todoList.todos
.map((todo) => todo.id === action.payload.updateTodo.id
? action.payload.updateTodo
: todo
);
})
.addCase(fetchUpdateTodo.rejected, (state) => {
state.isLoading = false;
});
// delete todo
builder
.addCase(fetchDeleteTodo.pending, (state) => {
state.isLoading = true;
})
.addCase(fetchDeleteTodo.fulfilled, (state, action) => {
state.isLoading = false;
state.todoList.todos = state.todoList.todos.filter(
(todo) => todo.id !== action.payload.id
);
})
.addCase(fetchDeleteTodo.rejected, (state) => {
state.isLoading = false;
});
},
주석으로 구분하긴 했지만, 그래도 보기가 좋지 않았다. 호출하는 함수만 다른 같은 로직이 수두룩 빽빽했기 때문이다. 이걸 어떻게 해결해야 하나 진짜 오래 고민했다. 그러다 참지 못하고 검색 찬스를 사용하고 말았다...!
RTK extrareducers builder combine
을 검색하니 가장 위에 나와 비슷한 고민을 한 사람의 스택오버플로 질문이 있었다. 이 사람도 에러 로직이 같은데 어떻게 DRY하게 만드냐고 질문했다. 그 중 addMatcher
를 사용한 답변을 참고했다.
공식 문서를 살펴 보니 addMatcher
는 들어오는 작업을 action.type 속성 대신 자체 필터 함수와 일치시킬 수 있는 메서드이다.(역시 공식 문서는 답을 알고 있다, 두둥!)
첫 번째 인자로 matcher
함수를 받고, 두 번째 인자로 reducer
를 받는다.
내게 필요한 과정은 pending
일 때 isLoading = true
로, fulfilled
or rejected
일 때는 isLoading = false
로 변화시키는 것이다.
enum AsyncThunkTypes {
pending = "pending",
fulfilled = "fulfilled",
rejected = "rejected",
}
const todoReducer = createSlice({
// (...)
extraReducers: (builder) => {
// ...builder.addCase(...)
builder
.addMatcher(
(action: PayloadAction) => action.type.endsWith(AsyncThunkTypes.pending),
(state) => {
state.isLoading = true;
}
)
.addMatcher(
(action: PayloadAction) =>
action.type.endsWith(AsyncThunkTypes.fulfilled || AsyncThunkTypes.rejected),
(state) => {
state.isLoading = false;
}
);
},
}
pending
일 때의 addMatcher
와 fulfilled
또는 rejected
일 때의 addMatcher
로 나눠 적용했다. 어쨌든 반복되는 부분을 줄여 만족했다.
6) 커스텀 훅 내용 대체
auth
때와 마찬가지로 todo
도 useTodoList
커스텀 훅의 내용을 수정했다.
function useTodoList() {
const { createTodo, getTodos, updateTodo, deleteTodo } = useToDoContext();
const [todoList, setTodoList] = useState<ResponseToDoType[]>([]);
const { state, loading, onFetching } = useFetch(getTodos);
useEffect(() => {
if (state?.ok) {
setTodoList(state.todos);
}
}, [loading, state]);
return { ... }
}
상태 관리와 별개로 useFetch
커스텀 훅으로 받아온 데이터를 다시 useState
에 저장하는 쓸데없는 코드와 context
를 제거했다.
function useTodoList() {
const {
todoList: { todos: todoList },
isLoading: loading,
} = useAppSelector(selectTodoState);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchTodoList());
}, [dispatch]);
return { ... }
}
Redux
를 적용하고자 공식 문서를 보면서 열심히 달렸다. 충격적인 사실은 내가 본 것은 Redux
공식 문서였고, 내가 사용한 Redux Toolkit
의 공식 문서는 따로 있었다는 사실이다! 어쩐지 API를 아무리 봐도 스펙 설명이 없더라니... 조금 더 꼼꼼히 살펴야겠다. - Redux Toolkit DocsRTK
를 처음 사용해봤지만, 개발하는데 필요한 정보는 공식 문서에 다 있었다. 평소 라이브러리를 사용할 때 강의나 서적으로 접해 써 보고, 에러가 발생하면 구글링부터 했다. 그러나 이번에 공식 문서만 보면서 강의 및 구글링의 유혹을 떨쳐냈고, 목표한 바를 구현했다. 중간에 참지 못하고 검색했지만, 그 문제의 해결책 역시 이미 공식 문서에 있었다. 앞으로는 공식 문서로 먼저 공부해야겠다.reducer
만 건들면 되니까. 적절한 라이브러리의 사용은 삶의 질을 향상시키는 것 같다.