Redux Thunk
리덕스 Thunk란?
리덕스를 사용하는 앱에서 비동기 작업을 할 때 많이 사용하는 방법이 redux-thunk이다. 이것도 앞서 만들어본 logger Middleware 처럼 리덕스 미들웨어이며, 리덕스를 개발한 Dan Abramov가 만들었다.
"thunk"라는 단어는 "일부 지연된 작업을 수행하는 코드 조각"을 의미하는 프로그래밍 용어이다. 지금 일부 논리(logic)를 실행하는 대신 나중에 작업을 수행하는데 사용할 수 있는 함수 본문이나 코드를 작성할 수 있다.
여러 경우가 있지만 서버에 요청을 보내서 데이터를 가져올 때 주로 비동기 요청을 보낸다.
비동기로 링크텍스트 에 요청을 보내면 Dummy 데이터를 받을 수 있다. 이 더미 데이터로 포스트를 만들어보자.
Api request를 위한 모듈
npm install axios --save
기존 reducers/todos.tsx의 코드를 복사해와서 수정해준다.
<reducers/posts.tsx>
// 'ActionType'이라는 이름의 enum을 선언합니다. 이 enum은 액션의 type 필드에 사용될 문자열 상수들을 정의합니다.
enum ActionType {
FETCH_POSTS = "FETCH_POSTS", // 포스트를 가져오는 액션
DELETE_POSTS = "DELETE_POSTS" // 포스트를 삭제하는 액션
}
// 'Post'라는 interface를 선언합니다. 이 interface는 게시물의 구조를 정의합니다.
interface Post {
userId: number; // 유저 아이디
id: number; // 게시물의 아이디
title: string; // 게시물의 제목
}
// 'Action'이라는 interface를 선언합니다. 이 interface는 액션 객체의 구조를 정의하는데 사용됩니다.
// 여기서는 type 필드는 'ActionType' enum으로 정의되고, payload 필드는 Post 배열로 정의되어 있습니다.
interface Action {
type: ActionType; // 액션 타입
payload: Post[] // 액션에 포함된 게시물 정보 배열
}
// 'posts'라는 리듀서 함수를 정의합니다. 이 함수는 현재 상태(state)와 액션 객체(action)를 받아서
// 새로운 상태를 반환하는 함수입니다. 이 리듀서는 포스트에 대한 상태 변화를 처리합니다.
const posts = (state = [], action: Action) => {
switch (action.type) {
// 액션 타입이 "FETCH_POSTS"인 경우, 새로 가져온 포스트를 상태 배열에 추가하여 반환합니다.
case "FETCH_POSTS":
return [...state, ...action.payload]
// 그 외의 액션 타입에 대해서는 현재 상태를 그대로 반환합니다.
default:
return state;
}
}
// 'posts' 리듀서를 export하여 다른 파일에서 import하여 사용할 수 있게 합니다.
export default posts;
그리고 reducers/index.tsx에서 posts.tsx를 import해주고,rootReducer 안에 posts를 추가한다.
App.tsx의 컴포넌트가 마운트 되었을 때 바로 posts를 가져와야하는데, jsonplaceholder 사이트에 요청을 보내면 가져올 수 있다. 그래서 posts 데이터를 위한 요청을 redux를 이용해 보낼 것이다.
<App.tsx>
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from './reducers';
import axios from 'axios';
type Props = {
value: number;
onIncrement: () => void;
onDecrement: () => void;
}
function App({ value, onIncrement, onDecrement }: Props) {
const dispatch = useDispatch()
const todos: string[] = useSelector((state: RootState) => state.todos)
const counter = useSelector((state: RootState) => state.counter);
const [todoValue, setTodoValue] = useState("");
// 컴포넌트가 마운트될 때, fetchPosts()를 호출합니다.
useEffect(() => {
dispatch(fetchPosts()) // fetchPosts()는 밑에 정의된 비동기 통신 함수입니다.
}, [dispatch]) // dispatch가 변경될 때마다 이 효과가 다시 실행됩니다.
// fetchPosts 함수는 비동기 작업을 처리하는 thunk 함수를 반환합니다.
const fetchPosts = (): any => {
// thunk 함수는 dispatch와 getState를 인자로 가진 함수입니다.
return async function fetchPostsThunk(dispatch:any, getState: any) {
// axios를 이용해 비동기적으로 데이터를 불러옵니다.
const response = await axios.get("https://jsonplaceholder.typicode.com/posts");
// 불러온 데이터를 FETCH_POSTS 액션과 함께 dispatch 합니다.
dispatch({ type: "FETCH_POSTS", payload: response.data })
}
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTodoValue(e.target.value);
}
const addTodo = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text: todoValue})
setTodoValue("");
}
return (
<div className="App">
Clicked: {counter} times
<button onClick={onIncrement}>
+
</button>
<button onClick={onDecrement}>
-
</button>
<ul>
{todos.map((todo, index) => <li key={index}>{todo}</li>)}
</ul>
<form onSubmit={addTodo}>
<input type="text" value={todoValue} onChange={handleChange} />
<input type="submit" />
</form>
</div>
);
}
export default App;
위와 같이 코드를 작성하면, 다음과 같이 에러가 발생하는 것을 확인할 수 있다.
원래 Actions은 객체여야 하는데 현재는 함수를 Dispatch 하고 있다. 그러기 때문에 나는 에러이다. 그래서 함수를 dispatch 할 수 있게 해주는 Redux-Thunk 미들웨어를 설치해서 사용해보자.
redux-thunk 설치
npm install redux-thunk --save
redux-thunk 적용
index.tsx에서 redux-thunk로 부터 thunk를 import해주고, applyMiddleware 안에 thunk를 추가해준다.
import thunk from 'redux-thunk';
그리고 다음과 같이 콘솔창을 확인하면, 데이터를 잘 받아온 것을 확인할 수 있다.
App.tsx에 useSelector라는 Hook을 사용해서 Redux 스토어의 state에서 posts라는 상태를 가져오는 작업을 수행하는 코드를 작성하자.
const posts: Post[] = useSelector((state: RootState) => state.posts)
참고로 다시 설명하자면, 이 코드에서 Post는 TypeScript에서 사용하는 interface이다. interface는 객체가 가져야 할 구조를 정의하는데 사용되는 문법이다.
여기서 Post 인터페이스는 Post라는 타입의 객체가 반드시 가져야 하는 프로퍼티와 그 타입을 정의하고 있는 것이다.
interface Post {
userId: number;
id: number;
title: string;
}
그리고 다음 코드를 추가해 posts를 랜더링 해준다.
<ul>
{posts.map((post, index) => <li key={index}>{post.title}</li>)}
</ul>
그럼 다음과 같이 posts들이 잘 가져와진 모습을 확인할 수 있다.
다음과 같이 폴더와 파일을 만들어주고,
App.tsx에 있는 다음의 코드를 옮겨준다. 그리고 export 해서 다른 곳에서 쓸 수 있도록 한다. 그리고 axios를 import 해온다.
<actions/posts.tsx>
import axios from "axios";
export const fetchPosts = (): any => {
return async function fetchPostsThunk(dispatch:any, getState: any) {
const response = await axios.get("https://jsonplaceholder.typicode.com/posts");
dispatch({ type: "FETCH_POSTS", payload: response.data })
}
}
그리고 App.tsx에서는 fetchPosts를 import해준다.
import {fetchPosts} from './actions/posts';
<actions/posts.tsx>
import axios from "axios";
export const fetchPosts = ():any => async(dispatch: any, getState: any) => {
const response = await axios.get("https://jsonplaceholder.typicode.com/posts");
dispatch({ type: "FETCH_POSTS", payload: response.data })
}
이렇게 redux-thunk를 사용함으로써 액션 생성자가 그저 하나의 액션 객체를 생성할 뿐 아니라 그 내부 안에서 여러 가지 작업도 할 수 있게 만들어 준다.