1. 생성
create-react-app redux-counter
// redux-counter 디렉리토 이동후 아래 명령실행
yarn add redux react-redux
2. 불필요한 파일 제거
- App.css
- App.js
- App.test.js
- logo.svg
3. 디렉토리 생성
- src 아래 actions, components, containers, reducers, lib 폴더 생성
A. 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트 설명
- 리덕스를 사용하는 프로젝트에서 자주 사용하는 구조
1) 프리젠테이셔널 컴포넌트
- 오직 뷰만 담당
- 옵션 : DOM 엘리먼트와 스타일, 프리젠테이셔널 컴포넌트나 컨테이너 컴포넌트가 있을수 있음
- 리덕스 스토어에 직접 접근할 권한 X
- 오직 prop로만 데이터를 가져올수 있따.
- 대부분 state가 없다. UI관련 state만 존재
- 주로 함수형 컴포넌트로 작성 (state가 있어야 하거나 최적화를 하려고 라이플사이클 메서드가 필요할 때는 클래스형 컴포넌트로 작성)
2) 컨테이너 컴포넌트
- 프리젠테이셔널 컨포넌트들과 컨테이너 컴포넌트들의 관리를 담당
- 내부에 DOM 엘리먼트를 직접적으로 사용할 떄는 없고, 감싸는 용도일때만 사용
- 스타일을 가지고 있지 않다.
- 상태를 가지고 있을때가 많으며, 리덕스에 직접 접근할수 있다.
이 구조의 장점 : 사용자가 이용할 유저 인터페이스와 상태를 다루는 데이터가 분리되어 프로젝트를 이해하기 쉽고, 컴포넌트 재사용률도 높다.
src/components/Counter.js
import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';
const Counter = ({number, color, index, onIncrement, onDecrement, onSetColor}) => {
return (
<div
className="Counter"
onClick={() => onIncrement(index)}
onContextMenu={(e) => {
e.preventDefault();
onDecrement(index);
}}
onDoubleClick={() => onSetColor(index)}
style={{
backgroundColor: color
}}>
{number}
</div>
);
};
Counter.propTypes = {
index: PropTypes.number,
number: PropTypes.number,
color: PropTypes.string,
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
Counter.defaultProps = {
index: 0,
number: 0,
color: 'black',
onIncrement: () => console.warn('onIncrement not defined'),
onDecrement: () => console.warn('onDecrement not defined'),
onSetColor: () => console.warn('onSetColor not defined')
};
export default Counter;
onContextMenu 는 오른쪽 마우스 이벤트 + e.preventDefault() 함수로 메뉴 열리는것을 방지
onDoubleClick 는 마우스 더블 클릭
src/components/CounterList.js
import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';
import './CounterList.css';
const CounterList = ({counters, onIncrement, onDecrement, onSetColor}) => {
const counterList = counters.map(
(counter, i) => (
<Counter
key={i}
index={i}
{...counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
onSetColor={onSetColor}/>
)
);
return (
<div className="CounterList">
{counterList}
</div>
);
};
CounterList.propTypes = {
counters: PropTypes.arrayOf(PropTypes.shape({color: PropTypes.string, number: PropTypes.number})),
onIncrement: PropTypes.func,
onDecrement: PropTypes.func,
onSetColor: PropTypes.func
};
CounterList.defaultProps = {
counters: []
}
export default CounterList;
src/components/Buttons.js
import React from 'react';
import PropTypes from 'prop-types';
import './Buttons.css';
const Buttons = ({onCreate, onRemove}) => {
return (
<div className="Buttons">
<div className="btn add" onClick={onCreate}>생성</div>
<div className="btn remove" onClick={onRemove}>제거</div>
</div>
);
}
Buttons.propTypes = {
onCreate: PropTypes.func,
onRemove: PropTypes.func
};
Buttons.defaultProps = {
onCreate: () => console.warn('onCreate not defined'),
onRemove: () => console.warn('onRemove not defined')
};
export default Buttons;
액션 생성
- 액션은 객체이다
- 모든 액션 객체에는 type 값이 필수로 있어야 한다.
- type은 액션 이름과도 같다. 추후 리듀서가 액션을 전달받으면 이 값에 따라서 해야 할 작업을 결정
src/ations/ActionTypes.js
/*
Action의 종류들을 선언합니다.
앞에 export를 붙이면 나중에 이것들을 불러올 때,
import * as types from './ActionTypes'를 할 수 있어요.
*/
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';
export const CREATE = 'CREATE';
export const REMOVE = 'REMOVE';
액션 생성 함수
src/actions/index.js
/*
action 객체를 만드는 액션 생성 함수들을 선언합니다(action creators).
여기서 () => ({})은 function() { return { } }와 동일한 의미입니다.
*/
import * as types from './ActionTypes';
export const create = (color) => ({
type: types.CREATE,
color
});
export const remove = () => ({
type: types.REMOVE
});
export const increment = (index) => ({
type: types.INCREMENT,
index
});
export const decrement = (index) => ({
type: types.DECREMENT,
index
});
export const setColor = ({index, color}) => ({
type: types.SET_COLOR,
index,
color
});
리듀서 생성
- 리듀서는 액션의 type에 따라 변화를 일으키는 함수
- 초기 상태를 정의가 필요
- 리듀서는 state와 action을 파라미터로 받는다.
- state가 undefined일때(스토어가 생설될때) state 기본값을 initialState로 사용한다.
- action.type에 따라 다른 작업을 하고, 새 상태를 만들어서 반환한다.
- 이때 주의할점은 state를 직접 수정하면 안되고, 기존 상태값에 원하는 값을 덮어쓴 새로운 객체를 만들어서 반환해야한다.
import * as types from '../actions/ActionTypes';
// 초기 상태를 정의합니다.
const initialState = {
counters: [
{
color: 'black',
number: 0
}
]
};
function counter(state = initialState, action) {
// 레퍼런스 생성
const { counters } = state;
switch (action.type) {
case types.CREATE:
return {
counters: [
...counters,
{
color: action.color,
number: 0
}
]
};
case types.REMOVE:
return {
counters: counters.slice(0, counters.length - 1)
};
case types.INCREMENT:
return {
counters: [
...counters.slice(0, action.index), // 선택한 인덱스의 전 아이템들
{
...counters[action.index], // 기존 객체에
number: counters[action.index].number + 1 // 새 number 값 덮어쓰기
},
...counters.slice(action.index + 1, counters.length) // 선택한 인덱스의 다음 아이템들
]
};
case types.DECREMENT:
return {
counters: [
...counters.slice(0, action.index),
{
...counters[action.index],
number: counters[action.index].number - 1
},
...counters.slice(action.index + 1, counters.length)
]
};
case types.SET_COLOR:
return {
counters: [
...counters.slice(0, action.index),
{
...counters[action.index],
color: action.color
},
...counters.slice(action.index + 1, counters.length)
]
};
default:
return state;
}
}
export default counter;
스토어 생성
- 리덕스에서 가장 핵심적인 인스턴스
- 이 안에서 현재 상태가 내장되어 있고, 상태를 업데이트할 때마다 구독 중인 함수들을 호출한다.
- 리액트에서 스토어를 생성할 떄는 보통 프로젝트의 엔트리 포인트인 src/index.js 파일에서 만든다.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './containers/App';
import './index.css';
// Redux 관련 불러오기
import { createStore } from 'redux';
import reducers from './reducers';
// 스토어 생성
const store = createStore(reducers);
ReactDOM.render(
<App/>,
document.getElementById('root')
);
Provider
- react-redux 라이브러리에 내장된 리액트 애플리케이션에 손쉽게 스토어를 연동할 수 있도록 도와주는 컴포넌트
- 이 컴포넌트를 불러온후 연동할 프로젝트의 최상위 컴포넌트를 감싸고, Provider 컴포넌트의 props로 store를 넣어주면된다.
...
import { Provider } from 'react-redux';
...
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
CounterListContainer 컴포넌트
- react-redux 라이브러리 connect 함수를 사용하여 컴포넌트를 스토어에 연결시킨다.
connect([mapStateToProps], [mapDispatchToProps]. [mergeProps])
// mapStateToProps : store.getState() 결과값인 state를 파라미터로 받아 컴포넌트의 props로 사용할 객체를 반환
// mapDispatchToProps : dispatch를 파라미터로 받아 액션을 디스패치하는 함수들을 객체안에 넣어서 반환한다.
// mergeProps : state와 dispatch가 동시에 필요한 함수를 props로 전달해야 할 떄사용하는데, 일반적으로 사용하지 않는다.
src/containers/CounterListContainer.js
import CounterList from '../components/CounterList';
import * as actions from '../actions';
import {connect} from 'react-redux';
import getRandomColor from '../lib/getRandomColor';
// store 안의 state 값을 props로 연결해줍니다.
const mapStateToProps = (state) => ({counters: state.counters});
/*
액션 생성자를 사용하여 액션을 생성하고,
해당 액션을 dispatch하는 함수를 만든 후, 이를 props로 연결해줍니다.
*/
const mapDispatchToProps = (dispatch) => ({
onIncrement: (index) => dispatch(actions.increment(index)),
onDecrement: (index) => dispatch(actions.decrement(index)),
onSetColor: (index) => {
const color = getRandomColor();
dispatch(actions.setColor({index, color}));
}
})
// 데이터와 함수들이 props로 붙은 컴포넌트 생성
const CounterListContainer = connect(mapStateToProps, mapDispatchToProps)(CounterList);
export default CounterListContainer;
src/lib/getRandomColor.js
// 13가지 색상 중 랜덤으로 선택하는 함수입니다.
export default function getRandomColor() {
const colors = [
'#495057',
'#f03e3e',
'#d6336c',
'#ae3ec9',
'#7048e8',
'#4263eb',
'#1c7cd6',
'#1098ad',
'#0ca678',
'#37b24d',
'#74b816',
'#f59f00',
'#f76707'
];
// 0부터 12까지 랜덤 숫자
const random = Math.floor(Math.random() * 13);
// 랜덤 색상 반환
return colors[random];
}
이렇게 리덕스와 연동된 컴포넌트를 CounterListContainer 안에 담아 이를 내보낸후, App 컴포넌트에서 Counter 대신 CounterListContainer 컴포넌트를 불러와 렌더링한다.
src/containers/App.js
import React, {Component} from 'react';
import Buttons from '../components/Buttons';
import CounterListContainer from './CounterListContainer';
import getRandomColor from '../lib/getRandomColor';
import { connect } from 'react-redux';
import * as actions from '../actions';
class App extends Component {
render() {
const { onCreate, onRemove } = this.props;
return (
<div className="App">
<Buttons
onCreate={onCreate}
onRemove={onRemove}
/>
<CounterListContainer/>
</div>
);
}
}
// 액션 생성 함수 준비
const mapToDispatch = (dispatch) => ({
onCreate: () => dispatch(actions.create(getRandomColor())),
onRemove: () => dispatch(actions.remove())
});
// 리덕스에 연결시키고 내보냅니다
export default connect(null, mapToDispatch)(App);
이제 버튼은 컨테이너 없이 바로 액션 생성 함수를 만들고 리덕스에 연결시켜버린다.
결과물)
'WEB > REACT' 카테고리의 다른 글
10. 미들웨어 (0) | 2019.07.11 |
---|---|
9. Immutable & Ducks & 예제 (0) | 2019.07.11 |
7. 리덕스 (상태 관리 라이브러리) (0) | 2019.07.08 |
6. 예제 - 일정관리 (0) | 2019.06.25 |
5. 리액트 컴포넌트 스타일링 방식 (0) | 2019.06.21 |