React-12 Redux-Thunk (23/03/15)

nazzzo·2023년 3월 15일
0
post-custom-banner

Redux & Redux-Thunk



1. 리액트 초기세팅



먼저 오늘 필요한 패키지들입니다

설치

npx create-react-app .
npm install react-router-dom redux redux-thunk react-redux axios styled-components
npm install -D redux-devtools-extension



간단한 백엔드 서버부터 먼저 만들어보겠습니다


Backend

  • CORS 처리에 특히 주의!

[server.js]

npm install express cors
const express = require("express");
const app = express();
const cors = require("cors");

app.use(cors({
    origin: true,
    credintials: true,
}));
app.use(express.json());
app.use((req, res, next) => {
  console.log(req.method, req.path, req.body)
  next()
});

// axios 요청에 대해 응답할 데이터
app.get("/categories", (req, res) => {
  res.json([
    { path: "/", name: "Home" },
    { path: "/counter", name: "counter" },
  ]);
});

app.listen(3005, () => {
  console.log(`backend server on 3005`);
});



서버 설정이 끝났으면 다음은 프론트 차례

리액트 프로젝트는 항상 라우터 설정부터 시작합니다

BrowserRouter
	Routes
    	Route
        	NavLinkr

[./index.jsx]

import { BrowserRouter } from 'react-router-dom'


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
  • BrowserRouter는 따로 분리해서 index의 최상단 컴포넌트로(App 컴포넌트를 감싸는 형태) 두는 것을 추천

[./src/routes/AppRouter.jsx]

import { Routes, Route } from 'react-router-dom'

export const AppRouter = () => {
    return (
        <Routes>
            <Route path="/" element={<Main />}></Route>
            <Route path="/counter" element={<Counter />}></Route>
        </Routes>
    )
}



다음으로 Header 컴포넌트를 만들어서 <NavLink>의 동작을 확인합니다

[./src/common/header/Header.jsx]

import { NavLink } from "react-router-dom"

export const Header = () => {
    return <ul>
        <li><NavLink to={"/"}>Main</NavLink></li>
        <li><NavLink to={"/counter"}>Counter</NavLink></li>
    </ul>
}



2. react-redux


  1. store 디렉토리를 생성하고 초기 세팅을 시작합니다
  • Provider, store, rootReducer

[./src/store/Store.jsx]

  • createStore() 메서드는 redux에서 불러옵니다
import { createStore } from "redux";
const rootReducer = () => {
    return { name: "web7722" }
}
export const store = createStore(rootReducer);

[./index.jsx]

  • Providerreact-redux에서 불러옵니다
import { Provider } from "react-redux";
import { store } from "./store" 
import { BrowserRouter } from "react-router-dom";

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


  1. rootReducer 쪼개기 & 초기 설정

[./src/store/rootReducer.jsx]

  • combineReducers의 인자로 빈객체를 넣은 것은 에러 방지를 위해서...
import { combineReducers } from "redux"
export const rootReducer = combineReducers({})



  • redux devtools를 설치합니다 (크롬 브라우저 익스텐션)
npm install -D redux-devtools-extension

[./src/store/Store.jsx]

import { createStore } from "redux";
import { rootReducer } from "./rootReducer"
import { composeWithDevTools } from 'redux-devtools-extension'

export const store = createStore(rootReducer, composeWithDevTools());



  1. 초기 상태값 설정하기

먼저 category 디렉토리를 생성하겠습니다


[./src/store/category/reducer.jsx]

import {
  CATEGORY_REQUEST_START,
  CATEGORY_REQUEST_SUCCESS,
  CATEGORY_REQUEST_ERROR,
} from "./types";

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

export const category = (state = initialState, action) => {
  switch (action.type) {
    case CATEGORY_REQUEST_START:
      return { ...state };
    case CATEGORY_REQUEST_SUCCESS:
      return { ...state, loading: false, error: null, data: action.payload };
    case CATEGORY_REQUEST_ERROR:
      return { ...state };
    default:
      return state;
  }
};

[./src/store/rootReducer.jsx]

import { combineReducers } from "redux";
import { category } from "./category";

export const rootReducer = combineReducers({
  category: category,
});


  1. 미들웨어 장착하기 ~ redux-thunk

[./src/store/rootReducer.jsx]

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { rootReducer } from "./rootReducer";
import { composeWithDevTools } from "redux-devtools-extension";

export const store = createStore(
  rootReducer,
  // 불러온 thunk를 인자로 장착
  composeWithDevTools(applyMiddleware(thunk))
);

미들웨어의 용도는 상태를 바꾸기 전에 요청 내용을 조작할 함수를 끼워넣기 위함



  1. Axios 요청하기

비동기요청으로 서버에 있는 카테고리 배열 데이터를 가져오는 것이 목표힙니다


먼저 axios 요청을 위한 사전세팅부터

[./src/utils/request.js]


import axios from "axios";

const request = axios.create({
  baseURL: "http://localhost:3005",
  withCredentials: true,
  headers: {
    "Content-type": "application/json",
  },
});

export default request

액션과 리듀서는 다음과 같이 작성합니다

[./src/store/category/actions.jsx]

import {
  CATEGORY_REQUEST_SUCCESS,
  CATEGORY_REQUEST_ERROR,
  CATEGORY_REQUEST_START,
} from "./types";
import request from "../../utils/request";

export const RequestSuccess = (payload) => ({
  type: CATEGORY_REQUEST_SUCCESS,
  payload,
});

export const RequestError = (payload) => ({
  type: CATEGORY_REQUEST_ERROR,
  payload,
});

// 고차함수의 첫번째 인자로는 props값을 전달받을 수 있습니다 
// 지금 코드에서는 사용할 필요가 없지만 props를 전달받아야 할 상황을 대비
export const CategoryRequest = (props) => {
  return async (dispatch) => {
    // request start
    dispatch({ type: CATEGORY_REQUEST_START });
    try {
      const response = await request.get("/categories");
      console.log(response);
      // request success
      dispatch(RequestSuccess(response.data));
    } catch (e) {
      // request error
      dispatch(RequestError(e));
    }
  };
};


// then() 함수를 사용해서 처리하는 방법. 위와 같은 코드입니다
// 둘 중 하나만 써야지 await, then 혼용하면 안돼요!!
// export const CategoryRequest = (props) => (dispatch) => {
//     dispatch({ type: CATEGORY_REQUEST_START });
//     request.get("/categories")
//     .then(({data}) => dispatch(RequestSuccess(data)))
//     .catch(error => dispatch(RequestError(error)))
// }

[./src/store/category/reducer.jsx]

import {
  CATEGORY_REQUEST_START,
  CATEGORY_REQUEST_SUCCESS,
  CATEGORY_REQUEST_ERROR,
} from "./types";

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

export const category = (state = initialState, action) => {
  switch (action.type) {
    case CATEGORY_REQUEST_START:
      return { ...state, loading: true, error: null };
    case CATEGORY_REQUEST_SUCCESS:
      return { ...state, loading: false, error: null, data: action.payload };
    case CATEGORY_REQUEST_ERROR:
      return { ...state, loading: false, error: action.payload.message };
    default:
      return state;
  }
};

[App.jsx]

import { useEffect } from "react";
import { AppRouter } from "./routes";
import { Header } from "./common";
import { useDispatch, useSelector } from "react-redux";
import { RequestSuccess } from "./store";

const App = () => {
  // 고차함수 형태
  const dispatch = useDispatch();
  // 내가 원하는 상태값을 인자로 가져올 수 있습니다 (state는 전체 객체, 그 중에서 category만)
  const { loading, error, data } = useSelector((state) => state.category);

  useEffect(() => {
    const payload = [
      { path: "/", name: "Home" },
      { path: "/counter", name: "Counter" },
    ];

    // dispatch 액션 함수를 따로 만들 것인지는 선택...
    dispatch(RequestSuccess(payload));
  }, [dispatch]);

  // 스켈레톤... 생명주기 메서드를 사용해서 false값으로 바꿔야 합니다
  if (loading) return <>로딩중...</>;
  if (error) return <>{error}</>;

  return (
    <>
      <Header items={data} />
      <AppRouter />
    </>
  );
};

export default App;



post-custom-banner

0개의 댓글