๐Ÿฌ ๊ธดํ˜ธํก/๊ณ ์Šค๋ฝ ํ‹ฐ์ผ“

[ํ‹ฐ์ผ“ ์˜ˆ๋งค ํ”„๋กœ์ ํŠธ] 3. ์–ป์–ด๊ฐ„ ๊ฒƒ๋“ค

ํ•œ๊ทœ์ง„ 2022. 2. 10. 00:15

ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋ฉด์„œ ์–ป์–ด๊ฐ„ ์ง€์‹๊ณผ ๊ฒฝํ—˜์„ ๋ช‡๊ฐœ ์ •๋ฆฌํ•ด๋ณธ๋‹ค. ๋ ˆ์˜ค๊ฐ€ ๋งŽ์ด ์•Œ๋ ค์ฃผ๊ณ  ๋„์™€์ฃผ์—ˆ๋‹ค.

 


 

1. ๋ฆฌ๋•์Šค ํ™œ์šฉ

์ฒ˜์Œ ๋ฆฌ๋•์Šค๋ฅผ ๊ณต๋ถ€ํ•  ๋•Œ ์ฝ์—ˆ๋˜ ์ฑ…์—์„œ Ducks ํŒจํ„ด์„ ๋ฐฐ์› ๋‹ค. ๋ง‰์ƒ ์จ๋ณด๋ ค๋‹ˆ๊นŒ ๊ดœํžˆ ๋ณต์žกํ•œ ๋Š๋‚Œ์ด ๋“ค์–ด์„œ ๊ทธ ํ›„๋กœ ๋ฆฌ๋•์Šค๋ฅผ ๋ณ„๋กœ ์•ˆ ์ข‹์•„ํ–ˆ๋˜... ๊ธฐ์–ต์ด.. ์ด๋ฒˆ์— ์“ฐ๊ฒŒ๋œ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. ๋ฆฌ๋•์Šค ๊ณต์‹ ์‚ฌ์ดํŠธ์— ์ ํ˜€์žˆ๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ• ๊ฑธ? ์•ก์…˜ ํƒ€์ž…, ์•ก์…˜ ์ƒ์„ฑํ•จ์ˆ˜ ๊ทธ๋ฆฌ๊ณ  ๋ฆฌ๋“€์„œ๋ฅผ ๊ฐ๊ฐ ํ•˜๋‚˜์˜ ํด๋”์— ๋‹ด๋Š”๋‹ค. ํ•œ๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•  ์ผ์ด ์ƒ๊ฒผ์„ ๋•Œ 3๊ฐœ์˜ ํŒŒ์ผ์„ ๋ชจ๋‘ ๊ฑด๋“œ๋ ค์•ผ ํ•œ๋‹ค๋Š” ๋‹จ์ ์œผ๋กœ ์–ด์ฉŒ๊ตฌ์ €์ฉŒ๊ตฌ ํ•˜๋Š” ๊ธ€๋“ค์„ ๋งŽ์ด ๋ดค๋Š”๋ฐ, ํŒŒ์ผ ํ•˜๋‚˜์— ํƒ€์ž…, ์•ก์…˜, ๋ฆฌ๋“€์„œ ๋‹ค ๋•Œ๋ ค๋ฐ•์•„์„œ ์ฝ”๋“œ ๊ธธ์–ด์ง€๋Š”๊ฒŒ ๋” ์˜คํžˆ๋ ค ๋ถˆํŽธํ–ˆ๋‹ค. ๊ฐœ์ธ์ ์œผ๋กœ๋Š” ๋” ๋งˆ์Œ์— ๋“œ๋Š” ๋ฐฉ์‹์ด๋‹ค.

 

reducer/index.js ์€ ๋ถˆ๋Ÿฌ์˜จ ๊ฐ ํŒŒ์ผ๋“ค์„ combineReducer๋กœ ๋ฌถ์–ด์„œ exportํ•˜๋Š” ์—ญํ• ๋งŒ ํ•˜๊ณ 

storeSetting.js์—์„œ ๋ฐ๋ธŒํˆด์ฆˆ, thunk, createStore ๋“ฑ์˜ ๋‹ค๋ฅธ ์„ธํŒ…๋“ค์„ ํ•ด์ค€๋‹ค.

 

// ์ธ์ฆ๋œ ์ƒํƒœ์ผ๋•Œ ๊ฐ•์ œ๋กœ accessToken ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ž๋ฆฌ(initial ๋ฆฌ๋•์Šค ์— ์ง‘์–ด๋„ฃ๊ธฐ ์œ„ํ•จ (์ƒˆ๋กœ๊ณ ์นจ ์‹œ))
const userAccessToken = localStorage.getItem('userAccessToken');
const phoneNumber = localStorage.getItem('phoneNumber');

// axios Bearer ํ† ํฐ์— ์ปค๋จผ ํ—ค๋”๋กœ ๊ปด๋†“๊ธฐ ์œ„ํ•จ
axios.defaults.headers.common.Authorization = `Bearer ${userAccessToken}`;

/**
* ...
**/

export const store = createStore(
  reducers,
  {
    // initial state for autheticated
    auth: {
      authenticated: userAccessToken === null ? false : true,
      userAccessToken: userAccessToken === null ? null : userAccessToken,
      phoneNumber: phoneNumber === null ? null : phoneNumber
    },
    routePagination: {
      currentPage: firstPathName
    }
  },
  enhancer
);

storeSetting.js ์˜ ์ผ๋ถ€์ด๋‹ค. ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์— ์žˆ๋Š” ๊ฐ’๋“ค์„ ๊ฐ€์ ธ์™€์„œ initial state์— ๋„ฃ์–ด๋‘”๋‹ค. ํŒŒ์ผ์„ index.js์— import ์‹œ์ผœ๋†“์•˜๊ธฐ ๋•Œ๋ฌธ์— ๋งค๋ฒˆ ์ƒˆ๋กœ๊ณ ์นจํ• ๋•Œ๋งˆ๋‹ค ๋ฐ”๋กœ๋ฐ”๋กœ ์‹คํ–‰๋œ๋‹ค.

 

์ธ์ƒ๊นŠ์—ˆ๋˜ ๊ฒƒ์€ axios default๋ฅผ ์„ค์ •ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์ด์—ˆ๋‹ค. ์ด์ „์—๋Š” ํ† ํฐ์ด ํ•„์š”ํ•œ ์š”์ฒญ์„ ๋ณด๋‚ผ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ํ—ค๋”๋ฅผ ๋„ฃ์–ด์„œ ๋ณด๋ƒˆ์—ˆ๋‹ค. ํ—ค๋”๋ฅผ ๋””ํดํŠธ๋กœ ๋„ฃ์–ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๊ฑธ ๋งค๋ฒˆ ํŽ˜์ด์ง€๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋  ๋•Œ๋งˆ๋‹ค ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์š”์ฒญ์— ๋”ฐ๋กœ ํ—ค๋”๋ฅผ ๋„ฃ์–ด์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.

 

 

2. react-router-dom , history ํ™œ์šฉ

import {
  MESSAGE_SEND_SUCCESS,
  MESSAGE_SEND_PENDING,
  MESSAGE_SEND_ERROR
} from '../action-types';
import axios from 'axios';
import history from '../../history';

export const messageSend =
  ({ phoneNumber }) =>
  async dispatch => {
    try {

    /**
    * ..
    **/

      // ์ž๋™์œผ๋กœ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€๊ฒŒ ๋”
      history.push('/auth/validation');
    } catch (e) {
      //400 ~
      dispatch({ type: MESSAGE_SEND_ERROR, payload: '๋ฉ”์‹œ์ง€ ์ „์†ก ์˜ค๋ฅ˜' });
    }
  };

์•ก์…˜ ์ƒ์„ฑํ•จ์ˆ˜์— ์žˆ๋Š” ํ•œ ํŒŒ์ผ์„ ๋“ค๊ณ ์™€ ๋ณด์•˜๋‹ค. '๋‹ค์Œ์œผ๋กœ ๊ฐ€๊ธฐ' ๊ฐ™์€ ๋ฒ„ํŠผ์— ๋‹ฌ๋ฆฐ ์•ก์…˜์ด๋‹ค. ๋ฆฌ๋•์Šค thunk๋ฅผ ์ด์šฉํ•˜๊ณ  ์žˆ๊ณ , axios ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ ํ›„์— history.push(...)๋กœ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋„˜์–ด๊ฐ€๋„๋ก ํ–ˆ๋‹ค.

 

๋ณดํ†ต์€ useNavigate ๋“ฑ์„ ์ด์šฉํ•ด์„œ ๋ผ์šฐํŒ…์„ ํ•˜๋Š”๋ฐ, ์œ„์˜ ํ•จ์ˆ˜๋Š” ๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์“ฐ์ด๋Š”๊ฒŒ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ history ๊ฐ์ฒด๋ฅผ ์„ค์ •์„ ํ•ด์ค€ ๋‹ค์Œ์— ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

import history from '../../history' ๊ฐ€ ๋ฐ”๋กœ ๊ทธ ๋ถ€๋ถ„์ด๋‹ค.

 

import { createBrowserHistory } from 'history';
import { routeChange } from './state/actions-creators';
import { store } from './state/storeSetting';
const history = createBrowserHistory();

history.listen(({ action, location }) => {
  console.log(action, location);
  store.dispatch(routeChange({ pathname: location.pathname }));
});
export default history;

history.js ์ด๋‹ค. createBrowserHistory() ๋กœ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๊ณ ,

 

import React from 'react';

/**
* ..
**/
import history from './history';

import {
  Routes,
  Route,
  unstable_HistoryRouter as HistoryRouter
} from 'react-router-dom';

/**
* ..
**/

ReactDOM.render(
  <HistoryRouter history={history}>
    <Provider store={store}>
      <Routes>
        <Route path="/" element={<LandingPage />} />
        <Route path="/*" element={<Pagination />} />
      </Routes>
    </Provider>
  </HistoryRouter>,
  document.getElementById('root')
);

index.js ๋กœ ๊ฐ€์ ธ์™€ import ํ•œ๋‹ค. BrowserRouter๊ฐ€ ์•„๋‹Œ HistoryRouter๋กœ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ฐ์‹ธ๊ณ , history ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•ด์ค€๋‹ค. ์ฐธ๊ณ 

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด history๋ฅผ ์ „์—ญ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

3. HOC ํ™œ์šฉ

์ธ์ฆ์ด ๋˜์–ด์žˆ์„ ๋•Œ๋งŒ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€์— ์ ‘๊ทผ์„ ๋ง‰์„ ๋ฐฉ๋ฒ•์œผ๋กœ HOC๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

 

import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import authPassHOC from '../../hoc/authPassHOC';
import requireAuth from '../../hoc/requireAuth';
import ListLandingPage from './ListLandingPage/ListLandingPage';
import TicketListPage from './TicketListPage/TicketListPage';

function ListProcess({ location }) {
  // ์ธ์ฆ๋˜์–ด์žˆ์œผ๋ฉด ๋žœ๋”ฉํŽ˜์ด์ง€ ๋„˜๊ธฐ๊ณ  ๋ฐ”๋กœ ํ‹ฐ์ผ“๋ฆฌ์ŠคํŠธ๋กœ
  const APHTicketListPage = authPassHOC(ListLandingPage, '/list/mytickets');
  // ์ธ์ฆ ์•ˆ๋˜์–ด์žˆ์œผ๋ฉด ์ธ์ฆํŽ˜์ด์ง€๋กœ,
  const RATicketListPage = requireAuth(TicketListPage);

  return (
    <Routes location={location}>
      <Route
        exact
        path="landing"
        element={<APHTicketListPage style={{ position: 'absolute' }} />}
      />
      <Route
        exact
        path="mytickets"
        element={<RATicketListPage style={{ position: 'absolute' }} />}
      />
      {/*์ด์ƒํ•œ url๋กœ ๋“ค์–ด์˜ฌ๊ฒฝ์šฐ ํ™ˆํŽ˜์ด์ง€๋กœ ์ด๋™*/}
      <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  );
}

export default ListProcess;

์˜ˆ๋งคํ•œ ํ‹ฐ์ผ“์„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€๋ฅผ ์œ„ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜์ด๋‹ค. 'list/landing'์€ ์ธ์ฆ์ด ๋˜์–ด ์žˆ์ง€ ์•Š์„ ๋•Œ๋งŒ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๊ณ  'list/mytickets'๋Š” ์ธ์ฆ์ด ๋˜์–ด์žˆ์„ ๋•Œ๋งŒ ๋ณด์—ฌ์ค˜์•ผ ํ•œ๋‹ค. ๋ผ์šฐํŠธ ์•ˆ์—์„œ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋ง ํ•˜๊ธฐ ์ „์— hoc๋กœ ํ•œ๋ฒˆ ๊ฐ์‹ธ์„œ ์ธ์ฆ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ ํ›„์— ๋„˜๊ฒจ์ค€๋‹ค.

 

import React from 'react';
import { useSelector } from 'react-redux';

import { Navigate } from 'react-router-dom';

/**
* ์ธ์ฆ์ด ๋˜์–ด์žˆ์œผ๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ๋„˜๊น€
**/
const authPassHOC =
  (Component, nextUrl) =>
  ({ ...props }) => {
    const { authenticated } = useSelector(state => state.auth);

    return authenticated === true ? (
      <Navigate to={nextUrl} />
    ) : (
      <Component {...props} />
    );
  };

export default authPassHOC;


/**
* ์ธ์ฆ์ด ์•ˆ๋˜์–ด์žˆ์œผ๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋Œ๋ ค๋ณด๋ƒ„
**/
const requireAuth =
  Component =>
  ({ ...props }) => {
    const { authenticated } = useSelector(state => state.auth);

    return authenticated === true ? (
      <Component {...props} />
    ) : (
      <Navigate to="/" />
    );
  };

export default requireAuth;

hoc์˜ ๊ตฌ์กฐ๋Š” ์œ„์™€ ๊ฐ™๋‹ค. ๊ฐ„๋‹จํ•˜๋‹ค. ์Šคํ† ์–ด์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ณ  ์ธ์ฆ ์œ ๋ฌด์— ๋”ฐ๋ผ ๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •ํ•ด์ค€๋‹ค.

 

์ด์ „์—๋Š” ํŽ˜์ด์ง€์— ์ง„์ž…ํ•œ ํ›„์— ์ธ์ฆ ์œ ๋ฌด๋ฅผ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ๋Œ๋ ค๋ณด๋‚ด๋Š” ๋ฐฉ์‹์œผ๋กœ๋งŒ ํ–ˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋ง ํ•˜๊ธฐ ์ „์— hoc ์•ˆ์—์„œ ํ•œ๋ฒˆ ๊ฒ€์ฆ์„ ๊ฑฐ์น˜๋Š” ๊ณผ์ •์„ ํ†ตํ•ด ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ๋ฆ„์„ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.