카운터 구현하기

카운터의 상태를 리덕스를 사용하여 관리해보겠습니다. 구현하기에 앞서, Counter.js 컴포넌트를 살펴봅시다.

import React from 'react';

const Counter = ({
  number,
  onIncrement,
  onDecrement
}) => {
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrement}>증가 (+)</button>
      <button onClick={onDecrement}>감소 (-)</button>
    </div>
  );
};

Counter.defaultProps = {
  number: 0
}

export default Counter;

이 컴포넌트에서는, 숫자값 number 와, 값을 증가시키는 함수 onIncrement, 그리고 값을 감소시키는 함수 onDecrement 를 props 로 받아옵니다.

counter 모듈 작성하기

그럼, 여기서 필요한 리덕스 모듈을 작성해봅시다.

src/store/modules/counter.js

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
// 이 함수들은 나중에 다른 파일에서 불러와야 하므로 내보내줍니다.
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

// 모듈의 초기 상태를 정의합니다.
const initialState = {
  number: 0
};

// 리듀서를 만들어서 내보내줍니다.
export default function reducer(state = initialState, action) {
  // 리듀서 함수에서는 액션의 타입에 따라 변화된 상태를 정의하여 반환합니다.
  // state = initialState 이렇게 하면 initialState 가 기본 값으로 사용됩니다.
  switch(action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state; // 아무 일도 일어나지 않으면 현재 상태를 그대로 반환합니다.
  }
}

리덕스 매뉴얼에선 액션과 리듀서를 각각 다른 파일에 작성하여 관리하는 것을 알려주는데요, 그렇게 사용 했을때는, 새 액션을 추가 할 때마다 두개의 파일을 건들여야 한다는점이 불편합니다. 이렇게 하나의 파일에 모두 작성하는 것은 Ducks 구조라고 부릅니다.

이 구조에서는, 리덕스 관련 코드를 기능별로 하나의 파일로 나눠서 작성합니다. 액션이름을 만들 때에는 const 를 사용하여 레퍼런스에 문자열을 담는데, 앞에 도메인을 추가하는 방식으로, 서로 다른 모듈에서 동일한 액션 이름을 가질 수 있게 됩니다. 예를들어서, 다른 모듈에서도 INCREMENT 라는 이름을 사용하되 "another/INCREMENT" 값을 담게 하면 되겠죠?

redux-actions 의 createAction 과 handleActions 사용하기

위 코드에서는 각 액션들마다 액션 객체를 만들어주는 액션 생성 함수를 일일히 작성해주었습니다. redux-actions 의 createAction 이라는 함수를 사용하면 액션 생성 함수 코드를 다음과 같이 작성 할 수 있게 됩니다.

src/store/modules/counter.js

import { createAction } from 'redux-actions';

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

(...)

액션 생성함수에서 파라미터를 필요하게 되는 경우에도 createAction 을 사용 할 수 있습니다. 그 부분은 추후 우리가 todo 모듈을 작성하게 될 때 알아보겠습니다.

우리가 기존에 작성한 리듀서에서는 각 액션타입에 따라 다른 작업을 하기 위해서 switch 구문을 사용했었죠? switch 문은 block 으로 따로 나뉘어져 있는것이 아니기 때문에 이러한 작업은 못합니다:

switch(value) {
  case 0: 
    const a = 1;
    break;
  case 1:
    const a = 2; // ERROR!
    break;
  default:
}

그 이유는, const 혹은 let 의 스코프는 블록({ }) 으로 제한되어있는데, 모든 case 는 하나의 블록안에 있기 때문에, 위와같이 중복 선언이 불가능해진다는 문제점도 있고, 여러모로 switch case 문은 귀찮습니다.

handleActions 를 사용하면, 리듀서 코드를 조금 더 깔끔하게 작성 할 수 있습니다.

src/store/modules/counter.js

import { createAction, handleActions } from 'redux-actions';

// 액션 타입을 정의해줍니다.
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생성 함수를 만듭니다.
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

// 모듈의 초기 상태를 정의합니다.
const initialState = {
  number: 0
};

// handleActions 의 첫번째 파라미터는 액션을 처리하는 함수들로 이뤄진 객체이고
// 두번째 파라미터는 초기 상태입니다.
export default handleActions({
  [INCREMENT]: (state, action) => {
    return { number: state.number + 1 };
  },
  // action 객체를 참조하지 않으니까 이렇게 생략을 할 수도 있겠죠?
  // state 부분에서 비구조화 할당도 해주어서 코드를 더욱 간소화시켰습니다.
  [DECREMENT]: ({ number }) => ({ number: number - 1 })
}, initialState);

combineReducers 로 리듀서 합치기

지금은 리듀서가 하나밖에 없지만, 앞으로 우리가 todo 리듀서도 만들고 나면 한 프로젝트에 여러개의 리듀서가 존재하게 됩니다. 여러개의 리듀서가 있을 때에는, redux 의 함수 combineReducers 를 사용하여 하나의 리듀서로 합쳐줄 수 있습니다. 이렇게 합쳐진 리듀서는 루트 리듀서 라고 부릅니다.

src/modules/index.js

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

export default combineReducers({
  counter
});

만약에 리듀서가 늘어나면 combineReducers({}) 부분에 더 추가를 해주면 됩니다.

스토어 만드는 함수 만들기

스토어를 만드는 함수 configure 를 만들어서 내보내주겠습니다. 기본적으로는, 이렇게 작성하면 됩니다:

src/store/configure.js

import { createStore } from 'redux';
import modules from './modules';

const configure = () => {
  const store = createStore(modules);
  return store;
}

export default configure;

우리는, 개발을 더 편하게 하기 위해서 redux-devtools 라는 크롬 익스텐션을 사용해볼건데요, 이를 사용하기 위해선 크롬 웹스토어 에서 설치를 하고, 스토어 생성 함수를 조금 바꿔주어야 합니다.

src/store/configure.js

import { createStore } from 'redux';
import modules from './modules';

const configure = () => {
  // const store = createStore(modules);
  const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  const store = createStore(modules, devTools);

  return store;
}

export default configure;

스토어 만들어서 내보내기

방금 만든 configure 함수를 사용하여 스토어를 만들고, 내보내주겠습니다.

src/store/index.js

import configure from './configure';
export default configure();

간단하지요?

리액트 앱에 리덕스 적용하기

리액트 앱에 리덕스를 적용 할 때에는, react-redux 에 들어있는 Provider 를 사용합니다. 프로젝트의 최상위 컴포넌트인 Root 컴포넌트를 열어서, Provider 와 우리가 방금 만든 store 를 불러온 뒤 다음과 같이 코드를 작성하세요.

src/Root.js

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';

import App from './components/App';

const Root = () => {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  );
};

export default Root;

CounterContainer 컴포넌트 만들기

이제 리덕스와 연동된 컴포넌트인 CounterContainer 컴포넌트를 만들겠습니다. 일단, 컴포넌트를 만들어서 단순히 Counter 를 불러온다음에 렌더링하세요.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';

class CounterContainer extends Component {
  render() {
    return (
      <Counter />
    );
  }
}

export default CounterContainer;

그리고 이 컴포넌트를 App 에서 불러와서 기존의 Counter 를 대체하겠습니다.

src/components/App.js

import React, { Component } from 'react';
import CounterContainer from 'containers/CounterContainer';
import AppTemplate from './AppTemplate';
import Todos from './Todos';

class App extends Component {
  render() {
    return (
      <AppTemplate
        counter={<CounterContainer />}
        todos={<Todos />}
      />
    );
  }
}

export default App;

컴포넌트가 그대로 렌더링되고 있나요? 그러면 CounterContainer 를 리덕스에 연결해주겠습니다. 코드의 하단부 부터 주석과 함께 코드를 읽어가면서 작성해보세요.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    this.props.increment();
  }

  handleDecrement = () => {
    this.props.decrement();
  }

  render() {
    const { handleIncrement, handleDecrement } = this;
    const { number } = this.props;

    return (
      <Counter 
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        number={number}
      />
    );
  }
}

// props 값으로 넣어 줄 상태를 정의해줍니다.
const mapStateToProps = (state) => ({
  number: state.counter.number
});

// props 값으로 넣어 줄 액션 함수들을 정의해줍니다
const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch(counterActions.increment()),
  decrement: () => dispatch(counterActions.decrement())
})

// 컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다.
// connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다.
// 반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다.
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);

mapStateToProps 스토어의 상태를 파라미터로 받아오는 함수로서, 컴포넌트에 상태로 넣어줄 props 를 반환합니다. mapDispatchToProps 는 dispatch 를 파라미터로 받아오는 함수로서, 컴포넌트에 넣어줄 액션 함수들을 반환합니다.

코드를 저장하고 카운터의 증가버튼와 감소버튼을 눌러보세요. 숫자가 바뀌나요?

보통은 위와 같은 코드처럼, mapStateToProps 와 mapDispatchToProps 를 따로 만들곤 하는데, 사람마다 차이가 있을 수 있겠지만 그냥 함수를 connect 내부에서 정의하면 코드가 조금 더 깔끔해집니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  (...)
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => ({
    increment: () => dispatch(counterActions.increment()),
    decrement: () => dispatch(counterActions.decrement())
  })
)(CounterContainer);

그리고 지금 dispatch 를 보면 각 액션 함수마다 일일히 dispatch(actionCreator()) 형식으로 작성해야 된다는점이 조금 귀찮습니다. 이 부분은, redux 의 bindActionCreator 함수를 사용하면 더 간소화 할 수 있습니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  (...)
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => bindActionCreators(counterActions, dispatch)
)(CounterContainer);

코드가 좀 간소화됐죠? 나중에 가면 여러분이 만들 컨테이너 컴포넌트에서 여러 모듈에서 액션 생성 함수를 참조해야 하게 되는 일도 있습니다. 그러한 경우엔 다음과 같이 bindActionCreators 의 결과물을 CounterActions 라는 props 로 넣어주면 됩니다. 그리고 물론, 이에 따라 메소드들도 조금 바꿔줘야겠죠?

src/containers/CounterContainer.js

import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as counterActions from 'store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    const { CounterActions } = this.props;
    CounterActions.increment();
  }
  handleDecrement = () => {
    const { CounterActions } = this.props;
    CounterActions.decrement();
  }
  render() {
    const { handleIncrement, handleDecrement } = this;
    const { number } = this.props;

    return (
      <Counter 
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        number={number}
      />
    );
  }
}

export default connect(
  (state) => ({
    number: state.counter.number
  }), 
  (dispatch) => ({
    CounterActions: bindActionCreators(counterActions, dispatch)
  })
)(CounterContainer);

이제 카운터 코드는 모두 작성하였습니다. 코드를 저장하고 카운터가 제대로 작동하는지 확인해보세요.

results matching ""

    No results matching ""