├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── actions
│ ├── ActionTypes.js
│ └── index.js
├── components
│ ├── Buttons.css
│ ├── Buttons.js
│ ├── Counter.css
│ ├── Counter.js
│ ├── CounterList.css
│ └── CounterList.js
├── containers
│ ├── App.js
│ └── CounterListContainer.js
├── index.css
├── index.js
├── reducers
│ └── index.js
├── registerServiceWorker.js
└── utils
│ └── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # redux-counter
2 |
3 | 리덕스 기초부터 ImmutableJS, ducks 구조와 redux-actions 를 통한 손쉬운 액션관리까지 다루는 프로젝트
4 |
5 | [튜토리얼](https://velopert.com/3365)
6 |
7 | 각 섹션별로 branch / tag 가 생성되어있습니다.
8 |
9 | ## branch
10 |
11 | - basic-counter: 리덕스 기초를 배우면서 만든 단일 카운터
12 | - multi-counter: 조금 더 복잡해진 상태를 다루는 멀티 카운터
13 | - immutable: ImmutableJS 를 적용하여 더욱 쉬워진 상태관리
14 | - ducks: Ducks 구조와 redux-actions 를 통한 쉬워진 액션관리
15 |
16 | ## tag
17 |
18 | 태그는 각 챕터당 모든 섹션마다 만들어져있습니다.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-counter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "immutable": "^3.8.1",
7 | "react": "^15.5.4",
8 | "react-dom": "^15.5.4",
9 | "react-redux": "^5.0.5",
10 | "redux": "^3.6.0"
11 | },
12 | "devDependencies": {
13 | "react-scripts": "1.0.7"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vlpt-playground/redux-counter/cac1f88e43deb4546762bb1d2d6ac551b368cf38/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/actions/ActionTypes.js:
--------------------------------------------------------------------------------
1 | /*
2 | Action 의 종류들을 선언합니다.
3 | 앞에 export 를 붙임으로서, 나중에 이것들을 불러올 때,
4 | import * as types from './ActionTypes' 를 할 수 있어요.
5 | */
6 |
7 | export const CREATE = 'CREATE';
8 | export const REMOVE = 'REMOVE';
9 |
10 | export const INCREMENT = 'INCREMENT';
11 | export const DECREMENT = 'DECREMENT';
12 | export const SET_COLOR = 'SET_COLOR';
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | action 객체를 만드는 액션 생성자들을 선언합니다. (action creators)
3 | 여기서 () => ({}) 은, function() { return { } } 와 동일한 의미입니다.
4 | scope 이슈와 관계 없이 편의상 사용되었습니다.
5 | */
6 |
7 | import * as types from './ActionTypes';
8 |
9 | export const create = (color) => ({
10 | type: types.CREATE,
11 | color
12 | });
13 |
14 | export const remove = () => ({
15 | type: types.REMOVE
16 | });
17 |
18 | export const increment = (index) => ({
19 | type: types.INCREMENT,
20 | index
21 | });
22 |
23 | export const decrement = (index) => ({
24 | type: types.DECREMENT,
25 | index
26 | });
27 |
28 | export const setColor = ({index, color}) => ({
29 | type: types.SET_COLOR,
30 | index,
31 | color
32 | });
--------------------------------------------------------------------------------
/src/components/Buttons.css:
--------------------------------------------------------------------------------
1 | .Buttons {
2 | display: flex;
3 | }
4 |
5 | .Buttons .btn {
6 | flex: 1;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | height: 3rem;
11 |
12 | color: white;
13 | font-size: 1.5rem;
14 | cursor: pointer;
15 | }
16 |
17 | .Buttons .add {
18 | background: #37b24d;
19 | }
20 |
21 | .Buttons .add:hover {
22 | background: #40c057;
23 | }
24 |
25 | .Buttons .remove {
26 | background: #f03e3e;
27 | }
28 |
29 | .Buttons .remove:hover {
30 | background: #fa5252;
31 | }
--------------------------------------------------------------------------------
/src/components/Buttons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import './Buttons.css';
5 |
6 | const Buttons = ({onCreate, onRemove}) => {
7 | return (
8 |
9 |
10 | 생성
11 |
12 |
13 | 제거
14 |
15 |
16 | );
17 | };
18 |
19 | Buttons.propTypes = {
20 | onCreate: PropTypes.func,
21 | onRemove: PropTypes.func
22 | };
23 |
24 | Buttons.defaultProps = {
25 | onCreate: () => console.warn('onCreate not defined'),
26 | onRemove: () => console.warn('onRemove not defined')
27 | };
28 |
29 | export default Buttons;
--------------------------------------------------------------------------------
/src/components/Counter.css:
--------------------------------------------------------------------------------
1 | .Counter {
2 | /* 레이아웃 */
3 | width: 10rem;
4 | height: 10rem;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | margin: 1rem;
9 |
10 | /* 색상 */
11 | color: white;
12 |
13 | /* 폰트 */
14 | font-size: 3rem;
15 |
16 | /* 기타 */
17 | border-radius: 100%;
18 | cursor: pointer;
19 | user-select: none;
20 | transition: background-color 0.75s;
21 | }
--------------------------------------------------------------------------------
/src/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import './Counter.css';
4 |
5 | const Counter = ({number, color, index, onIncrement, onDecrement, onSetColor}) => {
6 | return (
7 | onIncrement(index)}
10 | onContextMenu={
11 | (e) => {
12 | e.preventDefault();
13 | onDecrement(index);
14 | }
15 | }
16 | onDoubleClick={() => onSetColor(index)}
17 | style={{backgroundColor: color}}>
18 | {number}
19 |
20 | );
21 | };
22 |
23 | Counter.propTypes = {
24 | index: PropTypes.number,
25 | number: PropTypes.number,
26 | color: PropTypes.string,
27 | onIncrement: PropTypes.func,
28 | onDecrement: PropTypes.func,
29 | onSetColor: PropTypes.func
30 | };
31 |
32 | Counter.defaultProps = {
33 | index: 0,
34 | number: 0,
35 | color: 'black',
36 | onIncrement: () => console.warn('onIncrement not defined'),
37 | onDecrement: () => console.warn('onDecrement not defined'),
38 | onSetColor: () => console.warn('onSetColor not defined')
39 | };
40 |
41 | export default Counter;
--------------------------------------------------------------------------------
/src/components/CounterList.css:
--------------------------------------------------------------------------------
1 | .CounterList {
2 | margin-top: 2rem;
3 | display: flex;
4 | justify-content: center;
5 | flex-wrap: wrap;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/CounterList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Counter from './Counter';
3 | import PropTypes from 'prop-types';
4 | import { List } from 'immutable';
5 |
6 | import './CounterList.css';
7 |
8 | const CounterList = ({counters, onIncrement, onDecrement, onSetColor}) => {
9 |
10 | const counterList = counters.map(
11 | (counter, i) => (
12 |
20 | )
21 | );
22 |
23 | return (
24 |
25 | {counterList}
26 |
27 | );
28 | };
29 |
30 | CounterList.propTypes = {
31 | counters: PropTypes.instanceOf(List),
32 | onIncrement: PropTypes.func,
33 | onDecrement: PropTypes.func,
34 | onSetColor: PropTypes.func
35 | };
36 |
37 | CounterList.defaultProps = {
38 | counters: [],
39 | onIncrement: () => console.warn('onIncrement not defined'),
40 | onDecrement: () => console.warn('onDecrement not defined'),
41 | onSetColor: () => console.warn('onSetColor not defined')
42 | }
43 |
44 | export default CounterList;
--------------------------------------------------------------------------------
/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import Buttons from '../components/Buttons';
3 | import CounterListContainer from './CounterListContainer';
4 |
5 | import { connect } from 'react-redux';
6 | import * as actions from '../actions';
7 |
8 | import { getRandomColor } from '../utils';
9 |
10 | class App extends Component {
11 | render() {
12 | const { onCreate, onRemove } = this.props;
13 | return (
14 |
15 |
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | // 액션함수 준비
26 | const mapToDispatch = (dispatch) => ({
27 | onCreate: () => dispatch(actions.create(getRandomColor())),
28 | onRemove: () => dispatch(actions.remove())
29 | });
30 |
31 | // 리덕스에 연결을 시키고 내보낸다
32 | export default connect(null, mapToDispatch)(App);
--------------------------------------------------------------------------------
/src/containers/CounterListContainer.js:
--------------------------------------------------------------------------------
1 | import CounterList from '../components/CounterList';
2 | import * as actions from '../actions';
3 | import { connect } from 'react-redux';
4 | import { getRandomColor } from '../utils';
5 |
6 | // store 안의 state 값을 props 로 연결해줍니다.
7 | const mapStateToProps = (state) => ({
8 | counters: state.get('counters')
9 | });
10 |
11 | /*
12 | 액션 생성자를 사용하여 액션을 생성하고,
13 | 해당 액션을 dispatch 하는 함수를 만들은 후, 이를 props 로 연결해줍니다.
14 | */
15 |
16 | const mapDispatchToProps = (dispatch) => ({
17 | onIncrement: (index) => dispatch(actions.increment(index)),
18 | onDecrement: (index) => dispatch(actions.decrement(index)),
19 | onSetColor: (index) => {
20 | const color = getRandomColor();
21 | dispatch(actions.setColor({ index, color}));
22 | }
23 | })
24 |
25 | // 데이터와 함수들이 props 로 붙은 컴포넌트 생성
26 | const CounterListContainer = connect(
27 | mapStateToProps,
28 | mapDispatchToProps
29 | )(CounterList);
30 |
31 | export default CounterListContainer;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './containers/App';
4 | import './index.css';
5 |
6 | // Redux 관련 불러오기
7 | import { createStore } from 'redux'
8 | import reducers from './reducers';
9 | import { Provider } from 'react-redux';
10 |
11 | // 스토어 생성
12 | const store = createStore(reducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
13 |
14 | ReactDOM.render(
15 |
16 |
17 | ,
18 | document.getElementById('root')
19 | );
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/ActionTypes';
2 | import { Map, List } from 'immutable';
3 |
4 | // 초기 상태를 정의합니다.
5 | const initialState = Map({
6 | counters: List([
7 | Map({
8 | color: 'black',
9 | number: 0
10 | })
11 | ])
12 | })
13 |
14 | // 리듀서 함수를 정의합니다.
15 | function counter(state = initialState, action) {
16 | const counters = state.get('counters');
17 |
18 | switch(action.type) {
19 | // 카운터를 새로 추가합니다
20 | case types.CREATE:
21 | return state.set('counters', counters.push(Map({
22 | color: action.color,
23 | number: 0
24 | })))
25 | // slice 를 이용하여 맨 마지막 카운터를 제외시킵니다
26 | case types.REMOVE:
27 | return state.set('counters', counters.pop());
28 |
29 | // action.index 번째 카운터의 number 에 1 을 더합니다.
30 | case types.INCREMENT:
31 | return state.set('counters', counters.update(
32 | action.index,
33 | (counter) => counter.set('number', counter.get('number') + 1))
34 | );
35 |
36 | // action.index 번째 카운터의 number 에 1 을 뺍니다
37 | case types.DECREMENT:
38 | return state.set('counters', counters.update(
39 | action.index,
40 | (counter) => counter.set('number', counter.get('number') - 1))
41 | );
42 |
43 | // action.index 번째 카운터의 색상을 변경합니다
44 | case types.SET_COLOR:
45 | return state.set('counters', counters.update(
46 | action.index,
47 | (counter) => counter.set('color', action.color))
48 | );
49 | default:
50 | return state;
51 | }
52 | };
53 |
54 | export default counter;
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | export default function register() {
12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
13 | window.addEventListener('load', () => {
14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
15 | navigator.serviceWorker
16 | .register(swUrl)
17 | .then(registration => {
18 | registration.onupdatefound = () => {
19 | const installingWorker = registration.installing;
20 | installingWorker.onstatechange = () => {
21 | if (installingWorker.state === 'installed') {
22 | if (navigator.serviceWorker.controller) {
23 | // At this point, the old content will have been purged and
24 | // the fresh content will have been added to the cache.
25 | // It's the perfect time to display a "New content is
26 | // available; please refresh." message in your web app.
27 | console.log('New content is available; please refresh.');
28 | } else {
29 | // At this point, everything has been precached.
30 | // It's the perfect time to display a
31 | // "Content is cached for offline use." message.
32 | console.log('Content is cached for offline use.');
33 | }
34 | }
35 | };
36 | };
37 | })
38 | .catch(error => {
39 | console.error('Error during service worker registration:', error);
40 | });
41 | });
42 | }
43 | }
44 |
45 | export function unregister() {
46 | if ('serviceWorker' in navigator) {
47 | navigator.serviceWorker.ready.then(registration => {
48 | registration.unregister();
49 | });
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export function getRandomColor() {
2 | const colors = [
3 | '#495057',
4 | '#f03e3e',
5 | '#d6336c',
6 | '#ae3ec9',
7 | '#7048e8',
8 | '#4263eb',
9 | '#1c7cd6',
10 | '#1098ad',
11 | '#0ca678',
12 | '#37b24d',
13 | '#74b816',
14 | '#f59f00',
15 | '#f76707'
16 | ];
17 |
18 | // 0 부터 12까지 랜덤 숫자
19 | const random = Math.floor(Math.random() * 13);
20 |
21 | // 랜덤 색상 반환
22 | return colors[random];
23 | }
--------------------------------------------------------------------------------