개발일지

[Typescript + Redux-Toolkit] createAsyncThunk에서의 에러 핸들링 본문

개발일지

[Typescript + Redux-Toolkit] createAsyncThunk에서의 에러 핸들링

Seobe95 2022. 11. 4. 01:31
redux-toolkit이나 createSlice, createAsyncThunk 등을 설명하지 않습니다! (쪼금은 있을지도?)

 

사이드 프로젝트를 진행하면서, 리액트를 다루는 기술에서의 마지막 프로젝트인 블로그 프로젝트를 Typescript, emotion, redux-toolkit을 이용하여 책과는 조금 다르게 프로젝트를 만들어보고 있다. 아무래도 책과는 다른 환경에서 진행하다보니 조금 더디게 진행되고 있는데, 문제가 되는 상황은 createAsyncThunk를 이용하여 api처리를 할 때, 어떻게 구체적인 에러 핸들링을 해야하는지, 핸들링을 하면서 어떻게 타이핑을 해야하는지 해결하는데 조금 시간이 걸리게 되었고, 타이핑에서의 오류가 많이 발생하게 되었다.

 

에러 핸들링이 필요했던 이유

기본적으로 createAsyncThunk를 사용하여 api 비동기 요청을 할 때 기본적으로 rejected에 걸려 에러를 확인할 수 있지만, 내가 원하는 값들만 redux에 저장 후, 그 값에 따라 화면에 표시를 해주려는 상황이었는데, rejected에 나온 값들은 에러에 대한 status가 message 안에 담겨있었고, 이렇게 되면 문자열 안의 401, 409 등의 에러 status를 찾아내는 함수를 만들어 찾아내야 하는데, 이렇게 하는 것 보다는 애러를 내가 받고 싶은 사항들만 받으려고 했다.

 

createAsyncThunk에서 제공하는 rejectWithValue라는 thunkoption을 사용할 때, rejected가 되었을 경우 action의 error값으로 api요청에 대한 에러가 들어가는 것이 아니라, action의 payload값으로 에러가 들어가게 된다.

그래서 이번 포스트는 createAsyncThunk에서의 에러핸들링 및 이를 타이핑 하는 것에 대한 내용이다.

내가 작성한 코드는 다음과 같다.

...

export interface UserFetchReults {
  _id: string | null;
  username: string | null;
}

export interface UserInput {
  username: string;
  password: string;
  passwordConfirm?: string;
}

export interface AxiosResponseError {
  data: string;
  status: number;
  statusText: string;
}

export const fetchUserLogin = createAsyncThunk<
  UserFetchReults,
  UserInput,
  { rejectValue: AxiosResponseError }
>('user/LOGIN', async (user, thunkOption) => {
  const { username, password } = user;
  const { rejectWithValue } = thunkOption;
  try {
    const response = await login({ username, password });
    return response;
  } catch (e) {
    if (axios.isAxiosError(e) && e.response) {
      return rejectWithValue({
        data: e.response.data,
        status: e.response.status,
        statusText: e.response.statusText,
      });
    } else {
      throw new Error('에러가 발생했습니다.');
    }
  }
});

...

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
  	...
  },
  extraReducers: (builder) => {
    builder
      ...
      .addCase(fetchUserLogin.rejected, (state, action) => {
        if (state.loading === 'pending' && action.payload !== undefined) {
          state.loading = 'idle';
          console.log(action.error);
          state.error = action.payload;
        }
      });
      ...
  }
});

하나 하나 뜯어보자면, 아래의 코드는 사용자가 입력한 것들에 대한 타입과 api 성공시 return 값에 대한 타입, error가 발생했을 경우의 타입이다.

export interface UserInput {
  username: string;
  password: string;
  passwordConfirm?: string;
}

export interface UserFetchReults {
  _id: string | null;
  username: string | null;
}

export interface AxiosResponseError {
  data: string;
  status: number;
  statusText: string;
}

 

 

다음 코드는 createAsyncThunk를 활용한 비동기 api 요청에 사용하는 코드이다.

export const fetchUserLogin = createAsyncThunk<
  UserFetchReults,
  UserInput,
  { rejectValue: AxiosResponseError }
>('user/LOGIN', async (user, thunkOption) => {
  const { username, password } = user;
  const { rejectWithValue } = thunkOption;
  try {
    const response = await login({ username, password });
    return response.data;
  } catch (e) {
    if (axios.isAxiosError(e) && e.response) {
      return rejectWithValue({
        data: e.response.data,
        status: e.response.status,
        statusText: e.response.statusText,
      });
    } else {
      throw new Error('에러가 발생했습니다.');
    }
  }
});

여기서 createAsyncThunk의 제네릭의 3번째 인자인 rejectValue는, 위에서 언급한 rejectWithValue의 인자로 사용될 내용에 대한 타입이다.

 

여기서 내가 사용한 방법은 try catch문을 사용하여 비동기 요청이 실패한 경우를 구분했고, 이 에러에 대한 타이핑을 위해 axios.isAxiosError 메서드를 활용하여 해당 에러가 axios에서의 에러인지(api 요청 중)와 error의 response객체가 있는지를 확인하여 내가 필요한 data, status, statusText를 뽑아낼 수 있었다.

 

else 문에서는 api 요청에서의 에러가 아닌 다른 상황에서의 에러라고 판단하여 throw new Error()를 이용하여 error를 발생시키도록 하였다.

 

이렇게만 처리한 경우 createSlice에서 type 에러가 발생했었는데, 에러가 발생한 코드는 위의 코드와는 조금 달랐었다. 

에러가 발생한 코드는 다음과 같다.

.addCase(fetchUserLogin.rejected, (state, action) => {
  if (state.loading === 'pending') {
    state.loading = 'idle';
    state.error = action.payload;
  }
})

여기서, state.error 부분이 에러가 발생하였고, 내용은 다음과 같다.

typescript 에러 내용...

사실, 어느 부분에서 undefined가 return되는지 정확하게 알 수 없었다. 그렇기 때문에, 약간의 꼼수를 부려 action.payload가 undefined가 아닌 경우를 조건문에 추가하여 사용하였다.

.addCase(fetchUserLogin.rejected, (state, action) => {
                                  // 추가된 부분
  if (state.loading === 'pending' && action.payload !== undefined) {
    state.loading = 'idle';
    state.error = action.payload;
  }
})

이런식으로 createAsyncThunk에서 조금 더 원하는 방식으로 에러를 다룰 수 있었고, 이를 통해 에러의 상태값에 따라 화면의 뷰를 변경시킬 수 있었다.

마치며

이런 작업을 하면서, 요즘 많이 사용하는 React-Query, SWR 등이 왜 인기가 많은지 알 수 있었다. redux-toolkit을 사용하게 되어 redux만 사용했을 때보다 보일러 플레이트가 많이 줄었다고는 하지만, Zustand나 Recoil에 비하면 코드량이 많기도 하다.

 

그럼에도 이 기술을 사용했던 이유는, 프론트엔드의 흐름을 알고 싶어서였다. 왜 redux가 불편하다고 하고 공부하기 어렵다고 하는지, 이 상태에서 redux-saga까지 사용하여 비동기 처리를 하는지 등을 redux-toolkit을 이용하여 찍먹해봤다고 생각한다. 이러한 흐름을 알면서 뭐가 더 나은 라이브러리인지를 알기보다는 어떤 상황을 해결해야 할 때 어떤 상태관리 라이브러리가 편리한지를 알 수 있도록 공부해야 한다고 생각한다.