6. 알록달록 카운터 만들기

알록달록 카운터를 만들어봅시다! 리덕스 공식 매뉴얼 을 보면 액션을 위한 파일과 리듀서를 위한 파일을 따로 작성합니다. 그렇게 하셔도 상관은 없는데 저는 개인적으로 불편하다고 생각합니다. 하나의 파일로 작성하는 방법도 있는데, 이를 Ducks 패턴이라고 합니다.

우리는, 처음 배울떄부터 이 Ducks 패턴으로 개발을 하겠습니다.

counter 모듈 만들기

특정 기능을 구현하기위하여 필요한 액션과, 액션생성함수와, 초깃값과, 리듀서함수가 들어있는 파일을 우리는 모듈 이라고 부릅니다. 그리고 이 파일은 src/store/modules 경로에 저장합니다.

src/store/modules/counter.js 라는 새 파일을 만들어주세요.

액션 타입 정의하기

그 파일에, 우리가 카운터 쪽에서 사용할 액션들을 작성해주세요.

src/store/modules/counter.js

// 액션 타입 정의
const CHANGE_COLOR = 'counter/CHANGE_COLOR';
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

Ducks 패턴을 사용 할 땐 위와 같이 액션 이름을 지을 때 문자열의 앞부분에 모듈 이름을 넣습니다. 이는, 다른 모듈에서 작성하게 될 수도 있는 액션들과 충돌되지 않게 하기 위함입니다.

액션 생성함수 정의하기

위에서 정의했던 액션 타입에 따라 액션 생성함수를 만들어주겠습니다.

src/store/modules/counter.js

// 액션 타입 정의
const CHANGE_COLOR = 'counter/CHANGE_COLOR';
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// **** 액션 생섬함수 정의
export const changeColor = color => ({ type: CHANGE_COLOR, color });
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

액션 생성함수를 정의할땐 위와 같이 꼭 앞에 export 를 붙여주세요. 여기서 만든 함수들은 나중에 우리가 컴포넌트에 리덕스를 연동하고 불러와서 사용하게 됩니다.

초기상태와 리듀서 정의

이제 초기상태와 리듀서를 정의해주겠습니다.

src/store/modules/counter.js

// 액션 타입 정의
const CHANGE_COLOR = 'counter/CHANGE_COLOR';
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';

// 액션 생섬함수 정의
export const changeColor = color => ({ type: CHANGE_COLOR, color });
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

// **** 초기상태 정의
const initialState = {
  color: 'red',
  number: 0,
};

// **** 리듀서 작성
export default function counter(state = initialState, action) {
  switch (action.type) {
    case CHANGE_COLOR:
      return {
        ...state,
        color: action.color,
      };
    case INCREMENT:
      return {
        ...state,
        number: state.number + 1,
      };
    case DECREMENT:
      return {
        ...state,
        number: state.number - 1,
      };
    default:
      return state;
  }
}

리듀서 함수의 경우엔, 꼭 export default 를 해주어야합니다. 나중에 스토어를 만들 때, 이 함수를 필요로 합니다.

이제 알록 달록 카윤터에서 필요로하는 리덕스 모듈을 다 만들었습니다. 이제, 스토어를 만들어줄 차례인데요, 이번 프로젝트 같은 경우는, 앞으로 우리가 두개의 리듀서를 만들거여서 (아직은 만들지 않았지만) 여러개의 리듀서들을 합치는 작업을 해주어야 합니다.

combineReducers 로 리듀서 합치기

리듀서가 여러개일대는 redux 의 내장함수인 combineReducers 를 사용하여 리듀서를 하나로 합치는 작업을 합니다. 여러개로 나뉘어진 리듀서들을 서브리듀서 라고 부르고, 하나로 합쳐진 리듀서를 루트리듀서 라고 부릅니다.

modules 디렉토리에 index.js 파일을 다음과 같이 만들어주세요

src/store/modules/index.js

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

export default combineReducers({
  counter,
  // 다른 리듀서를 만들게되면 여기에 넣어줌..
});

이렇게 리듀서를 합치게 되면, 루트 리듀서의 초깃값은 다음과 같은 구조로 됩니다.

{
  counter: {
    color: 'red',
    number: 0,
  },
  // ... 다른 리듀서에서 사용하는 초깃값들
}

스토어 만들기

이제 스토어를 만들어주겠습니다! 우리가 이전에 스토어를 만들때는 createStore 라는 함수를 사용하여 파라미터로는 리듀서를 넣어준다고 했었지요? 그리고, 리덕스의 3 가지 규칙을 배울떄 우리는 "하나의 애플리케이션 안에는 하나의 스토어가 있습니다." 라고 배웠습니다.

스토어는 여러분의 앱이 시작되는 src/index.js 쪽에서 딱 한번, 만드시면 됩니다.

다음과 같이 코드를 작성해보세요.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// **** (1) createStore 와 루트 리듀서 불러오기
import { createStore } from 'redux';
import rootReducer from './store/modules';

import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

// **** (2) 스토어를 만들고 현재 값 확인해보기
const store = createStore(rootReducer);
console.log(store.getState());

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

store.getState() 를 호출하셔서 현재 스토어의 값을 확인해보면 다음과 같이 나타날 것입니다.

Edit colorful-counter

그럼 스토어를 성공적으로, 잘 만드신겁니다!

리덕스 개발자 도구 적용하기

리덕스 개발을 더욱 편하게 하기 위해서 Redux Devtools 라는 크롬 확장프로그램을 활용하시면 정말 편합니다. 크롬 웹스토어 에서 설치를 하시고, 스토어를 만들 때 다음과 같이 코드를 수정해주면 적용됩니다.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// createStore 와 루트 리듀서 불러오기
import { createStore } from 'redux';
import rootReducer from './store/modules';

import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

// **** 리덕스 개발자도구 적용
const devTools =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);

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

Provider 를 사용하여 리액트 프로젝트에 스토어 연동

리액트 프로젝트에 스토어를 연동 할 때에는 react-redux 라이브러리 안에 들어있는 Provider 라는 컴포넌트를 사용합니다. 기존의 JSX 를 Provider 로 감싸고, store 는 props 로 Provider 한테 넣어주면 됩니다.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// createStore 와 루트 리듀서 불러오기
import { createStore } from 'redux';
import rootReducer from './store/modules';
// **** (1) Provider 불러오기
import { Provider } from 'react-redux';

import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

// 리덕스 개발자도구 적용
const devTools =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);

// **** (2) Provider 렌더링해서 기존의 App 감싸주기
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
registerServiceWorker();

connect 함수를 사용하여 컴포넌트에 스토어 연동하기

이제 우리는 컴포넌트에 리덕스 스토어 안에 있는 값이나 액션 함수들을 연동해줄건데요, 이렇게 리덕스와 연동된 컴포넌트를 우리는 컨테이너 컴포넌트라고 부릅니다. 그리고, 그냥 단순히 props 를 전달해주면 그대로 보여주는 컴포넌트들은 프리젠테이셔널 컴포넌트라고 부릅니다.

컨테이너 컴포넌트는 똑똑한 (Smart) 컴포넌트, 프리젠테이셔널 컴포넌트는 멍청한 (Dumb) 컴포넌트라고 부르기도 합니다.

프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트, 이렇게 컴포넌트를 분류하는 방식은 리덕스의 창시자인 Dan Abramov 가 제시한 방법이고, 리덕스를 사용 할 때 이렇게 하면 좋다고 권장하긴 하지만, 무조건 따를 필요까지는 없습니다. (우리는 이 방식대로 개발하긴 할겁니다. 충분히 유용한 흐름입니다.)

이러한 개발 방식에 있어서 최대 장점은 프리젠테이셔널 컴포넌트에선 UI 의 모양새에만 집중 할 수 있고, 컨테이너 컴포넌트쪽에서는 유저 인터랙션쪽에 집중 할 수 있다는 점이 있습니다.

src 디렉토리에 containers 라는 디렉토리를 만들고, PaletteContainer 라는 컴포넌트를 만드세요.

src/containers/PaletteContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Palette from '../components/Palette';
import { changeColor } from '../store/modules/counter';

class PaletteContainer extends Component {
  handleSelect = color => {
    const { changeColor } = this.props;
    console.log('what');
    changeColor(color);
  };

  render() {
    const { color } = this.props;
    return <Palette onSelect={this.handleSelect} selected={color} />;
  }
}

// props 로 넣어줄 스토어 상태값
const mapStateToProps = state => ({
  color: state.counter.color,
});

// props 로 넣어줄 액션 생성함수
const mapDispatchToProps = dispatch => ({
  changeColor: color => dispatch(changeColor(color)),
});

// 컴포넌트에 리덕스 스토어를 연동해줄 때에는 connect 함수 사용
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(PaletteContainer);

컨테이너 컴포넌트를 만들땐, react-redux 안에 들어있는 connect 라는 함수를 사용합니다. 이 함수의 파라미터에 전달해주는 mapStateToProps 는 스토어 안에 들어있는 값을 props 로 전달해주고, mapDispatchToProps 는 액션 생성함수들을 props 로 전달해줍니다.

여기서 mapDispatchToProps 가 조금 헷갈리실 수 도 있는데, 액션생성함수는, 호출한다고 해서 상태에 변화가 일어나는것이 아닙니다. 그 대신에, 액션 객체를 생성해내죠. 그 액션 객체를 스토어한테 전달해주어야 상태에 변화가 발생합니다.

여기 있는 mapDispatchToProps 에서는, color 를 파라미터로 받아와서, 그 값을 가지고 CHANGE_COLOR 액션 객체를 생성한다음에 스토어한테 디스패치 하는 함수를, 컴포넌트의 props 로 전달해주는 것 이랍니다.

connect 함수가 호출되면, 반환되는 값은 특정 컴포넌트에 설정된 props 를 전달해주는 함수입니다. 지금 보시면 connect(...)(PaletteContainer) 이런식으로 호출되었는데, connect() 를 호출해서 반환받은 함수에, PaletteContainer 를 파라미터로 넣어서 호출한것이다 라고 이해하시면 됩니다.

컨테이너 컴포넌트를 다 만드셨다면, App 에서 보여지는 Palette 를 PaletteContainer 로 대체하시구요,

src/App.js

import React, { Component } from 'react';

import './App.css';
import Counter from './components/Counter';
import WaitingList from './components/WaitingList';
import PaletteContainer from './containers/PaletteContainer'; // **** (1) 불러오기

class App extends Component {
  render() {
    return (
      <div className="App">
        <PaletteContainer /> {/* **** (2) 대체하기 */}
        <Counter value={0} color="red" />
        <WaitingList />
      </div>
    );
  }
}

export default App;

Palette 컴포넌트에서 PaletteItem 에 onClick 함수를 제대로 구현해주면, 팔레트에서 다른 색상을 클릭하시면 제대로 선택이 될 것입니다.

src/components/Palette.js

import React from 'react';
import './Palette.css';

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];

const PaletteItem = ({ color, active, onClick }) => {
  return (
    <div
      className={`PaletteItem ${active ? 'active' : ''}`}
      style={{ backgroundColor: color }}
      onClick={onClick}
    />
  );
};

const Palette = ({ selected, onSelect }) => {
  return (
    <div className="Palette">
      <h2>색깔을 골라골라</h2>
      <div className="colors">
        {colors.map(color => (
          <PaletteItem
            color={color}
            key={color}
            active={selected === color}
            onClick={() => onSelect(color)} // **** onClick 구현
          />
        ))}
      </div>
    </div>
  );
};

export default Palette;

Edit colorful-counter

이번엔, 비슷한 원리대로, CounterContainer 도 만들어보겠습니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increment, decrement } from '../store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    this.props.increment();
  };
  handleDecrement = () => {
    this.props.decrement();
  };
  render() {
    const { color, number } = this.props;
    return (
      <Counter
        color={color}
        value={number}
        onIncrement={this.handleIncrement}
        onDecrement={this.handleDecrement}
      />
    );
  }
}

const mapStateToProps = ({ counter }) => ({
  color: counter.color,
  number: counter.number,
});

const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(increment()),
  decrement: () => dispatch(decrement()),
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CounterContainer);
``;

mapStateToProps 부분에서는, state 에 해당하는 부분을 비구조화 할당을 해주었습니다. 그러면, 각 값을 조회 할 떄마다 state. 를 생략해도 되겠죠?

추가적으로 mapDispatchToProps 부분에선, 이번엔 액션이 두개가 있습니다. 계속 dispatch 해주기가 조금 귀찮죠? 그러한 경우엔 bindActionCreators 라는 함수를 사용하면 조금 더 쉽게 할 수 있습니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; // **** (1) 불러오기
import Counter from '../components/Counter';
import { increment, decrement } from '../store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    this.props.increment();
  };
  handleDecrement = () => {
    this.props.decrement();
  };
  render() {
    const { color, number } = this.props;
    return (
      <Counter
        color={color}
        value={number}
        onIncrement={this.handleIncrement}
        onDecrement={this.handleDecrement}
      />
    );
  }
}

const mapStateToProps = ({ counter }) => ({
  color: counter.color,
  number: counter.number,
});

const mapDispatchToProps = dispatch =>
  bindActionCreators({ increment, decrement }, dispatch); // **** (2) bindActionCreators 사용.

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CounterContainer);

이렇게하면, 기존에 했던 actionCreator: (...params) => dispatch(actionCreator(...params) 에 해당하는 작업을 자동으로 해줍니다. 만약에 액션 생성함수가 파라미터를 필요로 하는것이더라도, 정상적으로 작동합니다.

또 다른 방식으로는 mapDispatchToProps 를 함수형태가 아닌 아예 액션생성함수로 이뤄진 객체를 전달해주면, connect 가 발생하게 될 때 bindActionCreators 를 자동으로 해줍니다.

src/containers/CounterContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increment, decrement } from '../store/modules/counter';

class CounterContainer extends Component {
  handleIncrement = () => {
    this.props.increment();
  };
  handleDecrement = () => {
    this.props.decrement();
  };
  render() {
    const { color, number } = this.props;
    return (
      <Counter
        color={color}
        value={number}
        onIncrement={this.handleIncrement}
        onDecrement={this.handleDecrement}
      />
    );
  }
}

const mapStateToProps = ({ counter }) => ({
  color: counter.color,
  number: counter.number,
});

// **** 함수가 아닌 객체 설정시 자동 bindActionCreators 됨
const mapDispatchToProps = { increment, decrement };

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(CounterContainer);

다 작성하셨으면, App 에서 Counter 대신 CounterContainer 를 보여주겠습니다.

src/App.js

import React, { Component } from 'react';

import './App.css';
import WaitingList from './components/WaitingList';
import PaletteContainer from './containers/PaletteContainer';
import CounterContainer from './containers/CounterContainer'; // **** (1) 불러오기

class App extends Component {
  render() {
    return (
      <div className="App">
        <PaletteContainer />
        <CounterContainer /> {/* ****(2) 대체하기 */}
        <WaitingList />
      </div>
    );
  }
}

export default App;

그럼, 카운터쪽 구현도 끝납니다! 버튼들을 눌러보세요.

Edit colorful-counter [counter]

results matching ""

    No results matching ""