DEVELOP
article thumbnail

도서 리액트를 다루는 기술 | 김민준 을 읽고 작성한 게시글입니다.

 

리액트를 다루는 기술(개정판)

개발은 언제나 즐겁고 재밌어야 한다는 생각을 갖고 있는 개발자이며, IT 기술을 가르치는 것을 굉장히 좋아하는 교육자이다. 또한, 사용자를 행복하게 만드는 서비스를 만드는 것이 가장 중요

books.google.co.kr


  • 리액트 프로젝트에서 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초 뒤에 숫자가 변경되고 콘솔에 로그가 찍히는 것을 확인할 수 있다.

웹 요청 비동기 작업 처리하기

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의 데이터를 화면에 성공적으로 출력하는 것을 확인할 수 있다.

콘솔을 열어 확인하면 액션 발생 순서를 확인할 수 있다.

  1. GET_POST
  2. GET_USERS
  3. GET_POST_SUCESS (만약 실패일 경우 GET_POST_FAILURE 가 대신 발생)
  4. 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,
});
  • 위 코드에서 getPostcreateRequestThunk에 의해 반환된 첫 번째 함수를 실행한 결과이다.
  • 이 함수는 두 번째 화살표 함수를 반환하고, 이 함수는 params를 매개변수로 받는 비동기 함수이다.
getPost(1);
  • getPost(1)을 호출하면, getPost가 참조하는 두 번째 화살표 함수가 실행되며, 이때 1params 매개변수로 전달된다.
  • createRequestThunk에서 반환된 함수는 클로저를 형성한다.
  • 클로저란 자신이 생성될 때의 환경을 '기억'하는 함수이다.
  • 여기서 두 번째 화살표 함수는 첫 번째 화살표 함수의 typerequest 매개변수에 대한 참조를 유지한다.
  • 따라서 getPost(1)을 호출할 때, 이 함수는 typerequest에 접근할 수 있으며, 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-sagaredux-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-thunk는 일반 함수로 이루어져 있기 때문에 간단명료하다는 장점이 있고,
  • redux-saga는 진입 장벽이 조금 있을 수 있으나 복잡한 상황에서 더욱 효율적으로 작업을 관리할 수 있다는 장점이 있다.
  • 또 다른 미들웨어로는 redux-promise-middleware, redix-pender, redux-observable 등이 있다.
profile

DEVELOP

@JUNGY00N