λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

✏️/React

React κ³΅λΆ€ν•˜κΈ°(6) - λ¦¬λ•μŠ€(Redux)

πŸ”§ λ¦¬λ•μŠ€ κ°œλ°œμžλ„κ΅¬ μ μš©ν•˜κΈ°

https://react.vlpt.us/redux/06-redux-devtools.html

 

6. λ¦¬λ•μŠ€ κ°œλ°œμžλ„κ΅¬ μ μš©ν•˜κΈ° · GitBook

6. λ¦¬λ•μŠ€ κ°œλ°œμžλ„κ΅¬ μ μš©ν•˜κΈ° μ΄λ²ˆμ—λŠ” λ¦¬λ•μŠ€ 개발자 도ꡬλ₯Ό μ‚¬μš©ν•˜λŠ” 방법에 λŒ€ν•΄μ„œ μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€. λ¦¬λ•μŠ€ 개발자 도ꡬλ₯Ό μ‚¬μš©ν•˜λ©΄ ν˜„μž¬ μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό 개발자 λ„κ΅¬μ—μ„œ 쑰회 ν•  수 있

react.vlpt.us

 

3. λ¦¬λ•μŠ€ μ‚¬μš©ν•  μ€€λΉ„ν•˜κΈ°

 

import { createStore } from 'redux'; // μŠ€ν† μ–΄ 생성

 

<exercise.js>

import { createStore } from 'redux';

// createStoreλŠ” μŠ€ν† μ–΄λ₯Ό λ§Œλ“€μ–΄μ£ΌλŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€.
// λ¦¬μ•‘νŠΈ ν”„λ‘œμ νŠΈμ—μ„œλŠ” 단 ν•˜λ‚˜μ˜ μŠ€ν† μ–΄λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

/* λ¦¬λ•μŠ€μ—μ„œ 관리 ν•  μƒνƒœ μ •μ˜ */
const initialState = {
  counter: 0,
  text: '',
  list: []
};

/* μ•‘μ…˜ νƒ€μž… μ •μ˜ */
// μ•‘μ…˜ νƒ€μž…μ€ 주둜 λŒ€λ¬Έμžλ‘œ μž‘μ„±ν•©λ‹ˆλ‹€.
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';

/* μ•‘μ…˜ μƒμ„±ν•¨μˆ˜ μ •μ˜ */
// μ•‘μ…˜ μƒμ„±ν•¨μˆ˜λŠ” 주둜 camelCase 둜 μž‘μ„±ν•©λ‹ˆλ‹€.
function increase() {
  return {
    type: INCREASE // μ•‘μ…˜ κ°μ²΄μ—λŠ” type 값이 ν•„μˆ˜μž…λ‹ˆλ‹€.
  };
}

// ν™”μ‚΄ν‘œ ν•¨μˆ˜λ‘œ μž‘μ„±ν•˜λŠ” 것이 λ”μš± μ½”λ“œκ°€ κ°„λ‹¨ν•˜κΈ°μ—,
// μ΄λ ‡κ²Œ μ“°λŠ” 것을 μΆ”μ²œν•©λ‹ˆλ‹€.
const decrease = () => ({
  type: DECREASE
});

const changeText = text => ({
  type: CHANGE_TEXT,
  text // μ•‘μ…˜μ•ˆμ—λŠ” type 외에 좔가적인 ν•„λ“œλ₯Ό λ§ˆμŒλŒ€λ‘œ 넣을 수 μžˆμŠ΅λ‹ˆλ‹€.
});

const addToList = item => ({
  type: ADD_TO_LIST,
  item
});

/* λ¦¬λ“€μ„œ λ§Œλ“€κΈ° */
// μœ„ μ•‘μ…˜ μƒμ„±ν•¨μˆ˜λ“€μ„ 톡해 λ§Œλ“€μ–΄μ§„ 객체듀을 μ°Έμ‘°ν•˜μ—¬
// μƒˆλ‘œμš΄ μƒνƒœλ₯Ό λ§Œλ“œλŠ” ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄λ΄…μ‹œλ‹€.
// 주의: λ¦¬λ“€μ„œμ—μ„œλŠ” λΆˆλ³€μ„±μ„ κΌ­ μ§€μΌœμ€˜μ•Ό ν•©λ‹ˆλ‹€!

function reducer(state = initialState, action) {
  // state 의 μ΄ˆκΉƒκ°’μ„ initialState 둜 μ§€μ •ν–ˆμŠ΅λ‹ˆλ‹€.
  switch (action.type) {
    case INCREASE:
      return {
        ...state,
        counter: state.counter + 1
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1
      };
    case CHANGE_TEXT:
      return {
        ...state,
        text: action.text
      };
    case ADD_TO_LIST:
      return {
        ...state,
        list: state.list.concat(action.item)
      };
    default:
      return state;
  }
}

/* μŠ€ν† μ–΄ λ§Œλ“€κΈ° */
const store = createStore(reducer);

console.log(store.getState()); // ν˜„μž¬ store μ•ˆμ— λ“€μ–΄μžˆλŠ” μƒνƒœλ₯Ό μ‘°νšŒν•©λ‹ˆλ‹€.

// μŠ€ν† μ–΄μ•ˆμ— λ“€μ–΄μžˆλŠ” μƒνƒœκ°€ λ°”λ€” λ•Œ λ§ˆλ‹€ ν˜ΈμΆœλ˜λŠ” listener ν•¨μˆ˜
const listener = () => {
  const state = store.getState();
  console.log(state);
};

const unsubscribe = store.subscribe(listener);
// ꡬ독을 ν•΄μ œν•˜κ³  싢을 λ•ŒλŠ” unsubscribe() λ₯Ό ν˜ΈμΆœν•˜λ©΄ λ©λ‹ˆλ‹€.

// μ•‘μ…˜λ“€μ„ λ””μŠ€νŒ¨μΉ˜ ν•΄λ΄…μ‹œλ‹€.
store.dispatch(increase());
store.dispatch(decrease());
store.dispatch(changeText('μ•ˆλ…•ν•˜μ„Έμš”'));
store.dispatch(addToList({ id: 1, text: 'μ™€μš°' }));

 

 

4. λ¦¬λ•μŠ€ λͺ¨λ“ˆ λ§Œλ“€κΈ°

src/modules/

 

λ¦¬λ•μŠ€ λͺ¨λ“ˆ λ§Œλ“€κΈ°

λ¦¬λ•μŠ€ λͺ¨λ“ˆ? λ‹€μŒ ν•­λͺ©λ“€μ΄ λͺ¨λ‘ λ“€μ–΄μžˆλŠ” μžλ°”μŠ€ν¬λ¦½νŠΈ 파일

  • μ•‘μ…˜ νƒ€μž…
  • μ•‘μ…˜ μƒμ„±ν•¨μˆ˜
  • λ¦¬λ“€μ„œ

<modules/counter.js>

/* μ•‘μ…˜ νƒ€μž… λ§Œλ“€κΈ° */
// Ducks νŒ¨ν„΄μ„ λ”°λ₯Όλ• μ•‘μ…˜μ˜ 이름에 접두사λ₯Ό λ„£μ–΄μ£Όμ„Έμš”.
// μ΄λ ‡κ²Œ ν•˜λ©΄ λ‹€λ₯Έ λͺ¨λ“ˆκ³Ό μ•‘μ…˜ 이름이 μ€‘λ³΅λ˜λŠ” 것을 방지 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

/* μ•‘μ…˜ μƒμ„±ν•¨μˆ˜ λ§Œλ“€κΈ° */
// μ•‘μ…˜ μƒμ„±ν•¨μˆ˜λ₯Ό λ§Œλ“€κ³  export ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•΄μ„œ λ‚΄λ³΄λ‚΄μ£Όμ„Έμš”.
export const setDiff = diff => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

/* 초기 μƒνƒœ μ„ μ–Έ */
const initialState = {
  number: 0,
  diff: 1
};

/* λ¦¬λ“€μ„œ μ„ μ–Έ */
// λ¦¬λ“€μ„œλŠ” export default 둜 λ‚΄λ³΄λ‚΄μ£Όμ„Έμš”.
export default function counter(state = initialState, action) {
  switch (action.type) {
    case SET_DIFF:
      return {
        ...state,
        diff: action.diff
      };
    case INCREASE:
      return {
        ...state,
        number: state.number + state.diff
      };
    case DECREASE:
      return {
        ...state,
        number: state.number - state.diff
      };
    default:
      return state;
  }
}

 

루트 λ¦¬λ“€μ„œ λ§Œλ“€κΈ°

루트 λ¦¬λ“€μ„œ? λ¦¬λ“€μ„œλ“€μ„ ν•©μ³μ„œ λ§Œλ“œλŠ” 루트 λ¦¬λ“€μ„œ

import { combineReducers } from 'redux';

<modules/index.js> => 루트 λ¦¬λ“€μ„œ λ§Œλ“€κΈ°

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

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

export default rootReducer;

<src/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 } from 'redux';
import rootReducer from './modules';

const store = createStore(rootReducer); // μŠ€ν† μ–΄λ₯Ό λ§Œλ“­λ‹ˆλ‹€.
console.log(store.getState()); // μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό ν™•μΈν•΄λ΄…μ‹œλ‹€.

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();

 

λ¦¬μ•‘νŠΈ ν”„λ‘œμ νŠΈμ— λ¦¬λ•μŠ€ μ μš©ν•˜κΈ°

$ yarn add react-redux

<src/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 } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';

const store = createStore(rootReducer); // μŠ€ν† μ–΄λ₯Ό λ§Œλ“­λ‹ˆλ‹€.

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

serviceWorker.unregister();

=> Provider둜 storeλ₯Ό λ„£μ–΄μ„œ App을 κ°μ‹Έκ²Œ 되면 μš°λ¦¬κ°€ λ Œλ”λ§ν•˜λŠ” κ·Έ μ–΄λ–€ μ»΄ν¬λ„ŒνŠΈλ˜μ§€ λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ— μ ‘κ·Όν•  수 μžˆλ‹€.

 

 

5. κ΅¬ν˜„ν•˜κΈ°

 

ν”„λ¦¬μ  ν…Œμ΄μ…”λ„ μ»΄ν¬λ„ŒνŠΈ λ§Œλ“€κΈ°

src/components/

ν”„λ ˆμ  ν…Œμ΄μ…”λ„ μ»΄ν¬λ„ŒνŠΈ? λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ— μ§μ ‘μ μœΌλ‘œ μ ‘κ·Όν•˜μ§€ μ•Šκ³  ν•„μš”ν•œ κ°’ λ˜λŠ” ν•¨μˆ˜λ₯Ό props둜만 λ°›μ•„μ™€μ„œ μ‚¬μš©ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ

=> 주둜 UIλ₯Ό μ„ μ–Έν•˜λŠ” 것에 μ§‘μ€‘ν•˜λ©°, ν•„μš”ν•œ κ°’λ“€μ΄λ‚˜ ν•¨μˆ˜λŠ” props둜 λ°›μ•„μ™€μ„œ μ‚¬μš©ν•˜λŠ” ν˜•νƒœλ‘œ κ΅¬ν˜„ν•œλ‹€.

<components/Counter.js>

import React from 'react';

function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
  const onChange = e => {
    // e.target.value 의 νƒ€μž…μ€ λ¬Έμžμ—΄μ΄κΈ° λ•Œλ¬Έμ— 숫자둜 λ³€ν™˜ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.
    onSetDiff(parseInt(e.target.value, 10));
  };
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <input type="number" value={diff} min="1" onChange={onChange} />
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
}

export default Counter;

 

μ»¨ν…Œμ΄λ„ˆ μ»΄ν¬λ„ŒνŠΈ λ§Œλ“€κΈ°

src/containers/

μ»¨ν…Œμ΄λ„ˆ μ»΄ν¬λ„ŒνŠΈ? λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό μ‘°νšŒν•˜κ±°λ‚˜, μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ ν•  수 μžˆλŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό μ˜λ―Έν•œλ‹€.

HTML νƒœκ·Έλ“€μ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³  λ‹€λ₯Έ ν”„λ¦¬μ  ν…Œμ΄μ…”λ„ μ»΄ν¬λ„ŒνŠΈλ“€μ„ λΆˆλŸ¬μ™€μ„œ μ‚¬μš©ν•œλ‹€.

<containers/CounterContainer.js>

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';

function CounterContainer() {
  // useSelectorλŠ” λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό μ‘°νšŒν•˜λŠ” Hookμž…λ‹ˆλ‹€.
  // state의 값은 store.getState() ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν–ˆμ„ λ•Œ λ‚˜νƒ€λ‚˜λŠ” κ²°κ³Όλ¬Όκ³Ό λ™μΌν•©λ‹ˆλ‹€.
  const { number, diff } = useSelector(state => ({
    number: state.counter.number,
    diff: state.counter.diff
  }));

  // useDispatch λŠ” λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ dispatch λ₯Ό ν•¨μˆ˜μ—μ„œ μ‚¬μš© ν•  수 있게 ν•΄μ£ΌλŠ” Hook μž…λ‹ˆλ‹€.
  const dispatch = useDispatch();
  // 각 μ•‘μ…˜λ“€μ„ λ””μŠ€νŒ¨μΉ˜ν•˜λŠ” ν•¨μˆ˜λ“€μ„ λ§Œλ“œμ„Έμš”
  const onIncrease = () => dispatch(increase());
  const onDecrease = () => dispatch(decrease());
  const onSetDiff = diff => dispatch(setDiff(diff));

  return (
    <Counter
      // μƒνƒœμ™€
      number={number}
      diff={diff}
      // μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ ν•˜λŠ” ν•¨μˆ˜λ“€μ„ props둜 λ„£μ–΄μ€λ‹ˆλ‹€.
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onSetDiff={onSetDiff}
    />
  );
}

export default CounterContainer;

<App.js>

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

function App() {
  return (
    <div>
      <CounterContainer />
    </div>
  );
}

export default App;

 

8. useSelector μ΅œμ ν™”

 

<container/CounterContainer.js>

const { number, diff } = useSelector(state => ({
  number: state.counter.number,
  diff: state.counter.diff
}));

=> useSeletor Hook을 톡해 맀번 λ Œλ”λ§ 될 λ•Œλ§ˆλ‹€ μƒˆλ‘œμš΄ 객체(number, diff)λ₯Ό λ§Œλ“œλŠ” 것이기 λ•Œλ¬Έμ— μƒνƒœκ°€ λ°”λ€Œμ—ˆλŠ”μ§€ 확인할 수 μ—†μ–΄μ„œ λ‚­λΉ„ λ Œλ”λ§μ΄ μ΄λ£¨μ–΄μ§€κ²Œ λœλ‹€.

 

μ΅œμ ν™” 방법! useSelectorλ₯Ό μ—¬λŸ¬λ²ˆ μ‚¬μš©ν•œλ‹€.

const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff);

=> μ΄λ ‡κ²Œ ν•˜λ©΄ ν•΄λ‹Ή κ°’λ“€ ν•˜λ‚˜λΌλ„ λ°”λ€Œμ—ˆμ„ λ•Œλ§Œ μ»΄ν¬λ„ŒνŠΈκ°€ λ¦¬λ Œλ”λ§λœλ‹€.

 

 

9. connect ν•¨μˆ˜

 

useSelector, useDispatchλž‘ λΉ„μŠ·

=> ν΄λž˜μŠ€ν˜• μ»΄ν¬λ„ŒνŠΈμ—μ„œλŠ” Hook μ‚¬μš© λͺ»ν•˜λ‹ˆκΉŒ connect ν•¨μˆ˜ μ‚¬μš©ν•¨..

  // useSelectorλŠ” λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό μ‘°νšŒν•˜λŠ” Hookμž…λ‹ˆλ‹€.
  // state의 값은 store.getState() ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν–ˆμ„ λ•Œ λ‚˜νƒ€λ‚˜λŠ” κ²°κ³Όλ¬Όκ³Ό λ™μΌν•©λ‹ˆλ‹€.
  const { number, diff } = useSelector(state => ({
    number: state.counter.number,
    diff: state.counter.diff
  }));

  // useDispatch λŠ” λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ dispatch λ₯Ό ν•¨μˆ˜μ—μ„œ μ‚¬μš© ν•  수 있게 ν•΄μ£ΌλŠ” Hook μž…λ‹ˆλ‹€.
  const dispatch = useDispatch();
  // 각 μ•‘μ…˜λ“€μ„ λ””μŠ€νŒ¨μΉ˜ν•˜λŠ” ν•¨μˆ˜λ“€μ„ λ§Œλ“œμ„Έμš”
  const onIncrease = () => dispatch(increase());
  const onDecrease = () => dispatch(decrease());
  const onSetDiff = diff => dispatch(setDiff(diff));

 

connectλŠ” HOC이닀.

HOC(Higher-Order Component): μ»΄ν¬λ„ŒνŠΈλ₯Ό νŠΉμ • ν•¨μˆ˜λ‘œ κ°μ‹Έμ„œ νŠΉμ • κ°’ λ˜λŠ” ν•¨μˆ˜λ₯Ό props둜 λ°›μ•„μ™€μ„œ μ‚¬μš©ν•  수 있게 ν•΄μ£ΌλŠ” νŒ¨ν„΄

const enhance = withState('counter', 'setCounter', 0)
const Counter = enhance(({ counter, setCounter }) =>
  <div>
    Count: {counter}
    <button onClick={() => setCounter(n => n + 1)}>Increment</button>
    <button onClick={() => setCounter(n => n - 1)}>Decrement</button>
  </div>
)

 

connect μ‚¬μš©ν•΄λ³΄κΈ°

<containers/CounterContainer.js>

import React from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';

function CounterContainer({ number, diff, onIncrease, onDecrease, onSetDiff }) {
  return (
    <Counter
      // μƒνƒœμ™€
      number={number}
      diff={diff}
      // μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ ν•˜λŠ” ν•¨μˆ˜λ“€μ„ props둜 λ„£μ–΄μ€λ‹ˆλ‹€.
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onSetDiff={onSetDiff}
    />
  );
}

// mapStateToProps λŠ” λ¦¬λ•μŠ€ μŠ€ν† μ–΄μ˜ μƒνƒœλ₯Ό μ‘°νšŒν•΄μ„œ μ–΄λ–€ 것듀을 props 둜 넣어쀄지 μ •μ˜ν•©λ‹ˆλ‹€.
// ν˜„μž¬ λ¦¬λ•μŠ€ μƒνƒœλ₯Ό νŒŒλΌλ―Έν„°λ‘œ λ°›μ•„μ˜΅λ‹ˆλ‹€.
const mapStateToProps = state => ({
  number: state.counter.number,
  diff: state.counter.diff
});

// mapDispatchToProps λŠ” μ•‘μ…˜μ„ λ””μŠ€νŒ¨μΉ˜ν•˜λŠ” ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄μ„œ props둜 λ„£μ–΄μ€λ‹ˆλ‹€.
// dispatch λ₯Ό νŒŒλΌλ―Έν„°λ‘œ λ°›μ•„μ˜΅λ‹ˆλ‹€.
const mapDispatchToProps = dispatch => ({
  onIncrease: () => dispatch(increase()),
  onDecrease: () => dispatch(decrease()),
  onSetDiff: diff => dispatch(setDiff(diff))
});

// connect ν•¨μˆ˜μ—λŠ” mapStateToProps, mapDispatchToProps λ₯Ό 인자둜 λ„£μ–΄μ£Όμ„Έμš”.
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CounterContainer);

/* μœ„ μ½”λ“œλŠ” λ‹€μŒκ³Ό λ™μΌν•©λ‹ˆλ‹€.
  const enhance = connect(mapStateToProps, mapDispatchToProps);
  export defualt enhance(CounterContainer);
*/