현재 각 페이지 별 주된 API 호출에 대한 비동기 상태를 redux로 관리하고 있습니다. 이렇게 진행한 이유는, 각 페이지에서 의존하고 있는 thunk가 공통되기 때문입니다. 주문서 페이지를 살펴보겠습니다.
export const deliveryAddressSlice = createSlice({
name: 'deliveryAddress',
// ...
extraReducers: (builder) => {
builder.addCase(fetchOrderSheet.fulfilled, (state, action: PayloadAction<OrderSheetResponse>) => {
// ...
})
},
})
export const orderPayMethodSlice = createSlice({
name: 'orderPayMethod',
// ...
extraReducers: (builder) => {
builder.addCase(fetchOrderSheet.fulfilled, (_, action: PayloadAction<OrderSheetResponse>) => {
// ...
})
},
})
export const orderProductSlice = createSlice({
name: 'orderProductSlice',
// ...
extraReducers: (builder) => {
builder.addCase(fetchOrderSheet.fulfilled, (state, action: PayloadAction<OrderSheetResponse>) => {
// ...
})
},
})
export const subscriptionDateSlice = createSlice({
name: 'subscriptionDate',
// ...
extraReducers: (builder) => {
builder.addCase(fetchOrderSheet.fulfilled, (_, action: PayloadAction<OrderSheetResponse>) => {
// ...
})
},
})
모두 동일한 thunk(fetchOrderSheet
)를 참조하여 이에 대한 액션으로 값을 초기화하고 있습니다. 제가 생각한 비동기 상태를 관리하기 위한 방법은 2가지입니다.
1번의 경우 slice들에 fetchOrderSheet
에 의존하는 상태들이 중복되어 포함됩니다. 따라서 이들을 중앙집중식으로 관리하고자 2번 방식을 적용했습니다.
이 3개의 함수들은 내부적으로 isAnyOf를 호출하여 asyncThunk의 상태(pending, fulfilled, rejected)에 해당하는 match함수를 반환합니다.
export function isPending<AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]>(
...asyncThunks: AsyncThunks | [any]
) {
if (asyncThunks.length === 0) {
return (action: any) => hasExpectedRequestMetadata(action, ['pending'])
}
if (!isAsyncThunkArray(asyncThunks)) {
return isPending()(asyncThunks[0])
}
return isAnyOf(...asyncThunks.map((asyncThunk) => asyncThunk.pending))
}
이를 이용하면 주문서 페이지, 결제수단 변경 페이지 각각에 대한 전역 비동기 상태를 관리할 수 있습니다.
export const asyncStatusSlice = createSlice({
name: 'asyncStatusSlice',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(isPending(fetchOrderSheet, fetchPaymentMethod), (state) => {
state.loading = true
state.error = null
})
// ...
현재 주문서 페이지에서는 2개의 API 요청(주문서 정보, 포인트 혜택 정보)이 진행되며, 이에 대한 상태를 관리해야 합니다. 만약, 해당 asyncThunk도 위에 정의한 전역 비동기 상태에 추가하게 된다면, 로직 상 오류가 발생합니다.
// fetchPointsReward도 추가하게 되면 tricky한 문제 발생!
builder.addMatcher(isFulfilled(fetchOrderSheet, fetchPaymentMethod, fetchPointsReward), (state) => {
state.loading = true
state.error = null
})
예를들어 포인트 혜택 정보 API가 여전히 pending일 때, 주문서 정보 API가 먼저 fulfilled 된다면, loading이 false가 되고 실제 데이터를 보여주게 됩니다. 그러나, 포인트 혜택 정보는 아직 불러오고 있기에 보여줄 정보가 없습니다.
이러한 문제를 해결하기 위해 포인트 혜택 정보의 비동기 상태를 별도로 관리하고자 했습니다.
export const pointsRewardSlice = createSlice({
name: 'pointsReward',
// ...
extraReducers: (builder) => {
builder.addCase(postPointsReward.pending, (state) => {
state.loading = true
state.error = null
})
// ...
builder.addCase(postPointsReward.rejected, (state, action) => {
state.loading = false
state.error = action.payload ?? CustomError.unknownError
})
},
})