๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

โœ๏ธ/React

React ๊ณต๋ถ€ํ•˜๊ธฐ(7) - redux-thunk of ๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด

4. redux-thunk : "์•ก์…˜ ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ ํ•จ์ˆ˜๋ฅผ ๋””์ŠคํŒจ์น˜ ํ•  ์ˆ˜ ์žˆ๋‹ค."

=> ํ•จ์ˆ˜๋ฅผ ๋””์ŠคํŒจ์น˜ ํ•  ๋•Œ์—๋Š”, ํ•ด๋‹น ํ•จ์ˆ˜์—์„œ dispatch์™€ getState๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„์™€์ฃผ์–ด์•ผ ํ•œ๋‹ค. ์ด ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•จ์ˆ˜๋ฅผ thunk๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

 

const getComments = () => (dispatch, getState) => {
  // ์ด ์•ˆ์—์„œ๋Š” ์•ก์…˜์„ dispatch ํ•  ์ˆ˜๋„ ์žˆ๊ณ 
  // getState๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ์ƒํƒœ๋„ ์กฐํšŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  const id = getState().post.activeId;

  // ์š”์ฒญ์ด ์‹œ์ž‘ํ–ˆ์Œ์„ ์•Œ๋ฆฌ๋Š” ์•ก์…˜
  dispatch({ type: 'GET_COMMENTS' });

  // ๋Œ“๊ธ€์„ ์กฐํšŒํ•˜๋Š” ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” getComments ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ด…์‹œ๋‹ค.
  api
    .getComments(id) // ์š”์ฒญ์„ ํ•˜๊ณ 
    .then(comments => dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments })) // ์„ฑ๊ณต์‹œ
    .catch(e => dispatch({ type: 'GET_COMMENTS_ERROR', error: e })); // ์‹คํŒจ์‹œ
};

(async/await)

const getComments = () => async (dispatch, getState) => {
  const id = getState().post.activeId;
  dispatch({ type: 'GET_COMMENTS' });
  try {
    const comments = await api.getComments(id);
    dispatch({ type:  'GET_COMMENTS_SUCCESS', id, comments });
  } catch (e) {
    dispatch({ type:  'GET_COMMENTS_ERROR', error: e });
  }
}

 

$ yarn add redux-thunk
import ReduxThunk from 'redux-thunk';

<index.js>

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';

const store = createStore(
  rootReducer,
  // logger ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, logger๊ฐ€ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•ฉ๋‹ˆ๋‹ค.
  composeWithDevTools(applyMiddleware(ReduxThunk, logger))
); // ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

serviceWorker.unregister();

<modules/counter.js>

// ์•ก์…˜ ํƒ€์ž…
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';

// ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

// thunk ํ•จ์ˆ˜
// getState๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๊ตณ์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์•„์˜ฌ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.
export const increaseAsync = () => dispatch => {
  setTimeout(() => dispatch(increase()), 1000);
};
export const decreaseAsync = () => dispatch => {
  setTimeout(() => dispatch(decrease()), 1000);
};

// ์ดˆ๊นƒ๊ฐ’ (์ƒํƒœ๊ฐ€ ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ์ˆซ์ž์—ฌ๋„ ์ƒ๊ด€ ์—†์Šต๋‹ˆ๋‹ค.)
const initialState = 0;

export default function counter(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return state + 1;
    case DECREASE:
      return state - 1;
    default:
      return state;
  }
}

<containers/CounterContainer.js>

import React from 'react';
import Counter from '../components/Counter';
import { useSelector, useDispatch } from 'react-redux';
import { increaseAsync, decreaseAsync } from '../modules/counter';

function CounterContainer() {
  const number = useSelector(state => state.counter);
  const dispatch = useDispatch();

  const onIncrease = () => {
    dispatch(increaseAsync());
  };
  const onDecrease = () => {
    dispatch(decreaseAsync());
  };

  return (
    <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
  );
}

export default CounterContainer;

 

 

5. redux-thunk๋กœ ํ”„๋กœ๋ฏธ์Šค ๋‹ค๋ฃจ๊ธฐ

6. API ์žฌ๋กœ๋”ฉ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ

 

๊ฐ€์งœ API ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ

<api/posts.js>

// n ๋ฐ€๋ฆฌ์„ธ์ปจ๋“œ๋™์•ˆ ๊ธฐ๋‹ค๋ฆฌ๋Š” ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ•จ์ˆ˜
const sleep = n => new Promise(resolve => setTimeout(resolve, n));

// ๊ฐ€์งœ ํฌ์ŠคํŠธ ๋ชฉ๋ก ๋ฐ์ดํ„ฐ
const posts = [
  {
    id: 1,
    title: '๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋ฐฐ์›Œ๋ด…์‹œ๋‹ค',
    body: '๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋ฉด ์ดํ•ดํ•˜๊ธฐ ์‰ฝ์ฃ .'
  },
  {
    id: 2,
    title: 'redux-thunk๋ฅผ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค',
    body: 'redux-thunk๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ด๋ด…์‹œ๋‹ค!'
  },
  {
    id: 3,
    title: 'redux-saga๋„ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค',
    body:
      '๋‚˜์ค‘์—” redux-saga๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ๋ฐฐ์›Œ๋ณผ ๊ฑฐ์˜ˆ์š”.'
  }
];

// ํฌ์ŠคํŠธ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜
export const getPosts = async () => {
  await sleep(500); // 0.5์ดˆ ์‰ฌ๊ณ 
  return posts; // posts ๋ฐฐ์—ด
};

// ID๋กœ ํฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜
export const getPostById = async id => {
  await sleep(500); // 0.5์ดˆ ์‰ฌ๊ณ 
  return posts.find(post => post.id === id); // id ๋กœ ์ฐพ์•„์„œ ๋ฐ˜ํ™˜
};

 

posts ๋ฆฌ๋•์Šค ๋ชจ๋“ˆ ์ค€๋น„ํ•˜๊ธฐ

  1. ํ”„๋กœ๋ฏธ์Šค๊ฐ€ ์‹œ์ž‘, ์„ฑ๊ณต, ์‹คํŒจํ–ˆ์„๋•Œ ๋‹ค๋ฅธ ์•ก์…˜์„ ๋””์ŠคํŒจ์น˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
  2. ๊ฐ ํ”„๋กœ๋ฏธ์Šค๋งˆ๋‹ค thunk ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  3. ๋ฆฌ๋“€์„œ์—์„œ ์•ก์…˜์— ๋”ฐ๋ผ ๋กœ๋”ฉ์ค‘, ๊ฒฐ๊ณผ, ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

<modules/posts.js>

import * as postsAPI from '../api/posts'; // api/posts ์•ˆ์˜ ํ•จ์ˆ˜ ๋ชจ๋‘ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
import {
  createPromiseThunk,
  reducerUtils,
  handleAsyncActions,
  createPromiseThunkById,
  handleAsyncActionsById
} from '../lib/asyncUtils';

/* ์•ก์…˜ ํƒ€์ž… */

// ํฌ์ŠคํŠธ ์—ฌ๋Ÿฌ๊ฐœ ์กฐํšŒํ•˜๊ธฐ
const GET_POSTS = 'GET_POSTS'; // ์š”์ฒญ ์‹œ์ž‘
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // ์š”์ฒญ ์„ฑ๊ณต
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // ์š”์ฒญ ์‹คํŒจ

// ํฌ์ŠคํŠธ ํ•˜๋‚˜ ์กฐํšŒํ•˜๊ธฐ
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';

// ์•„์ฃผ ์‰ฝ๊ฒŒ thunk ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunkById(GET_POST, postsAPI.getPostById);

// initialState ์ชฝ๋„ ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ initial() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฆฌํŒฉํ† ๋ง ํ–ˆ์Šต๋‹ˆ๋‹ค.
const initialState = {
  posts: reducerUtils.initial(),
  post: {}
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
    case GET_POSTS_SUCCESS:
    case GET_POSTS_ERROR:
      return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
    case GET_POST:
    case GET_POST_SUCCESS:
    case GET_POST_ERROR:
      return handleAsyncActionsById(GET_POST, 'post')(state, action);
    default:
      return state;
  }
}

 

๋ฆฌ๋•์Šค ๋ชจ๋“ˆ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

<lib/asyncUtils.js> createPromiseThunk, createPromiseThunkById ๋งŒ๋“ค๊ธฐ

// ํŠน์ • id ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” Thunk ์ƒ์„ฑํ•จ์ˆ˜
const defaultIdSelector = param => param;
export const createPromiseThunkById = (
  type,
  promiseCreator,
  // ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ id ๋ฅผ ์–ด๋–ป๊ฒŒ ์„ ํƒ ํ•  ์ง€ ์ •์˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
  // ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ id๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  // ํ•˜์ง€๋งŒ ๋งŒ์•ฝ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ { id: 1, details: true } ์ด๋Ÿฐ ํ˜•ํƒœ๋ผ๋ฉด
  // idSelector ๋ฅผ param => param.id ์ด๋Ÿฐ์‹์œผ๋กœ ์„ค์ • ํ•  ์ˆ˜ ์žˆ๊ณ˜์ฃ .
  idSelector = defaultIdSelector
) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];

  return param => async dispatch => {
    const id = idSelector(param);
    dispatch({ type, meta: id });
    try {
      const payload = await promiseCreator(param);
      dispatch({ type: SUCCESS, payload, meta: id });
    } catch (e) {
      dispatch({ type: ERROR, error: true, payload: e, meta: id });
    }
  };
};


// ๋ฆฌ๋“€์„œ์—์„œ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ์œ ํ‹ธ ํ•จ์ˆ˜๋“ค์ž…๋‹ˆ๋‹ค.
export const reducerUtils = {
  // ์ดˆ๊ธฐ ์ƒํƒœ. ์ดˆ๊ธฐ data ๊ฐ’์€ ๊ธฐ๋ณธ์ ์œผ๋กœ null ์ด์ง€๋งŒ
  // ๋ฐ”๊ฟ€ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
  initial: (initialData = null) => ({
    loading: false,
    data: initialData,
    error: null
  }),
  // ๋กœ๋”ฉ์ค‘ ์ƒํƒœ. prevState์˜ ๊ฒฝ์šฐ์—” ๊ธฐ๋ณธ๊ฐ’์€ null ์ด์ง€๋งŒ
  // ๋”ฐ๋กœ ๊ฐ’์„ ์ง€์ •ํ•˜๋ฉด null ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ๊ฐ’์„ ์œ ์ง€์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  loading: (prevState = null) => ({
    loading: true,
    data: prevState,
    error: null
  }),
  // ์„ฑ๊ณต ์ƒํƒœ
  success: payload => ({
    loading: false,
    data: payload,
    error: null
  }),
  // ์‹คํŒจ ์ƒํƒœ
  error: error => ({
    loading: false,
    data: null,
    error: error
  })
};

// id๋ณ„๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์œ ํ‹ธํ•จ์ˆ˜
export const handleAsyncActionsById = (type, key, keepData = false) => {
  const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
  return (state, action) => {
    const id = action.meta;
    switch (action.type) {
      case type:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.loading(
              // state[key][id]๊ฐ€ ๋งŒ๋“ค์–ด์ ธ์žˆ์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ๊นŒ ์œ ํšจ์„ฑ์„ ๋จผ์ € ๊ฒ€์‚ฌ ํ›„ data ์กฐํšŒ
              keepData ? state[key][id] && state[key][id].data : null
            )
          }
        };
      case SUCCESS:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.success(action.payload)
          }
        };
      case ERROR:
        return {
          ...state,
          [key]: {
            ...state[key],
            [id]: reducerUtils.error(action.payload)
          }
        };
      default:
        return state;
    }
  };
};

<modules/posts.js>

import * as postsAPI from '../api/posts'; // api/posts ์•ˆ์˜ ํ•จ์ˆ˜ ๋ชจ๋‘ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
import {
  createPromiseThunk,
  reducerUtils,
  handleAsyncActions
} from '../lib/asyncUtils';

/* ์•ก์…˜ ํƒ€์ž… */

// ํฌ์ŠคํŠธ ์—ฌ๋Ÿฌ๊ฐœ ์กฐํšŒํ•˜๊ธฐ
const GET_POSTS = 'GET_POSTS'; // ์š”์ฒญ ์‹œ์ž‘
const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // ์š”์ฒญ ์„ฑ๊ณต
const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // ์š”์ฒญ ์‹คํŒจ

// ํฌ์ŠคํŠธ ํ•˜๋‚˜ ์กฐํšŒํ•˜๊ธฐ
const GET_POST = 'GET_POST';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_ERROR = 'GET_POST_ERROR';

// ํฌ์ŠคํŠธ ๋น„์šฐ๊ธฐ
const CLEAR_POST = 'CLEAR_POST';

// ์•„์ฃผ ์‰ฝ๊ฒŒ thunk ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);

export const clearPost = () => ({ type: CLEAR_POST });

// 3๋ฒˆ์งธ ์ธ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด withExtraArgument ์—์„œ ๋„ฃ์–ด์ค€ ๊ฐ’๋“ค์„ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
export const goToHome = () => (dispatch, getState, { history }) => {
  history.push('/');
};

// initialState ์ชฝ๋„ ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ initial() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฆฌํŒฉํ† ๋ง ํ–ˆ์Šต๋‹ˆ๋‹ค.
const initialState = {
  posts: reducerUtils.initial(),
  post: reducerUtils.initial()
};

export default function posts(state = initialState, action) {
  switch (action.type) {
    case GET_POSTS:
    case GET_POSTS_SUCCESS:
    case GET_POSTS_ERROR:
      const postsReducer = handleAsyncActions(GET_POSTS, 'posts');
      return postsReducer(state, action);
    case GET_POST:
    case GET_POST_SUCCESS:
    case GET_POST_ERROR:
      const postReducer = handleAsyncActions(GET_POSTS, 'posts');
      return postReducer(state, action);
    case CLEAR_POST:
      return {
        ...state,
        post: reducerUtils.initial()
      };
    default:
      return state;
  }
}

<modules/index.js>

import { combineReducers } from 'redux';
import counter from './counter';
import posts from './posts';

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

export default rootReducer;

 

 

ํฌ์ŠคํŠธ ๋ชฉ๋ก ๊ตฌํ˜„ํ•˜๊ธฐ

<components/PostList.js>

import React from 'react';

function PostList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          {post.title}
        </li>
      ))}
    </ul>
  );
}

export default PostList;

<containers/PostListContainer.js>

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PostList from '../components/PostList';
import { getPosts } from '../modules/posts';

function PostListContainer() {
  const { data, loading, error } = useSelector(state => state.posts.posts);
  const dispatch = useDispatch();

  // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ํ›„ ํฌ์ŠคํŠธ ๋ชฉ๋ก ์š”์ฒญ
  useEffect(() => {
    if (data) return; // API ๋กœ๋”ฉ ๋ฌธ์ œ ํ•ด๊ฒฐ
    dispatch(getPosts());
  }, [data, dispatch]);

  if (loading) return <div>๋กœ๋”ฉ์ค‘...</div>;
  if (error) return <div>์—๋Ÿฌ ๋ฐœ์ƒ!</div>;
  if (!data) return null;
  return <PostList posts={data} />;
}

export default PostListContainer;

<App.js>

import React from 'react';
import PostListContainer from './containers/PostListContainer';

function App() {
  return <PostListContainer />;
}

export default App;

 

๋ฆฌ์•กํŠธ ๋ผ์šฐํ„ฐ ์ ์šฉํ•˜๊ธฐ

$ yarn add react-router-dom

<index.js>

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { BrowserRouter } from 'react-router-dom';

const store = createStore(
  rootReducer,
  // logger ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, logger๊ฐ€ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•ฉ๋‹ˆ๋‹ค.
  composeWithDevTools(applyMiddleware(ReduxThunk, logger))
); // ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

ํฌ์ŠคํŠธ ์กฐํšŒํ•˜๊ธฐ

<compontents/Post.js>

import React from 'react';

function Post({ post }) {
  const { title, body } = post;
  return (
    <div>
      <h1>{title}</h1>
      <p>{body}</p>
    </div>
  );
}

export default Post;

<containers/PostContainer.js>

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPost, goToHome } from '../modules/posts';
import Post from '../components/Post';

function PostContainer({ postId }) {
  const { data, loading, error } = useSelector(
    state => state.posts.post[postId]
  ) || {
    loading: false,
    data: null,
    error: null
  }; // ์•„์˜ˆ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ, ๋น„๊ตฌ์กฐํ™” ํ• ๋‹น์ด ์˜ค๋ฅ˜๋‚˜์ง€ ์•Š๋„๋ก
  const dispatch = useDispatch();

  useEffect(() => {
    if (data) return; // ํฌ์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜๋ฉด ์•„์˜ˆ ์š”์ฒญ์„ ํ•˜์ง€ ์•Š์Œ
    dispatch(getPost(postId));
  }, [postId, dispatch, data]);

  if (loading) return <div>๋กœ๋”ฉ์ค‘...</div>;
  if (error) return <div>์—๋Ÿฌ ๋ฐœ์ƒ!</div>;
  if (!data) return null;

  return (
    <>
      <button onClick={() => dispatch(goToHome())}>ํ™ˆ์œผ๋กœ ์ด๋™</button>
      <Post post={data} />
    </>
  );
}

export default PostContainer;

 

๋ผ์šฐํŠธ ์„ค์ •ํ•˜๊ธฐ

<pages/PostListPage.js>

import React from 'react';
import PostListContainer from '../containers/PostListContainer';

function PostListPage() {
  return <PostListContainer />;
}

export default PostListPage;

<pages/PostPage.js>

import React from 'react';
import PostContainer from '../containers/PostContainer';

function PostPage({ match }) {
  const { id } = match.params; // URL ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐํšŒํ•˜๊ธฐ

  // URL ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ’์€ ๋ฌธ์ž์—ด์ด๊ธฐ ๋•Œ๋ฌธ์— parseInt ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  return <PostContainer postId={parseInt(id, 10)} />;
}

export default PostPage;

<App.js>

import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import PostPage from './pages/PostPage';

function App() {
  return (
    <>
      <Route path="/" component={PostListPage} exact={true} />
      <Route path="/:id" component={PostPage} />
    </>
  );
}

export default App;

<components/PostList.js>

import React from 'react';
import { Link } from 'react-router-dom';

function PostList({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link to={`/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

export default PostList;

 

 

7. thunk์—์„œ ๋ผ์šฐํ„ฐ ์—ฐ๋™ํ•˜๊ธฐ

 

customHistory ๋งŒ๋“ค์–ด์„œ ์ ์šฉํ•˜๊ธฐ

<index.js>

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';

const customHistory = createBrowserHistory();

const store = createStore(
  rootReducer,
  // logger ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, logger๊ฐ€ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•ฉ๋‹ˆ๋‹ค.
  composeWithDevTools(
    applyMiddleware(
      ReduxThunk.withExtraArgument({ history: customHistory }),
      logger
    )
  )
); // ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์ ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ReactDOM.render(
  <Router history={customHistory}>
    <Provider store={store}>
      <App />
    </Provider>
  </Router>,
  document.getElementById('root')
);

serviceWorker.unregister();