프로젝트에서 RTK 쿼리로 비동기 처리를 할까? 고민도 했지만, 이미 대부분의 비동기 처리를 createAsyncThunk를 통해 해결하고 있었기에, 우선은 createAsyncThunk의 사용법과 개념을 확실히 가지고 RTK 쿼리로 넘어가기로 했다.
redux toolkit에서는 createAsyncThunk
와 createSlice
를 사용해 비동기 처리를 할 수 있다.
일반적으로 react의 컴포넌트 내에서 API를 받는 비동기 처리를 하려면 다음과 같은 코드를 작성해 주어야 했다.
try {
const response = await axios.post("http://localhost:8090/v1/login", loginData);
console.log(response)
} catch (e) {
console.log(e.response);
}
다행이도 createAsyncThunk
를 사용한 redux toolkit에서의 비동기 처리도 이와 크게 다르지 않다. 오히려 더 단순화 시켰다.
export const login = createAsyncThunk(
"auth/login",
async (loginData) => {
const response = await axios.post(`${API_URL}/v1/login`, loginData);
return response.data.data;
},
);
굳이 try-catch 문을 사용하지 않아도 된다. createAsyncThunk
는 결과에 상관없이 항상 이행된 프로미스를 반환하기 때문이다.
비동기 처리를 할 때, 대표적으로 사용되는 3가지 상태가 있다.
pending
: 비동기 호출 이전fulfilled
: 비동기 호출 성공rejected
: 비동기 호출 실패createSlice의 extraReducres를 통해 이런 비동기 상태를 쉽게 제어할 수 있다.
const slice = createSlice({
name: "auth",
initialState,
reducers: {
logout: (state, action) => {},
},
extraReducers: builder => {
builder.addCase(login.pending, (state) =>{
state.status = "loading";
})
builder.addCase(login.fulfilled, (state, { payload }) => {
state.status = "success";
state.isAuth = true;
state.user = payload;
});
builder.addCase(login.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload;
});
},
});
앞서 createAsyncThunk
로 만들었던 login 비동기 처리를 3가지 경우(상태)에 따라 처리한 모습이다.
login.pending
일 때, status를 "loading"으로 변환
login.fullfilled
일 때, status를 "success"로 변환하고, isAuth = true, user에는 response.data.data 값을 넣는다.
login.rejected
일 때, status를 "failed"로 변환하고, error에 action.error(오류 내역)을 넣는다.
createAsyncThunk
는 항상 이행된 프로미스만 반환하기에 비동기 처리가 실패했을 경우, 서버에서 이에 대한 오류 내역을 보내줘도, 해당 내역을 사용하고 싶다면 따로 처리를 해줘야한다.
따라서 이를 해결하기 위해서는 createAsyncThunk내에서 오류를 발견하고, 이를 반환해야 한다.
오류를 반환하기 위해선 rejectWithValue
를 사용해야 한다.
export const login = createAsyncThunk(
"auth/login",
async (loginData, { rejectWithValue }) => {
try{
const response = await axios.post(`${API_URL}/v1/login`, loginData);
return response.data.data;
}catch(err){
let error = err;
if(!error.response){
throw err;
}
return rejectWithValue(error.response.data);
}
}
);
아쉽게도 이런 오류 처리를 하는 경우 이전보다 코드량이 늘어난다.
try-catch문을 사용해서 오류를 잡아줘야 하는데,
만약 오류가 없다면 try 블럭의 return이 slice에서의 action.payload가 된다.
오류가 발생한다면, catch 블럭을 통해 오류가 보내지게 되는데,
만약 error.response가 존재하지 않는다면, throw err
를 통해 re-throw한다. 즉, 함수의 실행이 중지된다. (throw 이후의 명령문은 실행되지 않음.)
catch문 안에서의 throw 동작 과정
throw
insidecatch block
just re-throws the error further
so if the call tologin
is wrapped with try..catch then the re-throw will be caught there
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw#rethrow_an_exception
by @ScriptyChris
이렇게 됐을 때, 에러는 rejectWithValue의 파라미터 값으로 보내지게 되고, 이를 return하여 에러를 발생시킨다.
slice에서 오류는 다음과 같이 2가지 경우로 받아진다.
builder.addCase(login.rejected)
에서 action.payload가 존재한다는 뜻은 서버에서 보내준 에러 메세지를 받았다는 뜻이므로, 이를 state.error에 저장시킨다.
반대로 action.payload가 존재하지 않는다는 것은 서버에서 따로 에러처리를 해준게 없다는 뜻이기에, 기존에 처리하던 방식대로, action.error를 가져와 사용하게 된다.
extraReducers: builder => {
// The `builder` callback form is used here because it provides correctly typed reducers from the action creators
builder.addCase(login.fulfilled, (state, { payload }) => {
state.isAuth = true;
state.user = payload;
});
builder.addCase(login.rejected, (state, action) => {
if (action.payload) {
state.error = action.payload;
} else {
state.error = action.error.message;
}
}
);
따라서 전체 코드는 다음과 같다.
export const login = createAsyncThunk(
"auth/login",
async (loginData, { rejectWithValue }) => {
try{
const response = await axios.post(`${API_URL}/v1/login`, loginData);
return response.data.data;
}catch(err){
let error = err;
if(!error.response){
throw err;
}
return rejectWithValue(error.response.data);
}
}
);
const slice = createSlice({
name: "auth",
initialState,
reducers: {
logout: (state, action) => {},
},
extraReducers: builder => {
builder.addCase(login.pending, (state) =>{
state.status = "loading";
})
builder.addCase(login.fulfilled, (state, { payload }) => {
state.status = "success";
state.isAuth = true;
state.user = payload;
});
builder.addCase(login.rejected, (state, action) => {
state.status = "failed";
if (action.payload) {
state.error = action.payload;
} else {
state.error = action.error.message;
}
});
},
});
비동기 상태 관리는 확실하게 할 수 있지만, redux에서 과도하게 비동기 상태를 저장하는 문제로 인해 요새는 redux-toolkit에서 비동기 처리를 할 때, createAsyncThunk
보다는 RTK-query
를 선호한다고 한다.