Redux로 상태관리하기 - Redux Advanced (1) - 1

lbr·2022년 8월 16일
0

개요

진행순서

2-1) Async Action with Redux
(redux에서 비동기 액션처리(우리가 알고 있는 방식으로 접근해보기))
2-2) 리덕스 미들웨어
(2-1을 발전시켜서 미들웨어를 적용)
2-3) redux-devtools

2-4) redux-thunk

2-5) redux-promise-middleware

redux에서 비동기 처리를 위해서 어떻게 action을 처리하는지 알아보겠습니다.
redux 학습 때 사용했던 app을 이어서 계속 사용하겠습니다.

실습

github의 api를 이용하여 유저정보를 가져오는 것을 해보겠습니다.

redux의 combineReducers와
react-redux의 useSelector와
react-redux의 Provider 를 사용하겠습니다.

https://api.github.com/users

위 주소로 get방식으로 가져오면 배열을 가지고 올 수 있습니다.
이것을 가져오기 위해서 axios를 설치하고, 우리의 컴포넌트에 user를 관리할 수 있는 user에 대한 state를 작성하겠습니다.

먼저 reducer에 users를 추가하기 위해서는 크게 3가지를 해주어야 합니다.
1. combineReducers에 users를 추가하고,
2. reducer에 user 처리로직 작성.
3. action 정의.

// reducers/reducer.js
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";

const reducer = combineReducers({
  todos: todos,
  filter: filter,
  users, // users를 추가합니다.
});

export default reducer;
// reducers/users.js
import { GET_USERS_FAIL, GET_USERS_START, GET_USERS_SUCCESS } from "../actions";

const initialState = {
  loading: false,
  data: [],
  error: null,
};

export default function users(state = initialState, action) {
  if(action.type === GET_USERS_START) {
    return {
      ...state,
      loading: true,
      error: null,
    };
  }

  if(action.type === GET_USERS_SUCCESS) {
    return {
      ...state,
      loading: false,
      data: action.data,
    };
  }

  if(action.type === GET_USERS_FAIL) {
    return {
      ...state,
      loading: false,
      error: action.error,
    };
  }

  return state;
}

user에 대한 reducer를 정의했습니다.

// actions.js
// users에 대한 액션 정의

// github api 호출을 시작하는 것을 의미합니다.
export const GET_USERS_START = "GET_USERS_START"; // 로딩시작

// github api 호출에 대한 응답이 성공적으로 돌아온 경우.
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS"; // 로딩 끝내고 데이터를 세팅합니다.

// github api 호출에 대한 응답이 실패한 경우.
export const GET_USERS_FAIL = "GET_USERS_FAIL"; // 로딩 끝내고 에러를 세팅합니다.

export function getUsersStart() {
  return {
    type: GET_USERS_START,
  };
}

export function getUsersSuccess(data) {
  return {
    type: GET_USERS_SUCCESS,
    data,
  };
}

export function getUsersFail(error) {
  return {
    type: GET_USERS_FAIL,
    error,
  };
}

user에 대한 액션을 정의했습니다.

이제 유저목록을 보여줄 수 있는 컴포넌트와 컨테이너를 만들겠습니다. (아래)

// UserList.jsx
export default function UserList({ users }) {
  if(users.length === 0) {
    return <p>현재 유저 정보 없음</p>;
  }
  
  return (
    <ul>
      {users.map((user) => (
        <li>{JSON.stringify(user)}</li>
      ))}
    </ul>
  );
}
// userListContainer.jsx
import UserList from "../components/UserList";
import { useSelector } from "react-redux";

export default function UserListContainer() {
  const users = useSelector((state) => state.users.data);

  return <UserList users={users} />;
}

만든 컴포넌트를 App.js에 추가합니다.

import "./App.css";
import TodoListContainer from "./containers/TodoListContainer";
import TodoFormContainer from "./containers/TodoFormContainer";
import UserListContainer from "./containers/UserListContainer";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <UserListContainer />
        <TodoListContainer />
        <TodoFormContainer />
      </header>
    </div>
  );
}

이제 API를 호출해보겠습니다.

API 호출 코드 작성하기

api 호출을 위해 라이브러리를 설치합니다.

npm i axios

API 호출은 보통 componentDidMount 시점에 해야합니다.

UserList.jsx

// components/UserList.jsx useEffect() 부분

// ...
  useEffect(() => {
    // await를 사용하기 위해 async가 필요합니다. async를 사용하기 위해 함수를 하나 만듭니다.
    async function getUsers() {
      try { 
        // 1. 시작한다.
        const res = await axios.get('https://api.github.com/users');
        // 2. 성공했다.
      } catch(error) {
  		// 3. 실패했다.
      }
    }
  }, []);
// ...

위까지 작성해 놓고 보면, 이제 redux의 dispatch로 시작한다 성공했다 실패했다 를 전달할 준비가 되었습니다.

시작한다 성공했다 실패했다를 함수를 만들어서 받아와야합니다.

// components/UserList.jsx

import { useEffect } from "react";
import axios from "axios";

export default function UserList({ users, start, success, fail }) {
  useEffect(() => {
    // await를 사용하기 위해 async가 필요합니다. async를 사용하기 위해 함수를 하나 만듭니다.
    async function getUsers() {
      try {
        start();
        const res = await axios.get("https://api.github.com/users");
        success(res.data); // response를 성공적으로 받았으면..
      } catch (error) {
        fail(error);
      }
    }
    getUsers(); // 만들어진 getUsers함수를 실행합니다.
  }, [start, success, fail]); // 이 3개의 디펜던시는 이제 쉽게 바뀌지 않도록 잘 처리해야 합니다.

  if (users.length === 0) {
    return <p>현재 유저 정보 없음</p>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.login}</li>
      ))}
    </ul>
  );
}
// UserListContainer.jsx

import UserList from "../components/UserList";
import { useSelector, useDispatch } from "react-redux";
import { useCallback } from "react";
import { getUsersStart, getUsersSuccess, getUsersFail } from "../redux/actions";

export default function UserListContainer() {
  const users = useSelector((state) => state.users.data);
  const dispatch = useDispatch();

  const start = useCallback(() => {
    // dispatch를 받아와서 수행해야 합니다.
    dispatch(getUsersStart());
  }, [dispatch]);

  const success = useCallback(
    (data) => {
      // dispatch를 받아와서 수행해야 합니다.
      dispatch(getUsersSuccess(data));
    },
    [dispatch]
  );

  const fail = useCallback(
    (error) => {
      // dispatch를 받아와서 수행해야 합니다.
      dispatch(getUsersFail(error));
    },
    [dispatch]
  );

  return <UserList users={users} start={start} success={success} fail={fail} />;
}

여기까지 제일 기본적인 방식으로 비동기 작업을 했습니다.

여기까지 정리

비동기 작업을 어디서 하느냐? 가 제일 중요합니다.
우리는 프리젠테이션 컴포넌트에서 했습니다.

그래서 프리젠테이션 컴포넌트가 많이 복잡해 졌습니다.
예를들어 액션을 분리해야 합니다. Start, Success, Fail ...등등
그리고 이 각각의 액션을 dispatch해주어야합니다. 왜냐하면 reducer는 동기적인 처리방식으로 작성을 해야하기 때문입니다.
Pure 함수여야하기 때문에 그 안에서 api를 호출하는 등의 로직을 넣을 수 없습니다.

  • 당연히 reducer는 동기적인 것 => Pure
  • 마찬가지로 dispatch 도 동기적인 것

이런식으로 작업을 하면 컴포넌트가 복잡해지고, 컴포넌트 안에 뷰와 상관없는 로직이 계속 끼어들게 됩니다.

여기서는 추가적인 라이브러리를 사용하지 않고, 이 비동기작업을 컴포넌트에서 컨테이너로 옮기도록 하겠습니다.

비동기작업을 컴포넌트에서 컨테이너로 옮기기

UserList.js 에서

async function getUsers() {
  try {
    start();
    const res = await axios.get("https://api.github.com/users");
    success(res.data); // response를 성공적으로 받았으면..
  } catch (error) {
    fail(error);
  }
}

윗 부분이 비동기 작업입니다.

여기서 start와 success와 fail을 각각 만들어서 보냈지만, 이 로직들을 하나로 만들고, getUsers함수를 전달하는 방식으로 바꾸겠습니다.

// UserList.jsx

import { useEffect } from "react";

export default function UserList({ users, getUsers }) {
  useEffect(() => {
    getUsers();
  }, [getUsers]);

  if (users.length === 0) {
    return <p>현재 유저 정보 없음</p>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.login}</li>
      ))}
    </ul>
  );
}
// containers/UserListContainer.jsx

import UserList from "../components/UserList";
import { useSelector, useDispatch } from "react-redux";
import { useCallback } from "react";
import { getUsersStart, getUsersSuccess, getUsersFail } from "../redux/actions";
import axios from "axios";

export default function UserListContainer() {
  console.log("UserListContainer start");
  const users = useSelector((state) => state.users.data);
  const dispatch = useDispatch();

  const getUsers = useCallback(async () => {
    try {
      dispatch(getUsersStart());
      const res = await axios.get("https://api.github.com/users");
      dispatch(getUsersSuccess(res.data));
    } catch (error) {
      dispatch(getUsersFail(error));
    }
  }, [dispatch]);

  return <UserList users={users} getUsers={getUsers} />;
}

여기까지 정리

비동기 로직을 컴포넌트 안에서 하면 테스트하기도 어렵고, 뷰와 관련없는 로직이 추가되기 때문에 디버깅하기가 복잡할 수 있습니다. 그래서 이 컴포넌트는 props를 받아서 보여주는 용도로만 사용하고, 비동기 함수는 container에서 함수 한 벌로 만들면 깔끔한 코드를 만들 수 있고, 역할에 맞는 코드가 작성이 됩니다.

지금까지 middleware를 사용하지않고 redux에서 비동기처리를 하는 방법에 대해서 알아봤습니다.

0개의 댓글