도서 리액트를 다루는 기술 | 김민준 을 읽고 작성한 게시글입니다.
- 리액트 프로젝트에서 API 서버를 연동할 때는 API 요청에 대한 상태도 잘 관리해야 한다.
- 예를 들어 요청이 시작되었을 때는 로딩 중임을, 요청이 성공하거나 실패했을 때는 로딩이 끝났음을 명시해야 한다.
- 요청이 성공하면 서버에서 받아온 응답에 대한 상태를 관리하고, 요청이 실패하면 서버에서 반환한 에러에 대한 상태를 관리해야 한다.
- 리액트 프로젝트에서 리덕스를 사용하고 있으며 이러한 비동기 작업을 관리해야 한다면, 미들웨어를 사용하여 매우 효율적이고 편하게 상태 관리를 할 수 있다.
미들웨어란?
- 리덕스 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행한다.
- 미들웨어는 액션과 리듀서 사이의 중간이라고 볼 수 있다.
- 리듀서가 액션을 처리하기 전에 미들웨어가 할 수 있는 작업은 여러가지가 있다.
- 전달받은 액션을 단순히 콘솔에 기록하거나, 전달받은 액션 정보를 기반으로 액션을 아예 취소하거나, 다른 종류의 액션을 추가로 디스패치할 수도 있다.
미들웨어 만들기
- 실제 프로젝트를 작업할 때 미들웨어를 직접 만들어서 사용할 일은 그리 많지 않고, 다른 개발자가 만들어 놓은 미들웨어를 사용한다.
- 하지만 미들웨어가 어떻게 작동하는지 이해하려면 직접 만들어 보는 것이 가장 효과적이다.
- 원하는 미들웨어를 찾을 수 없을 때는 상황에 따라 직접 만들거나 기존 미들웨어들을 커스터마이징하여 사용할 수도 있다.
- 미들웨어의 기본 구조는 아래와 같다.
// src/lib/loggerMiddleware
const loggerMiddleware = store=>next=>action=>{
// 미들웨어 기본 구조
};
// 위를 풀어서 쓴 구조
const loggerMiddleware = funtion loggerMiddleware(store){
return function(next){
return function(action){
// 미들웨어 기본 구조
}
}
}
- 미들웨어는 결국 함수를 반환하는 함수를 반환하는 함수이다.
- 함수 파라미터로 받아오는
store
는리덕스 스토어
인스턴스를,action
은디스패치된 액션
을 가리킨다. next
파라미터는 함수 형태이며store.dispatch
와 비슷한 역할을 한다- 하지만,
next(action)
을 호출하면 그다음 처리해야 할 미들웨어에게 액션을 넘겨주고, 만약 그다음 미들웨어가 없다면 리듀서에게 액션을 넘겨준다는 차이점이 있다.
- 미들웨어 내부에서
store.dispatch
를 사용하면 첫번째 미들웨어부터 다시 처리한다. - 만약 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않는다.
- 즉, 액션이 무시되는 것이다.
redux-logger 사용하기
- 오픈 소스 커뮤니티에 이미 올라와 있는 redux-logger 미들웨어를 사용한다.
npm install redux-logger
npm install @types/redux-logger
import { createLogger } from "redux-logger";
import { applyMiddleware, legacy_createStore as createStore } from "redux";
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(logger));
- 이전 상태와, 액션 정보, 새로워진 상태를 콘솔에 출력한다.
비동기 작업을 처리하는 미들웨어 사용
redux-thunk
- 비동기 작업을 처리할 때 가장 많이 사용되는 미들웨어이다.
- 객체가 아닌 함수 형태의 액션을 디스패치할 수 있게 해준다.
thunk란?
- Thunk는 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것을 의미한다.
- redux-thunk 라이브러리를 사용하면 thunk 함수를 만들어서 디스패치할 수 있다.
- 그러면 리덕스 미들웨어가 그 함수를 전달받아 store의 dispatch와 getState를 파라미터로 넣어서 호출해 준다.
- 아래는 redux-thunk에서 사용할 수 있는 예시 thunk 함수이다.
const sampleThunk = () => (dispatch, getState) => {
// 현재 상태를 참조할 수 있고,
// 새 액션을 디스패치할 수도 있다.
}
미들웨어 적용하기
npm i reudx-thunk // 공식적으로 TS를 지원하므로, 따로 설치x
- 미들웨어는 여러개 적용이 가능하다.
import { thunk } from "redux-thunk";
const logger = createLogger();
const store = createStore(rootReducer, applyMiddleware(thunk, logger));
thunk 생성 함수 만들기
redux-thunk
는 액션 생성 함수에서 일반 액션을 반환하는 대신에 함수를 반환한다.- 숫자가 1초뒤에 변경되도록 비동기 함수로 변경한다.
// src/modules/counter.ts
export const increaseAsync = () => (dispatch: Dispatch) => {
setTimeout(() => {
dispatch(increase());
}, 1000);
};
export const decreaseAsync = () => (dispatch: Dispatch) => {
setTimeout(() => {
dispatch(decrease());
}, 1000);
};
const actions = { increase, decrease, increaseAsync, decreaseAsync };
- CounterContainer에서 호출하던 액션 생성 함수도 변경해준다.
// src/containers/CounterContainer.tsx
import { ThunkDispatch } from "redux-thunk";
const CounterContainer = () => {
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch<ThunkDispatch<RootState, null, CounterAction>>();
const onIncrease = useCallback(() => dispatch(increaseAsync()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decreaseAsync()), [dispatch]);
return (
<Counter count={count} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
아래 그림과 같이 비동기적으로 1초 뒤에 숫자가 변경되고 콘솔에 로그가 찍히는 것을 확인할 수 있다.
웹 요청 비동기 작업 처리하기
- https://jsonplaceholder.typicode.com/ 에서 제공하는 더미데이터 API를 활용한다.
- 포스트 읽기 (:id는 1~100 사이 숫자),
GET
https://jsonplaceholder.typicode.com/posts/:id - 모든 사용자 정보 불러오기
GET
https://jsonplaceholder.typicode.com/users
- 포스트 읽기 (:id는 1~100 사이 숫자),
- API를 호출할 때는 주로
Promise
기반 웹 클라이언트인axios
를 사용한다.
axios 라이브러리 설치
npm i axios
api 호출 함수 작성
- API를 호출하는 함수를 따로 작성하면, 나중에 사용할 때 가독성도 좋고 유지 보수도 쉬워진다.
// src/lib/api.ts
import axios from "axios";
const API_URL = "https://jsonplaceholder.typicode.com";
export const getPost = (id: number) => axios.get(`${API_URL}/${id}`);
export const getUsers = () => axios.get(`${API_URL}/users`);
sample 리듀서 작성
- User 타입과 Post타입은 types 디렉토리에 미리 선언한다. (코드 생략)
- 위에서 선언한 API를 사용하여 데이터를 받아와 상태를 관리하는 sample이라는 리듀서를 생성한다.
// src/modules/sample.ts
/* 액션 타입 생성 - 한 요청 당 세개를 만들어야 함 */
const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_POST_FAILURE = "sample/GET_POST_FAILURE";
const GET_USERS = "sample/GET_USERS";
const GET_USERS_FAILURE = "sample/GET_USERS_FAILURE";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
/* createAsyncAction을 사용하여 액션 타입과 크리에이터를 정의한다. */
export const getPostAction = createAsyncAction(
GET_POST,
GET_POST_SUCCESS,
GET_POST_FAILURE
)<undefined, Post, Error>();
export const getUsersAction = createAsyncAction(
GET_USERS,
GET_USERS_SUCCESS,
GET_USERS_FAILURE
)<undefined, User[], Error>();
/* 액션 타입 정의 */
export type SampleAction = ActionType<
typeof getPostAction | typeof getUsersAction
>;
/* thunk 함수 생성
thunk 함수 내부에서는 시작할 때, 성공할 때, 실패했을 때 다른 액션을 디스패치함 */
export const getPost = (id: number) => async (dispatch: Dispatch) => {
dispatch({ type: GET_POST }); // 요청 시작을 알림
try {
const res = await api.getPost(id);
dispatch({
type: GET_POST_SUCCESS,
payload: res.data,
}); // 요청 성공
} catch (e) {
dispatch({
type: GET_POST_FAILURE,
payload: e,
error: true,
}); // 요청 실패, 에러 발생
throw e; // 나중에 컴포넌트단에서 에러를 조회할 수 있게 던져줌
}
};
export const getUsers = () => async (dispatch: Dispatch) => {
dispatch({ type: GET_USERS }); // 요청 시작을 알림
try {
const res = await api.getUsers();
dispatch({ type: GET_USERS_SUCCESS, payload: res.data }); // 요청 성공
} catch (e) {
dispatch({
type: GET_USERS_FAILURE,
payload: e,
error: true,
}); // 요청 실패, 에러 발생
throw e; // 나중에 컴포넌트 단에서 에러를 조회할 수 있게 던져줌
}
};
interface SampleState {
loading: {
GET_POST: boolean;
GET_USERS: boolean;
};
post: Post | null;
users: User[] | null;
}
/* 초기 상태 선언
요청의 로딩중 상태는 loading이라는 객체에서 관리 */
const initailState: SampleState = {
loading: {
GET_POST: false,
GET_USERS: false,
},
post: null,
users: null,
};
const sample = createReducer<SampleState, SampleAction>(initailState, {
[GET_POST]: (state) => ({
...state,
loading: {
...state.loading,
GET_POST: true,
},
}),
[GET_POST_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false,
},
post: action.payload,
}),
[GET_POST_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_POST: false,
},
}),
[GET_USERS]: (state) => ({
...state,
loading: {
...state.loading,
GET_USERS: true,
},
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false,
},
users: action.payload,
}),
[GET_USERS_FAILURE]: (state, action) => ({
...state,
loading: {
...state.loading,
GET_USERS: false,
},
}),
});
export default sample;
- 루트 리듀서에 sample 리듀서를 포함시킨다.
// src/modules/index.ts
const rootReducer = combineReducers({
counter,
todos,
sample,
});
export type RootState = ReturnType<typeof rootReducer>;
sample 컴포넌트 작성
- sample 컴포넌트를 작성한다.
- post의 title과 body, Users의 name과 username, email을 렌더링한다.
// src/components/Sample.tsx
import { Post } from "../types/post";
import { User } from "../types/user";
interface SampleProps {
loadingPost: boolean;
loadingUsers: boolean;
post: Post | null;
users: User[] | null;
}
const Sample = ({ loadingPost, loadingUsers, post, users }: SampleProps) => {
return (
<div>
<section>
<h1>post</h1>
{loadingPost && "loading ... "}
{!loadingPost && post && (
<div>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
)}
</section>
<hr />
<section>
<h1>Users</h1>
{loadingUsers && "loading ..."}
{!loadingUsers && users && (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} {user.username} ({user.email})
</li>
))}
</ul>
)}
</section>
</div>
);
};
export default Sample;
sample 컨테이너 컴포넌트 생성
- 컨테이너 컴포넌트를 생성한다.
- 위에서 생성한 sample 컴포넌트를 불러온다.
// src/containers/SampleContainer.tsx
import { useEffect } from "react";
import { Post } from "../types/post";
import { User } from "../types/user";
import { getPost, getUsers } from "../modules/sample";
import Sample from "../components/Sample";
import { connect } from "react-redux";
import { RootState } from "../modules";
import { Dispatch, bindActionCreators } from "redux";
interface SampleContainerProps {
post: Post | null;
users: User[] | null;
loadingPost: boolean;
loadingUsers: boolean;
getPost: typeof getPost;
getUsers: typeof getUsers;
}
const SampleContainer = ({
post,
users,
loadingPost,
loadingUsers,
getPost,
getUsers,
}: SampleContainerProps) => {
useEffect(() => {
getPost(1);
getUsers();
}, [getPost, getUsers]);
return (
<Sample
post={post}
users={users}
loadingPost={loadingPost}
loadingUsers={loadingUsers}
/>
);
};
export default connect(
(state: RootState) => ({
post: state.sample.post,
users: state.sample.users,
loadingPost: state.sample.loading.GET_POST,
loadingUsers: state.sample.loading.GET_USERS,
}),
(dispatch: Dispatch) => bindActionCreators({ getPost, getUsers }, dispatch)
)(SampleContainer);
- App.tsx에서 SampleContainer를 렌더링한다.
// src/App.tsx
function App() {
return (
<div className="App">
<CounterContainer />
<hr />
<TodosContainer />
<hr />
<SampleContainer />
</div>
);
}
export default App;
결과 확인
post와 Users의 데이터를 화면에 성공적으로 출력하는 것을 확인할 수 있다.
콘솔을 열어 확인하면 액션 발생 순서를 확인할 수 있다.
- GET_POST
- GET_USERS
- GET_POST_SUCESS (만약 실패일 경우 GET_POST_FAILURE 가 대신 발생)
- GET_USERS_SUCESS (만약 실패일 경우 GET_USERS_FAILURE 가 대신 발생)
코드 리팩토링
thunk 유틸 함수 정의
- API를 요청할 때마다 긴 thunk 함수를 작성하고 로딩 상태를 리듀서에서 관리하는 작업은 귀찮을 뿐 아니라 코드도 길어지게 만드므로 반복되는 로직을 따로 분리하여 코드의 양을 줄여보자.
- 아래와 같이 유틸 함수를 만들어 API 요청을 해주는 thunk 함수를 간단하게 생성할 수 있게 해준다.
// src/lib/createReqestThunk.ts
import { AxiosResponse } from "axios";
import { Dispatch } from "redux";
interface CreateRequestThunkProps<TRequestParams, TResponseData> {
type: string;
request: (params: TRequestParams) => Promise<AxiosResponse<TResponseData>>;
}
const createRequestThunk = <TRequestParams, TResponseData>({
type,
request,
}: CreateRequestThunkProps<TRequestParams, TResponseData>) => {
const SUCCESS = `${type}_SUCCESS`;
const FAILURE = `${type}_FAILURE`;
return (params?: any) => async (dispatch: Dispatch) => {
dispatch({ type }); // 시작됨
try {
const res = await request(params);
dispatch({
type: SUCCESS,
payload: res.data,
});
} catch (e) {
dispatch({
type: FAILURE,
payload: e,
error: true,
}); // 에러 발생
throw e;
}
};
};
export default createRequestThunk;
// 사용법 ex : createRequestThunk("GET_USERS".api.getUsers);
- 액션 타입과 API를 요청하는 함수를 파라미터로 넣어주면 나머지 작업을 대신 처리해준다.
- 이외의 코드는 모두 동일하다.
// src/modules/sample.ts
(...)
export const getPost = createRequestThunk<number, Post>({
type: GET_POST,
request: api.getPost,
});
export const getUsers = createRequestThunk<null, User[]>({
type: GET_USERS,
request: api.getUsers,
});
(...)
createRequestThunk 함수 추가 설명 (feat.갓 GPT)
createRequestThunk
는 고차 함수이다.- 고차 함수란 다른 함수를 반환하는 함수이다.
- 이 경우
createRequestThunk
는 매개변수로 설정과 요청 함수를 받고, 이를 바탕으로 새로운 함수를 반환한다.
export const getPost = createRequestThunk<number, Post>({
type: GET_POST,
request: api.getPost,
});
- 위 코드에서
getPost
는createRequestThunk
에 의해 반환된 첫 번째 함수를 실행한 결과이다. - 이 함수는 두 번째 화살표 함수를 반환하고, 이 함수는
params
를 매개변수로 받는 비동기 함수이다.
getPost(1);
getPost(1)
을 호출하면,getPost
가 참조하는 두 번째 화살표 함수가 실행되며, 이때1
이params
매개변수로 전달된다.createRequestThunk
에서 반환된 함수는 클로저를 형성한다.- 클로저란 자신이 생성될 때의 환경을 '기억'하는 함수이다.
- 여기서 두 번째 화살표 함수는 첫 번째 화살표 함수의
type
과request
매개변수에 대한 참조를 유지한다. - 따라서
getPost(1)
을 호출할 때, 이 함수는type
과request
에 접근할 수 있으며,params
로 받은1
을 사용하여api.getPost
를 호출할 수 있습니다.
로딩 상태 관리
- 기존에는 리듀서 내부에서 각 요청에 관한 액션이 디스패치될 때마다 로딩 상태를 변경해주었다.
- 이 작업을 로딩 상태만 관리하는 리덕스 모듈을 따로 생성하여 처리해보자.
import { handleActions } from "redux-actions";
const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";
/* 요청을 위한 액션 타입을 payload로 설정한다. (ex. "sample/GET_POST") */
export const startLoading = (requestType: string) => ({
type: START_LOADING,
payload: requestType,
});
export const finishLoading = (requestType: string) => ({
type: FINISH_LOADING,
payload: requestType,
});
interface LoadingState {
[requestType: string]: boolean;
}
const initialState: LoadingState = {};
// handleActions<StateType, PayloadType>
const loading = handleActions<LoadingState, string>(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true,
}),
[FINISH_LOADING]: (state, action) => ({
...state,
[action.payload]: false,
}),
},
initialState
);
export default loading;
- 루트 리듀서에 loading 모듈을 포함시킨다.
const rootReducer = combineReducers({
counter,
todos,
sample,
loading,
});
export type RootState = ReturnType<typeof rootReducer>;
- sampleContainer에서 로딩 상태를 위에서 만든 loadung 리덕스 모듈을 통해 조회할 수 있다.
// src/containers/SampleContainer.tsx
(...)
export default connect(
(state: RootState) => ({
post: state.sample.post,
users: state.sample.users,
loadingPost: state.loading["sample/GET_POST"],
loadingUsers: state.loading["sample/GET_USERS"],
}),
(dispatch: Dispatch) => bindActionCreators({ getPost, getUsers }, dispatch)
)(SampleContainer);
- 기존 sample 모듈에서의 로딩중에 대한 상태관리를 할 필요가 없어졌다.
- 성공했을 때의 케이스만 잘 관리해주면 된다.
// src/modulex/sample.ts
(...)
/* 액션 타입 선언 */
const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";
const GET_USERS = "sample/GET_USERS";
const GET_USERS_SUCCESS = "sample/GET_USERS_SUCCESS";
export const get_post_success = createAction(GET_POST_SUCCESS)<Post>();
export const get_users_success = createAction(GET_USERS_SUCCESS)<User[]>();
const actions = { get_post_success, get_users_success };
/* 액션 타입 정의 */
export type SampleAction = ActionType<typeof actions>;
/* thunk 함수 생성
thunk 함수 내부에서는 시작할 때, 성공할 때, 실패했을 때 다른 액션을 디스패치함 */
export const getPost = createRequestThunk<number, Post>({
type: GET_POST,
request: api.getPost,
});
export const getUsers = createRequestThunk<null, User[]>({
type: GET_USERS,
request: api.getUsers,
});
interface SampleState {
post: Post | null;
users: User[] | null;
}
/* 초기 상태 선언 */
const initailState: SampleState = {
post: null,
users: null,
};
const sample = createReducer<SampleState, SampleAction>(initailState, {
[GET_POST_SUCCESS]: (state, action) => ({
...state,
post: action.payload,
}),
[GET_USERS_SUCCESS]: (state, action) => ({
...state,
users: action.payload,
}),
});
export default sample;
- 추가로, 실패했을 때의 케이스를 관리하고 싶다면,
_FAILURE
가 붙은 액션을 리듀서에서 처리해주면 된다. - 또는, 컨테이너 컴포넌트에서
try/catch
구문을 사용해 에러 값을 조회할 수도 있다.
// src/containers/SampleContainer.tsx
(...)
useEffect(() => {
const fn = async () => {
try {
await getPost(138428309);
await getUsers();
} catch (e) {
console.error(e);
}
};
fn();
}, [getPost, getUsers]);
(...)
redux-saga
redux-saga
는redux-thunk
다음으로 많이 사용하는 비동기 작업 관련 미들웨어이다.- redux-thunk 는 함수 형태의 액션을 디스패치하여 미들웨어에서 해당 함수에 스토어의 dispatch와 getState를 파라미터로 사용하는 원리이다. 그래서 구현한 thunk 함수 내부에서 원하는 API 요청도 하고, 다른 액션을 디스패치하거나 현재 상태를 조회하기도 했다.
- 대부분의 경우는 redux-thunk로도 충분히 기능을 구현할 수 있다.
- redux-saga는 좀 더 까다로운 상황에 유용하다.
- 예를 들어 아래와 같은 상황에서는 redux-saga를 사용하는 것이 유리하다.
- 기존 요청을 취소 처리해야할 때 (불필요한 중복 요청 방지)
- 특정 액션이 발생했을 때 다른 액션을 발생시키거나, API 요청 들 리덕스와 관계없는 코드를 실행할 때
- 웹소켓을 사용할 때
- API 요청 실패 시 재요청해야 할 때
제너레이터 함수 이해하기
redux-saga
에서는 ES6 의제너레이터(generator)
함수라는 문법을 사용한다.- 보통 일반적인 상황에서는 많이 사용되지 않기 때문에 초반에 진입 장벽이 있을 수 있다.
- 제너레이터 문법의 핵심 기능은 함수를 작성할 때 함수를 특정 구간에 멈춰 놓을 수도 있고, 원할 때 다시 돌아가게 할 수도 있다는 것이다.
- 다음과 같은 함수가 있다고 가정해보자.
function weirdFunction() {
return 1;
return 2;
return 3;
return 4;
return 5;
}
- 하나의 함수에서 값을 여러 개 반환하는 것은 불가능하므로 이 코드는 제대로 작동하지 않는다.
- 정확히는 호출할 때마다 맨 위에 있는 값인 1만 반환된다.
- 하지만 제너레이터 함수를 사용하면 함수에서 값을 순차적으로 반환할 수 있다.
- 심지어는 함수의 흐름을 도중에 멈춰놓았다가 다시 이어서 진행시킬 수도 있다.
- 크롬 개발자 도구 콘솔에서 다음 함수를 작성해보자.
function* generatorFunction() {
console.log("Hello");
yield 1;
console.log("제너레이터 함수");
yield 2;
console.log("function*");
yield 3;
return 4;
}
- 제너레이터 함수를 만들 때는
funtion*
키워드를 사용한다. - 제너레이터 함수에는 화살표 함수를 사용할 수 없다.
- 함수를 작성한 뒤에는 다음 코드를 사용해 제너레이터를 생성한다.
const generator = generatorFunction();
- 제너레이터 함수를 호출했을 때 반환되는 객체를
제너레이터
라고 부른다. - 다음 코드를 순차적으로 한 줄씩 입력하고 어떤 결과가 나타나는지 확인해보자.
generator.next();
// Hello
// {value: 1, done: false}
generator.next();
// 제너레이터 함수
// {value: 2, done: false}
generator.next();
// function*
// {value: 3, done: false}
generator.next();
// {value: 4, done: true}
generator.next();
//{value: undefined, done: true}
- 제너레이터가 처음 만들어지면 함수의 흐름은 멈춰있는 상태이다.
next()
가 호출되면 다음yield
가 있는 곳까지 호출하고 다시 함수가 멈춘다.- 제너레이터 함수를 사용하면 함수를 도중에 멈출 수도 있고, 순차적으로 여러 값을 반환시킬 수도 있다.
next
함수에 파라미터를 넣으면 제너레이터 함수에서yield
를 사용하여 해당 값을 조회할 수도 있다.
function* sumGenerator() {
console.log("sumGenerator가 만들어졌습니다.");
let a = yield;
let b = yield;
yield a + b;
}
const sum = sumGenerator();
sum.next(); // 첫번째 yield에서 함수 실행이 멈춤
// sumGenerator가 만들어졌습니다.
// {value: undefined, done: false}
sum.next(1); // 1이 첫번째 yeild에 할당됨
// {value: undefined, done: false}
sum.next(2); // 2가 두번째 yield에 할당됨
// {value: 3, done: false}
sum.next();
// {value: undefined, done: true}
next(value)
메소드가 호출될 때,value
는 이전yield
표현식의 결과값으로 간주된다.- 즉, 제너레이터 함수 내부에서
yield
표현식은next()
에 전달된value
값으로 평가되고, 함수는 다음yield
표현식이 나타날 때까지 실행된다. reudx-saga
는 제너레이터 함수 문법을 기반으로 비동기 작업을 관리해준다.redux-saga
는 우리가 디스패치하는 액션을 모니터링해서 그에 따라 필요한 작업을 따로 수행할 수 있는 미들웨어이다.
function* watchGenerator() {
console.log("모니터링 중 ...");
let prevAction = null;
while (true) {
const action = yield;
console.log("이전 액션 : ", prevAction);
prevAction = action;
if (action.type === "HELLO") {
console.log("안녕하세요!");
}
}
}
const watch = watchGenerator();
watch.next();
// 모니터링 중 ...
// {value: undefined, done: false}
watch.next({ type: "TEST" });
// 이전 액션 : null
// {value: undefined, done: false}
watch.next({ type: "HELLLO" });
// 이전 액션 : {type: 'TEST'}
// {value: undefined, done: false}
redux-saga
는 위와 비슷한 원리로 동작한다.- 제너레이터 함수의 작동 방식만 기본적으로 파악하고 있으면, redux-saga에서 제공하는 여러가지 유용한 유틸 함수를 사용하여 액션을 쉽게 처리할 수 있다.
비동기 카운터 만들기
redux-saga 라이브러리 설치
npm i redux-saga
- counter 리덕스 모듈에서 기존 thunk 함수를 제거하고, INCRESE_ASYNC와 DECRESE_ASYNC라는 액션 타입을 생성한다.
- 해당 액션에 대한 액션 생성 함수도 만들고, 이어서 제너레이터 함수를 만든다.
- 이 제너레이터 함수를
사가(SAGA)
라고 부른다.
// src/modules/counter.ts
(...)
import { delay, put, takeEvery, takeLatest } from "redux-saga/effects";
(...)
const INCREASE_ASYNC = "count/INCREASE_ASYNC";
const DECREASE_ASYNC = "count/DECREASE_ASYNC";
export const increase = createStandardAction(INCREASE)();
export const decrease = createStandardAction(DECREASE)();
// 마우스 클릭 이벤트가 payload 안에 들어가지 않도록 undefined를 넣어준다.
export const increaseAsync = createStandardAction(INCREASE_ASYNC)<undefined>();
export const decreaseAsync = createStandardAction(DECREASE_ASYNC)<undefined>();
(...)
function* increaseSaga() {
yield delay(1000);
yield put(increase());
}
function* decreaseSaga() {
yield delay(1000);
yield put(decrease());
}
export function* counterSaga() {
// takeEvery는 들어오는 모든 액션에 대해 특정 작업을 처리해준다.
yield takeEvery(INCREASE_ASYNC, increaseSaga);
// takeLatest는 기존에 진행중이던 작업이 있다면 취소 처리하고 가장 마지막으로 실행된 작업만 수행한다.
yield takeLatest(DECREASE_ASYNC, decreaseSaga);
}
(...)
- 그리고 루트 리듀서를 만들었던 것처럼 루트 사가를 만들어주어야 한다.
- 추후 다른 리듀서에서도 사가를 만들어 등록할 것이기 때문이다.
// src/modules/index.ts
(...)
export function* rootSaga() {
// all 함수는 여러 사가를 함쳐주는 역할을 한다.
yield all([counterSaga()]);
}
export default rootReducer;
- 이제 스토어에 redux-saga 미들웨어를 적용해준다.
// src/index.tsx
(...)
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(logger, thunk, sagaMiddleware)
);
(...)
- 카운터 컨테이너에서 타입을 수정해준다.
// src/containers/CounerContainer.tsx
// 기존 dispatch
const dispatch = useDispatch<ThunkDispatch<RootState, null, CounterAction>>();
// dispatch 수정
const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increaseAsync()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decreaseAsync()), [dispatch]);
정리
redux-thun
k는 일반 함수로 이루어져 있기 때문에 간단명료하다는 장점이 있고,redux-saga
는 진입 장벽이 조금 있을 수 있으나 복잡한 상황에서 더욱 효율적으로 작업을 관리할 수 있다는 장점이 있다.- 또 다른 미들웨어로는
redux-promise-middleware
,redix-pender
,redux-observable
등이 있다.
'BOOK > 리액트를 다루는 기술' 카테고리의 다른 글
[ 리액트를 다루는 기술 ] 17장 리덕스 사용하여 리액트 애플리케이션 상태 관리하기 (1) | 2023.12.17 |
---|---|
[ 리액트를 다루는 기술 ] 16장 리덕스 라이브러리 이해하기 (0) | 2023.12.14 |
[ 리액트를 다루는 기술 ] 15장 Context API (0) | 2023.12.14 |