19. createAsyncThunk - 비동기 로직 단순화
09편에서 비동기 흐름을 “시작(pending) → 성공(fulfilled) → 실패(rejected)“라는 세 개의 동기 액션으로 손수 나눠 dispatch했습니다. 이 편은 이 반복적인 패턴을 createAsyncThunk 하나로 대체합니다.
학습 목표
createAsyncThunk가 API 호출 함수 하나로부터 pending/fulfilled/rejected 액션을 자동 생성하는 원리를 설명할 수 있다.extraReducers에서 이 세 상태를 처리해 로딩 상태를 관리할 수 있다.rejectWithValue로 에러 정보를 커스터마이징해 전달할 수 있다.
09편 패턴 복습: 손으로 만든 비동기 액션 3종
09편에서는 user 도메인으로 이 패턴을 봤습니다. 이번 편은 20편의 실습 프로젝트와 이어지도록 todos 도메인으로 같은 패턴을 다시 짜봅니다.
| |
액션 생성자 3개, thunk 함수 1개, 그리고 이 세 액션에 대응하는 리듀서 케이스까지 — API 호출 하나를 다루는 데 상당히 많은 코드가 필요했습니다.
createAsyncThunk: 하나의 비동기 함수로 액션 3종 생성
| |
createAsyncThunk("todos/fetchTodos", asyncFn)을 호출하면 다음 세 액션 타입이 자동으로 만들어집니다.
| 자동 생성되는 액션 타입 | 발생 시점 |
|---|---|
todos/fetchTodos/pending | 위 todosLoadStarted에 대응, 호출 시작 시 |
todos/fetchTodos/fulfilled | 위 todosLoadSucceeded에 대응, return한 값이 payload |
todos/fetchTodos/rejected | 위 todosLoadFailed에 대응, 예외 발생 시 |
액션 생성자 3개와 try/catch를 손으로 쓸 필요 없이, async 함수 하나만 작성하면 됩니다.
extraReducers로 세 상태 처리하기
17편에서 짧게 언급한 extraReducers가 바로 이 세 액션을 처리하는 곳입니다. 로딩 상태를 관리하는 표준 패턴은 다음과 같습니다.
| |
builder.addCase(actionCreator, reducerFn) 형태로 각 상태 전이를 처리합니다. fetchTodos.pending, fetchTodos.fulfilled, fetchTodos.rejected는 createAsyncThunk가 자동으로 붙여준 속성으로, 17편의 createAction으로 만든 액션과 마찬가지로 addCase의 첫 인자로 쓸 수 있습니다.
컴포넌트에서 사용하기
fetchTodos를 dispatch하는 쪽은 09편의 thunk를 dispatch하던 방식과 동일합니다.
| |
status 필드로 로딩/성공/실패 상태를 명시적으로 구분하는 것이 핵심입니다. 단순히 items.length === 0으로 “로딩 중"을 판단하면, “로딩이 끝났지만 결과가 빈 배열인 경우"와 구분할 수 없기 때문입니다.
rejectWithValue: 에러 정보 커스터마이징
기본적으로 rejected 액션의 action.error.message는 JS 예외 객체의 메시지를 그대로 담습니다. 서버가 반환하는 구조화된 에러 응답(예: { code: "NOT_FOUND", detail: "..." })을 그대로 전달하고 싶다면 rejectWithValue를 씁니다.
| |
이렇게 rejectWithValue로 감싼 값은 리듀서 쪽에서도 짝을 맞춰 읽어야 합니다. extraReducers의 rejected 케이스에서 action.payload를 우선 확인하도록 고칩니다.
| |
rejectWithValue로 반환한 값은 action.error가 아니라 **action.payload**에 담긴다는 점이 자주 헷갈리는 부분입니다. 이 구분은 리듀서에서 에러를 처리할 때 어느 필드를 읽어야 하는지 명확히 해줍니다.
인자를 받는 thunk
API 호출에 인자가 필요하면 async 함수의 첫 번째 매개변수로 받습니다.
| |
두 번째 매개변수는 { dispatch, getState, rejectWithValue, ... }를 담은 thunkAPI 객체로, 위 rejectWithValue 예시에서 이미 사용했습니다.
unwrap: 호출 지점에서 곧바로 성공/실패를 처리하기
지금까지는 extraReducers에서만 성공/실패를 처리했습니다. 하지만 폼 제출처럼 “이 dispatch가 성공했는지 바로 이 자리에서 알아야 하는” 상황도 자주 있습니다. dispatch(thunk())가 반환하는 Promise는 기본적으로 성공하든 실패하든 resolve되기 때문에(리듀서가 항상 fulfilled나 rejected 액션을 받아야 하므로), 일반적인 try/catch로는 실패를 잡을 수 없습니다. 이때 .unwrap()을 씁니다.
| |
.unwrap()이 없으면 dispatch(addTodo(text))가 반환하는 Promise는 실패했을 때도 그냥 rejected 액션 객체를 담은 채 정상적으로 resolve되므로, 컴포넌트 코드에서 try/catch로 실패를 감지할 수 없습니다. .unwrap()은 이 Promise를 “진짜 성공하면 resolve, 진짜 실패하면 reject"하는 일반적인 Promise로 바꿔줘서, 컴포넌트가 익숙한 try/catch로 후속 처리를 할 수 있게 해줍니다.
중복 요청 방지와 취소: condition과 signal
같은 요청이 이미 진행 중일 때 중복 dispatch를 막고 싶다면, condition 옵션으로 실행 여부를 직접 판단할 수 있습니다.
| |
condition이 false를 반환하면 pending조차 dispatch되지 않고 요청 자체가 취소됩니다 — 22편의 thunk에서 getState로 직접 가드를 작성했던 것과 같은 목적을, createAsyncThunk의 옵션으로 선언적으로 표현한 것입니다. 진행 중인 요청을 명시적으로 중단하고 싶다면, thunkAPI가 제공하는 signal(표준 AbortController의 signal)을 fetch에 그대로 전달합니다.
| |
실무 체크리스트
- API 호출을 09편 스타일의 손으로 만든 3-액션 thunk 대신
createAsyncThunk로 작성하고 있는가? - 로딩 상태를 boolean(
isLoading)이 아니라'idle'/'loading'/'succeeded'/'failed'같은 명시적 상태로 관리해, “로딩 끝 + 결과 없음"과 “로딩 중"을 구분하고 있는가? - 서버의 구조화된 에러 응답을 그대로 전달해야 한다면
rejectWithValue를 사용하고 있는가? - 폼 제출처럼 dispatch 지점에서 바로 성공/실패를 알아야 하는 곳에서
.unwrap()을 쓰고 있는가? - 이미 진행 중인 요청의 중복 dispatch를 막아야 한다면
condition옵션을 검토했는가?
연습 과제
기초(★☆☆)
- 09편의
fetchTodosthunk를createAsyncThunk버전으로 다시 작성해보세요.
중급(★★☆)
status필드를 활용해 “로딩 중”, “에러 발생”, “빈 목록”, “정상 목록” 네 가지 UI 상태를 모두 렌더링하는 컴포넌트를 작성해보세요.
고급(★★★)
- 서버가 404일 때
{ code: "NOT_FOUND" }를 응답한다고 가정하고,rejectWithValue로 이 정보를 받아 “해당 항목을 찾을 수 없습니다"라는 전용 메시지를 표시해보세요. fetchTodos에condition옵션을 추가해,status가"loading"일 때 같은 thunk를 두 번 연속 dispatch해도 네트워크 요청이 한 번만 발생하는지 확인해보세요.
요약
createAsyncThunk는 API 호출 함수 하나로부터 pending/fulfilled/rejected 세 액션을 자동 생성한다.extraReducers의addCase로 이 세 상태를 처리해 로딩 상태를 명시적으로 관리한다.rejectWithValue로 반환한 값은action.error가 아니라action.payload에 담긴다..unwrap()으로 dispatch 지점에서 바로 성공/실패를try/catch로 처리할 수 있고,condition/signal로 중복 요청 방지와 취소를 구현할 수 있다.
참고 문헌 및 출처(추천)
- Redux Toolkit 공식 문서, “createAsyncThunk” API 레퍼런스
- Redux Toolkit 공식 문서, “Redux Essentials, Part 5: Async Logic and Data Fetching”
- Redux Toolkit 공식 문서, “rejectWithValue”
![Featured image of post [Redux] 19. createAsyncThunk - 비동기 로직 단순화](/post/redux/create-async-thunk/wordcloud_hu_26e651913b573cec.webp)
![[Redux] 17. createSlice - 간결한 리듀서 작성](/post/redux/create-slice/wordcloud_hu_835b90d6ba21615f.webp)
![[Redux] 18. configureStore - Store 설정 자동화](/post/redux/configure-store/wordcloud_hu_286e52baac5619ca.webp)
![[Redux] 19. createAsyncThunk - 비동기 로직 단순화](/post/redux/create-async-thunk/wordcloud_hu_37ab180eb6db5e9b.webp)
![[Redux] 20. 실습: RTK로 앱 리팩터링](/post/redux/practice-rtk-app/wordcloud_hu_97bfaa8516f2360e.webp)
![[Redux] 21. Redux 미들웨어의 이해](/post/redux/redux-middleware/wordcloud_hu_650a1d7068d6bc0d.webp)
![[Redux] 22. Redux Thunk - 가장 간단한 비동기 처리](/post/redux/redux-thunk/wordcloud_hu_b4cddaa0144503a8.webp)
![[Redux] 24. RTK Query - 데이터 페칭의 혁명](/post/redux/rtk-query/wordcloud_hu_1807a958ecd17a19.webp)