[React] React + Redux로 상태 관리하기

이찬형·2020년 4월 2일
3
post-thumbnail
post-custom-banner

React + Redux


지난번엔 바닐라 JS에서 Redux를 이용하여 상태관리를 해봤어요.
이 친구를 React에 적용하면, class형 컴포넌트를 사용하지 않고도 각각의 파일에서 state 관리가 가능해집니다.

간단한 카운터 앱과 로그인 기능을 리덕스로 구현해볼게요!
파일 구조는 아래와 같아요.

actions

actions 폴더에 있는 친구들은 dispatch의 인자로 줄 action 객체를 생성해요.

바닐라 JS + Redux에서 배웠듯이, action 객체엔 type이란 키가 꼭 필요하죠?
type은 reducer 내부에서 switch-case 구문의 조건이 돼요.

먼저 counterActions.js를 봅시다.
increment(), decrement() 함수를 정의하고, 각각 type이 포함된 객체를 리턴해요.

src/actions/counterActions.js

const increment = () => {
  return {
    type: "INCREMENT"
  };
};

const decrement = () => {
  return {
    type: "DECREMENT"
  };
};

export default {
  increment,
  decrement
};

userActions.js는 사용자의 정보가 포함된 객체를 리턴해요.

src/actions/userActions.js

const loginUser = user => {
  return {
    type: "LOG_IN",
    user
  };
};

const logoutUser = () => {
  return {
    type: "LOG_OUT"
  };
};

export default {
  loginUser,
  logoutUser
};

index.js에서 이 친구들을 하나로 묶어줍니다.

src/actions/index.js

import counterActions from "./counterActions";
import userActions from "./userActions";

const allActions = {
  counterActions,
  userActions
};

export default allActions;

추후 allActions.counterActions.increment()로 호출하게 되면 { type: "INCREMENT" } 객체가 리턴돼요.
이 친구가 dispatch 함수의 인자로 가서 reducer에 전달된다면 상태 업데이트가 일어나겠죠?

reducers

reducers에선 상태를 생성하고 관리에 들어가요.

actions 폴더에서 생성된 객체들이 reducer 함수의 인자 action으로 들어가는거예요.

src/reducers/counterReducer.js

const counter = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
};

export default counter;

userReducer.js도 크게 다르지 않습니다.
action.type에 따라 데이터를 state에 넣어주는 역할을 해요.

대신 여기선 state의 불변성을 지켜주어야 합니다!!

src/reducers/userReducers.js

const currentUser = (state = {}, action) => {
  switch (action.type) {
    case "LOG_IN":
      return { ...state, user: action.user, login: true };
    case "LOG_OUT":
      return { ...state, user: "", login: false };
    default:
      return state;
  }
};

export default currentUser;

index.js에선 combineReducers()를 사용해 두 리듀서를 하나로 정의해줍니다.

src/reducers/index.js

import counter from "./counterReducer";
import currentUser from "./userReducer";
import { combineReducers } from "redux";

const rootReducer = combineReducers({ counter, currentUser });

export default rootReducer;

연결, 사용

이렇게 만들어진 리듀서를 하위 컴포넌트가 사용할 수 있도록 연결해야겠죠?

createStore로 저장소를 만들고 index.jsProvider를 사용해 연결합시다.

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./reducers";

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

rootReducer에 두 가지 리듀서가 합쳐져 있기 때문에 store에서 두 가지 상태에 모두 접근이 가능합니다.

이제 App.js에서 state를 사용하려면..
원래는 connectmapStateToProps와 같은 함수를 써야돼요.

이게 처음 배울 때 엄청 헷갈렸는데..
React Hooks의 등장으로 react-reduxuseSelector, useDispatch가 추가됩니다.

먼저 useSelector를 사용해볼게요.

const counter = useSelector(state => state.counter);
const currentUser = useSelector(state => state.currentUser);

useSelector 로 store에 접근하는 코드에요.
리듀서에서 만들어진 state가 할당이 됩니다.

useDispatch는 더 간단해요.

const dispatch = useDispatch();

리듀서에 action을 전해주려면 dispatch를 이용한다!! 는 사실을 기억하고 아래의 코드를 봅시다.

dispatch(allActions.counterActions.increment());

위에서 언급한 것처럼 allActions.counterActions.increment()는 객체를 반환해요.

()를 넣어주었으니 함수가 즉시 실행될 것이고,
결국 { type: "INCREMENT" } 가 dispatch 함수의 인자로 들어가는 거예요.

dispatch({ type: "INCREMENT" });

이제 이 친구는 createStore에서 인자로 준 rootReducer를 뒤져서 해당하는 타입을 찾아요.
src/reducers/counterReducer.js를 다시 볼까요?

src/reducers/counterReducer.js

const counter = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
      .
      .
   /* 다른 case문 */
  }
}

따라서 state를 하나 증가시키고 값을 저장합니다.

때문에 리렌더링이 이루어지고, App.js의 counter 변수가 변하게 돼요.

이렇게 리덕스로 상태관리가 가능하게 됩니다!!

src/App.js

import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import allActions from "./actions";

const App = () => {
  const counter = useSelector(state => state.counter);
  const currentUser = useSelector(state => state.currentUser);

  const dispatch = useDispatch();

  const name = "Lee";

  useEffect(() => {
    dispatch(allActions.userActions.loginUser(name));
  }, []);

  return (
    <div>
      {currentUser.login ? (
        <div>
          <div>Hello, {currentUser.user}</div>
          <button onClick={() => dispatch(allActions.userActions.logoutUser())}>
            Log Out
          </button>
        </div>
      ) : (
        <div>
          <div>Login</div>
          <button
            onClick={() => dispatch(allActions.userActions.loginUser(name))}
          >
            Login Lee
          </button>
        </div>
      )}
      <h1>{counter}</h1>
      <button onClick={() => dispatch(allActions.counterActions.increment())}>
        +1
      </button>
      <button onClick={() => dispatch(allActions.counterActions.decrement())}>
        -1
      </button>
    </div>
  );
};

export default App;

잘 작동합니다!!

thunk

만약 비동기 요청을 state에 담아야 한다면 어떻게 할까요??

useEffect() 안에 비동기로 데이터를 받는 함수를 작성한 다음 바로 호출하는 방법도 있겠지만..
actions / reducers 폴더를 활용하면 더 좋을 것 같습니다.

리덕스로 비동기 로직을 처리해야 한다면, 우리는 미들웨어의 도움을 받아야 합니다.

대표적인 것으로 redux-thunk 가 있어요.

이 친구를 사용하면 action 객체를 가로채서 비동기 작업을 기다린 후 다시 전송이 가능하답니다.

우선 설치를 해줍시다!!

npm install redux-thunk --save

그 다음 store를 생성하는 부분도 수정해줄게요.

src/index.js

.
.
import thunk from "redux-thunk";
import { createStore, applyMiddleware } from "redux";

const store = createStore(rootReducer, applyMiddleware(thunk));
.
.

이제 비동기로 데이터를 받아와봅시다.
영화 정보를 제공하는 API를 사용할게요.

src/actions/popularActions.js

const fetchPopularMovies = data => {
  return {
    type: "FETCH_POPULAR",
    data
  };
};

const fetchingPopularMovies = () => {
  return dispatch => {
    return axios
      .get(
        `https://api.themoviedb.org/3/movie/popular?api_key=${API_KEY}&language=en-US&page=1`
      )
      .then(response => {
        dispatch(fetchPopularMovies(response.data.results));
      })
      .catch(error => {
        throw error;
      });
  };
};

export default { fetchingPopularMovies, fetchPopularMovies };

fetchingPopularMovies() 에서 thunk가 사용되었어요.
dispatch를 잡았다가 axios 요청이 끝나면 fetchPopularMovies() 에 인자로 보내줍니다.

리듀서도 작성할게요.

src/reducers/popularReducer.js

const popularMovies = (state = [], action) => {
  switch (action.type) {
    case "FETCH_POPULAR":
      return [...state, ...action.data];
    default:
      return state;
  }
};

export default popularMovies;

이 친구들을 각각 폴더의 index.js에 추가해주면 됩니다!!

App.js에선 이렇게 사용해요.

src/App.js

const popularMovies = useSelector(state => state.popularMovies);

useEffect(() => {
    dispatch(allActions.popularActions.fetchingPopularMovies());
  }, []);

action을 보시면 thunk 부분을 연결해줘요.
thunk가 있는 fetchingPopularMovies() 에선 dispatch를 잡고, 비동기를 수행한 후 액션 생성자로 넘겨줍니다.

마무리


컨테이너 - 프레젠터 패턴에서 벗어나, 저장소를 만들고 필요한 컴포넌트에서 꺼내 쓸 수 있도록 만들었어요.

부모 컴포넌트가 멀어질수록 props로 넘겨야 하는 횟수가 많아지는데 redux를 사용하면 한 번에 접근이 가능하니 편리해졌네요.

코드 패턴에 조금 더 익숙해진 후 Netflix 클론코딩을 해보려고 합니다.

감사합니다 :D

profile
WEB / Security
post-custom-banner

0개의 댓글