자바스크립트로 만든 애플리케이션들을 위한 예측 가능한 상태의 저장소
1. Single Source of Truth
하나의 Store를 사용한다.
2. State is Read-only
어플리케이션에서 state를 직접 변경할 수 없다. 무조건 action이 dispatch되어야 한다.
3. Changes are made with pure Functions
action 객체를 처리하는 함수를 reducer
라고 부른다. reducer
는 정보를 받아서 상태를 어떻게 업데이트 할 지 정의한다. reducer
는 '순수 함수'로 작성되어야 한다. 즉, 비동기적인 처리를 하면 안된다. 즉, 네트워크 및 데이터베이스 접근 X, 인수 변경 X. 같은 인수로 실행된 함수는 언제나 같은 결과를 반환하고, 순수하지 않은 API는 사용할 수 없다.(Date.now(), Math.random() 등)
.
동기(synchronous: 동시에 일어나는): 동기는 말 그대로 동시에 일어난다는 뜻이다. 요청과 그 결과가 동시에 일어난다는 약속인데, 바로 요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져아 한다.
-> 요청과 결과가 한 자리세어 동시에 일어남
-> A노드와 B노드 사이의 작업 처리 단위(transaction)를 동시에 맞추겠다.
비동기(Asynchronous: 동시에 일어나지 않는): 비동기는 동시에 일어나지 않는다를 의미한다. 요청과 결과가 동시에 일어나지 않을거라는 약속이다.
-> 요청한 그 자리에서 결과가 주어지지 않음.
-> 노드 사이의 작업 처리 단위를 동시에 맞추지 않아도 된다.
동기와 비동기의 장단점
-> 동기방식
은 설계가 매우 간단하고 직관적이지만 결과가 주어질 때까지 아무것도 못하고 대기해야 하는 단점이 있고, 비동기방식
은 동기보다 복잡하지만 결과가 주어지는데 시간이 걸리더라도 그 시간 동안 다른 작업을 할 수 있으므로 자원을 효율적으로 사용할 수 있는 장점이 있다.
import { createStore} from 'redux';
// STORE -> GLOBALISED STATE
// ACTION INCREMENT
const increment = () => {
return {
type: 'INCREMENT'
}
}
// ACTION DECREMENT
const decrement = () => {
return {
type: 'DECREMENT'
}
}
// REDUCER
const counter = (state = 0, action) => {
switch (action.type) :
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
}
};
let store = createStore(counter);
// Display it in the console
store.subscribe(() => console.log(store.getState()));
// DISPATCH
store.dispatch(increment());
store.dispatch(decrement());
상태가 기본적으로 전부 store
에서 집중 관리 된다. 커다란 JSON
의 결정체 정도라고 생각할 수 있다. 규모가 클 경우에는 상태를 카테고리별로 분류하는 경우가 일반적이다.
// 세션과 관련된 것
session: {
loggedIn: true,
user: {
id: "114514",
screenName: "@mpyw",
},
},
// 표시 중인 타임라인에 관련된 것
timeline: {
type: "home",
statuses: [
{id: 1, screenName: "@mpyw", text= "hello"},
{id: 2, screenName: "@mpyw", text= "bye"},
],
},
// 알림과 관련된 것
notification: [],
}
Store
및 Store
에 존재하는 State
는 아주 신성한 것이다. React 컴포넌트 같은 하등한 것이 직접 접근하면 안된다. 직접 접근하기 위해서는 Action 이라는 의식을 거쳐야 하는데, 이벤트 드리븐과 같은 개념이다.
-> Store
에 대해 뭔가 하고 싶은 경우엔 Action을 발행한다.
-> Store
의 문지기가 Action의 발생을 감지하면, State
가 경신된다.
Action
은 기본적으로 아래와 같은 포멧을 가지고 있는 Object
이다.
{
type: "액션의 종류를 한번에 식별할 수 있는 문자열 혹은 심볼",
payload: "액션의 실행에 필요한 임의의 데이터",
}
예를 들어서 카운터의 값을 2배 늘리는 경우, 아래와 같은 Object
가 될 것이다. 머릿 부분에 @@myapp/
라고 prefix를 붙인 것은 다른 사람이 쓴 코드와의 충돌을 피하기 위함이다.
{
type: "@@myapp/ADD_VALUE",
payload: 2,
}
그런데 매 번 이런 Object
를 만드는 것을 수작업으로 하는 것은 괴로운 일이다. 그리고 매번 Action 명을 문자열로 쓰는 것도 불편하다. 그래서 이걸 편하게 하기 위해서 상수와 함수를 준비하는 것이 일반적이다. 외부 파일이 참고할수도 있으니 제대로 export 해둬야한다.
export const ADD_VALUE = '@@myapp/ADD_VALUE';
export const addValue = amount => ({type: ADD_VALUE, payload: amount});
store
의 문지기 역할이라고 생각하면 된다. 함수형 프로그래밍에서 Reducer
라는 용어는 합성곱을 의미하지만, Redux에 한해서는 아래와 같이 이전 상태와 Action을 합쳐, 새로운 state를 만드는 조작을 의미한다.
import { ADD_VALUE } from './actions';
export default (state = {value: 0}, action) => {
switch (action.type) {
case ADD_VALUE:
return { ...state, value: state.value + action.payload };
default:
return state;
}
}
Reducer
의 default 인수에서 정의된다.state
는 그 자체의 값으로 대체되는 것이 아니라, 새로운 것이 합성되는 것처럼 쓰여진다.반환된 state
는 store에 바로 반영 되어 아래와 같이 변화한다.
{
value: 2,
}
트위터처럼 대규모 개발에 Reducer
를 미세하게 분할하는 경우 Redux에서 제공하는 combineReducers
함수를 이용하여 아래와 같이 쓴다.
import { combineReducers } from 'redux';
const sessionReducer = (state = {loggedIn: false, user: null}, payload) => {
// 생략
};
const timelineReducer = (state = {type: "home", statuses: []}, payload) => {
// 생략
};
const notificationReducer = (state = [], payload) => {
// 생략
};
export default combineReducers({
session: sessionReducer,
timeline: timelineReducer,
notification: notificationReducer,
})
위와 같이 하면, Reducer 분할에 쓰인 Key가 그대로 State 분할에도 쓰인다. 또한 실제로 각각의 reducer의 정의 자체도 다른 파일로 나누는 것이 일반적이다.
React의 컴포넌트 자체는 Redux의 흐름에 타는 것이 불가능하다. 흐름에 타기 위해서는 ReactRedux에 의해 제공되는 connect
라고 불리는 함수를 이용해서 아래와 같이 쓴다.
>>>>>>>>>> 함수형
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
const Counter = ({ value, dispatchAddValue }) => (
<div>
Value: {value}
<a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
<a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
</div>
export default connect(
state => ({ value: state.value }),
dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) }))(Counter)
>>>>>>>>>> 클래스형
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addValue } from './actions';
class Counter extends Component {
render() {
const { value, dispatchAddValue } = this.props;
return (
<div>
Value: {value}
<a href="#" onClick={e => dispatchAddValue(1)}>+1</a>
<a href="#" onClick={e => dispatchAddValue(2)}>+2</a>
</div>
);
}
}
export default connect(
state => ({ value: state.value }),
dispatch => ({ dispatchAddValue: amount => dispatch(addValue(amount)) })
)(Counter)
먼저 컴포넌트가 store로부터 뭔가 정보를 받는 경우에, 그걸 props
를 통해서 받는다. props
는 immutable(불변의)하다. 즉, 상태가 변경될 때마다 새로운 컴포넌트가 다시 만들어진다는 것이다. 이것을 염두에 둔 후에 connect
를 실행하고 있는 주변 코드를 보자.
state
를 어떻게 props
에 엮을지 정한다. **(이 동작을 정의하는 함수는 mapStateToProps 라고 불린다.)dispatch
를 어떻게 props
에 엮을지 정한다. (이 동작을 정의하는 함수는 mapDispatchToProps 라고 불린다.)props
를 받을 컴포넌트를 정한다.connect(mapStateToProps, mapDispatchToProps)(Component)
라고 쓰인걸 보면 좀 독특하다고 생각할 수 있겠지만, 결국 최종적인 반환값은 4번과 같다.
여얼 리덕스 마스터 하신겁니까?
~