22. Redux Thunk - 가장 간단한 비동기 처리
21편에서 미들웨어가 store => next => action => {} 구조를 가진다는 것을 배웠습니다. 이 편은 그 구조 위에서 redux-thunk가 정확히 어떤 한 가지 일을 하는지 직접 구현해보며 확인합니다.
학습 목표
redux-thunk미들웨어를 처음부터 직접 구현할 수 있다.- thunk 함수(action 대신 dispatch되는 함수)가 무엇이고 왜 유효한지 설명할 수 있다.
getState를 활용해 현재 상태에 따라 조건부로 dispatch를 결정하는 thunk를 작성할 수 있다.extraArgument로 API 클라이언트 같은 의존성을 thunk에 주입해 테스트하기 쉽게 만들 수 있다.
Thunk 미들웨어를 직접 구현하기
redux-thunk가 하는 일은 사실 단 하나입니다. “dispatch된 액션이 함수라면, 그 함수를 대신 실행해준다.”
| |
이것이 전부입니다. 07편에서 배운 dispatch(action)은 원래 일반 객체만 받도록 되어 있었는데, thunk 미들웨어가 “액션이 함수인 경우"라는 새로운 경로를 추가해준 것입니다.
Thunk 함수란 무엇인가
Thunk는 “나중에 실행할 계산을 감싼 함수"를 뜻하는 일반적인 프로그래밍 용어입니다. Redux 맥락에서 thunk는 (dispatch, getState) => { ... } 형태의 함수를 말합니다.
| |
store.dispatch(incrementLater())를 호출하면, incrementLater()가 반환한 함수가 thunkMiddleware에 도달합니다. typeof action === "function"이 참이므로, 21편의 next(action) 대신 그 함수 자체를 호출합니다. 이 함수 안에서 원하는 시점에 dispatch를 호출해 실제 액션을 발생시킬 수 있습니다.
fetchTodos를 thunk로 이해하기
09편(user 도메인)과 19편(todos 도메인)에서 이미 사용했던 비동기 액션 패턴이 사실 thunk였습니다. 이제 그 구조를 명확히 짚어봅니다.
| |
fetchTodos()는 액션 객체가 아니라 함수를 반환하는 함수입니다. dispatch(fetchTodos())를 호출하면, thunk 미들웨어가 이 내부 함수를 감지해 (dispatch, getState)를 인자로 실행해줍니다. 함수 내부에서는 await로 비동기 흐름을 자유롭게 제어하면서, 원하는 시점마다 dispatch로 동기 액션을 발생시킵니다.
getState로 조건부 dispatch하기
Thunk의 두 번째 인자 getState는 dispatch 시점의 최신 상태를 읽을 수 있게 해줍니다. 이를 활용하면 “이미 로딩 중이면 중복 요청을 막는다” 같은 로직을 구현할 수 있습니다.
| |
컴포넌트가 마운트될 때마다 fetchTodosIfNeeded()를 호출해도, 이미 데이터를 가져온 상태라면 중복 네트워크 요청이 발생하지 않습니다. 이 패턴은 getState가 리듀서 밖(미들웨어)에서도 상태에 접근할 수 있다는 것을 보여주는 대표적인 예입니다.
extraArgument: API 클라이언트를 thunk에 주입하기
지금까지의 예제는 thunk 안에서 fetch를 직접 호출했습니다. 하지만 실무에서는 API 호출 로직을 별도의 클라이언트 객체(인증 헤더, 재시도, 공통 에러 처리를 캡슐화한 래퍼)로 분리하는 경우가 많고, 이 클라이언트를 테스트에서는 가짜 구현으로 바꿔 끼우고 싶을 때가 있습니다. Thunk 미들웨어는 이런 상황을 위해 세 번째 인자를 지원합니다.
| |
| |
테스트 코드에서는 실제 apiClient 대신 가짜 객체({ get: () => Promise.resolve([...]) })를 넘겨 네트워크 없이 thunk를 검증할 수 있습니다. dispatch, getState에 이어 extraArgument까지 활용하면, thunk 함수가 필요로 하는 의존성을 전부 매개변수로 받아 테스트하기 쉬운 형태로 유지할 수 있습니다.
여러 액션을 조합하는 thunk
Thunk는 여러 개의 dispatch를 원하는 순서와 조건으로 조합할 수 있습니다.
| |
이처럼 “검증 → 시작 알림 → API 호출 → 성공 시 후속 액션 두 개” 같은 여러 단계를 하나의 thunk 함수 안에 자연스럽게 표현할 수 있습니다.
Thunk의 한계
Thunk는 간단하지만, 다음과 같은 상황에서는 코드가 복잡해지기 쉽습니다.
- 여러 비동기 작업의 취소: 사용자가 검색어를 빠르게 여러 번 입력할 때, 이전 요청을 취소하고 마지막 요청만 반영하는 로직은 thunk 안에서 직접
AbortController등을 관리해야 한다. - 복잡한 순서 제어: “A가 끝난 뒤 B와 C를 동시에 실행하고, 그중 하나라도 실패하면 D를 실행” 같은 흐름은
async/await만으로 표현하면 코드가 빠르게 복잡해진다.
이런 경우를 위한 대안이 23편의 Redux Saga입니다. Saga는 제너레이터 함수로 이런 복잡한 흐름을 더 선언적으로 표현합니다. 다만 대부분의 실무 프로젝트에서는 thunk만으로 충분하며, Saga는 팀이 복잡한 사이드 이펙트 요구사항을 명확히 가지고 있을 때 선택하는 것이 일반적입니다.
실무 체크리스트
- Thunk 안에서
dispatch를 호출하는 순서가 실제로 의도한 상태 전이 순서와 일치하는가? - 중복 요청을 막아야 하는 thunk에
getState로 현재 상태를 확인하는 가드를 추가했는가? - API 호출 로직을 thunk 안에 하드코딩하지 않고,
extraArgument로 주입해 테스트에서 가짜 구현으로 교체할 수 있게 했는가? - thunk가 지나치게 복잡한 순서 제어를 담당하고 있다면, Saga 같은 대안이 더 적합하지 않은지 검토했는가?
연습 과제
기초(★☆☆)
thunkMiddleware를 직접 작성하고applyMiddleware로 등록해,dispatch(function() {...})형태의 액션이 정상적으로 실행되는지 확인해보세요.
중급(★★☆)
fetchTodosIfNeeded()를 작성해, 같은 컴포넌트가 두 번 마운트되어도 네트워크 요청이 한 번만 발생하는지 확인해보세요.
고급(★★★)
checkoutCart()thunk에 “이미 체크아웃이 진행 중이면 중복 실행을 막는” 가드를getState로 추가해보세요.fetchTodos를extraArgument로 주입한apiClient를 쓰도록 리팩터링하고, 테스트에서 가짜apiClient({ get: () => Promise.resolve([...]) })를 넘겨 실제 네트워크 없이 검증해보세요.
요약
redux-thunk는 “dispatch된 값이 함수면 그 함수를 대신 실행한다"는 단 하나의 규칙을 가진 미들웨어다.- Thunk 함수는
(dispatch, getState, extraArgument)를 받아, 그 안에서 자유롭게 비동기 로직과 조건부 dispatch를 조합하고 외부 의존성을 주입받을 수 있다. - 복잡한 순서 제어나 요청 취소가 필요한 경우에는 23편의 Saga가 대안이 될 수 있다.
참고 문헌 및 출처(추천)
- Redux Thunk 공식 문서(GitHub), “redux-thunk”
- Redux 공식 문서, “Writing Tests, Async Action Creators”
![Featured image of post [Redux] 22. Redux Thunk - 가장 간단한 비동기 처리](/post/redux/redux-thunk/wordcloud_hu_673ec04fcfc125a7.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] 23. Redux Saga - 강력한 사이드 이펙트 관리](/post/redux/redux-saga/wordcloud_hu_61e6fe9d8596c675.webp)
![[Redux] 24. RTK Query - 데이터 페칭의 혁명](/post/redux/rtk-query/wordcloud_hu_1807a958ecd17a19.webp)
![[Redux] 19. createAsyncThunk - 비동기 로직 단순화](/post/redux/create-async-thunk/wordcloud_hu_37ab180eb6db5e9b.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)