React.js Redux
05 Feb 2021 | ReactReact.js Redux
-
Redux 개요
- Redux는 애플리케이션 state를 관리하기 위한 오픈소스 Javascript 라이브러리이다
- Redux는 Javascript App을 위한 예측가능한 state container이다
- Redux는 React의 작동 원리인 state에 대한 변화를 쉽게 예측할 수 있도록 도와준다
- React 뿐만 아니라 Angular, jQuery, Vanilla JS 등 다양한 곳에서 적용이 가능하다
- 앱의 규모가 커질수록 local state를 props의 형태로 전달하기 위해서는 연결된 chain layer들을 모두 거쳐야 한다
- 위와 같은 경우 Context API를 쓸 수도 있다. 다시 말해, Redux만이 유일한 수단은 아니다
- Redux의 3 원칙
- Single source of truth : global state는 트리 구조에서 하나의 저장소에 저장된다
- State is read-only : state를 변경하는 유일한 방법은 action을 발생시키는 것이다
- Changes are made with pure functions : action에 의한 state tree 변환을 지정하려면 pure reducer를 작성해야 한다. 즉, 리듀서는 action과 state를 받아서 다음 state를 반환하는 순수 함수여야한다
출처 : Udemy / React - The Complete Guide
-
Redux 기본 사용법
- npm install –save redux (redux 패키지)
- npm install –save react-redux (react와 redux를 연결)
- Provider는 리액트의 컴포넌트들이 Redux store에 접근할 수 있도록 연결해준다 (상위 컴포넌트에서 사용)
- store는 애플리케이션의 state를 저장하는 객체이다
import { createStore } from 'redux'; import { Provider } from 'react-redux'; import reducer from 'path/reducer'; // store 생성, reducer를 넣어준다 const store = createStore(reducer); ReactDOM.render( // 하위 컴포넌트를 Provider로 감싸고 store 연결 <Provider store={store}> <App /> </Provider> document.getElementById('root') );
- reducer는 현재 state와 action을 취하고 새로운 state를 반환하는 순수 함수이다. state는 불변성을 유지해야한다. 즉, state의 직접 수정 없이 복사본을 만들어 수정해야한다. 그 이유는 redux의 변경 감지 알고리즘에 있는데, 자세한 것은 링크 참조.
const initialState = { counter: 0, }; // ES6 default parameter, state가 undefined일 경우 initialState로 초기화 const reducer = (state = initialState, action) => { if(action.type === 'INCREMENT') { return { counter: state.counter + 1, } } return state; }; export default reducer;
- action은 일반 객체로 state의 변화가 필요할 때, action을 발생시킨다. type 필드를 필수적으로 가지고 있어야한다. 일반 객체이기 때문에 얼마든지 함수로 생성하여 사용할 수 있다.
- dispatch는 store의 내장 메소드로 action을 파라미터로 받아 store의 reducer에 넘겨주는 역할을 한다.
- connect() 메소드는 Provider 컴포넌트로 감싼 하위 컴포넌트들이 store에 접근하게 하는데 하위 컴포넌트에서 사용한다
- mapStateToProps : connect 메소드의 첫번째 인자로 들어가는 함수, store의 state를 조회해서 props로 넣어준다. 파라미터로 state를 받아온다. null 값으로 자동 호출을 막을 수 있다.
- mapDispatchToProps : connect 메소드의 두번째 인자로 action을 dispatch하는 함수를 만들어 props로 넣어준다, dispatch를 파라미터로 받는다.
- 이름은 꼭 저게 아니어도 되지만 일반적으로 통용되는 것으로 맞춰주는게 좋다
import React from 'react'; import { connect } from 'react-redux'; class Counter extends React.Component { redner() { return ( <button onClick={this.props.onIncrementCounter}> {this.props.ctr} </button> ); } } const mapStateToProps = state => { return { ctr: state.counter, }; } const mapDispatchToProps = dispatch => { return { onIncrementCounter: () => dispatch({ type: 'INCREMENT'}), }; } export default connect(mapStateToProps,mapDispatchToProps)(Counter);
-
payload
- 위 코드에서는 reducer의 action 실행문에 의해 state의 값이 1씩 증가하게 된다. 하지만, 저렇게 하드코딩 되어있는 부분을 사용자가 지정한 임의의 값이 증가되도록 하려면 어떻게 해야될까?
- action은 일반 객체라고 했다. action을 dispatch할 때, 단순히 type 외의 다른 필드 값을 넘겨주고 reducer에서 받아서 사용하기만 하면 된다
const mapDispatchToProps = dispatch => { return { onIncrementCounter: () => dispatch({ type: 'INCREMENT', value: 5}), }; }
const reducer = (state = initialState, action) => { if(action.type === 'INCREMENT') { return { counter: state.counter + action.value, } } return state; };
- 저렇게 해도 작동은 잘 된다. 하지만, type 이외의 전달하고 싶은 값들은 되도록 payload 필드명을 이용하여 전달하는 것이 좋다. 그렇게 통일되어 있으니깐. createAction 메소드를 이용하여 action을 생성할 때도 두 번째 인자값으로 paylaod 값을 받도록 설정되어 있다.
-
Updating State Immutably
- Redux의 핵심 규칙 중 하나는, state는 read-only이기에 불변성(immutable)을 유지해야 한다고 했다.
- 위의 코드들과 달리 state가 복수개 존재한다면 기존 state를 통으로 복사해 필요한 부분만 수정하면 될 것이다.
- 여기서 ES6의 Object Spread Operator가 사용된다. Object.assign()으로 객체 복사를 해도 된다.
const initialState = { counter: 0, result: [], }; const reducer = (state = initialState, action) => { if(action.type === 'INCREMENT') { return { ...state, // 객체 복사 counter: state.counter + action.value, } } if(action.type === 'STORE') { return { ...state, // 객체 복사 results: state.results.concat({ // id는 임시 id id: new Date(), value: state.counter }), }// 기존배열을 직접 수정하지 않기 위해 concat 사용 } return state; }; export default reducer;
-
Constant Action Type
- 위의 코드들은 action을 하드코딩해서 직접 써넣었다
- 이것을 상수로 설정하여 오타 등 에러가 날 확률을 미연에 방지할 수 있다
export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; // 상수가 담긴 js 파일, 사용처에서 import하여 사용한다
import * as actionTypes from 'path/actions'; const reducer = (state = initialState, action) => { if(action.type === actionTypes.INCREMENT) { // 상수 사용 return { ...state, // 객체 복사 counter: state.counter + action.value, } }
import * as actionTypes from 'path/actions'; const mapDispatchToProps = dispatch => { return { onIncrementCounter: () => dispatch({ type: actionTypes.INCREMENT, value: 5}), }; }
-
Combining Multiple Reducers
- 애플리케이션의 규모가 커짐에 따라 한 개의 리듀로서만 모든걸 다루기에는 한계가 있다
- 리듀서를 쪼개서 하나의 리듀서처럼 사용할 수 있다
- 이를 위해서는 redux 패키지의 combineReducers가 필요하다
// store를 정의한 최상위 컴포넌트에서 import { createStore, combineReducers } from 'redux'; import counterReducer from 'path/counter'; import resultReducer from 'path/result'; const rootReducer = combineReducers({ ctr: counterReducer, res: resultReducer, }); const store = createStore(rootReducer);
const mapStateToProps = (state) => { return { // state의 블럭 단위가 한 단계씩 늘어난다 ctr: state.ctr.counter, storedResults: state.res.results, }; };
- reducer를 쪼개면 그에 따라 state도 나뉘게 된다. 자신에게 없는 state 값이 필요한 경우엔 어떻게 해야할까?
- 리듀서는 함수 안에서 global state에 접근할 수 없기때문에 값을 받아와야한다
- 하나의 방법으로 action을 이용하여 값을 전달할 수 있다
<button onClick={() => this.props.onStoreResult(this.props.ctr)}></button> const mapDispatchToProps = (dispatch) => { return { onStoreResult: (result) => dispatch({ type: actionTypes.STORE, result: result }) }// action을 이용하여 state를 전달 }
const reducer = (state = initialState, action) => { if (action.type === actionType.STORE) { return { ...state, results: state.results.concat({ id: new Date(), value: action.result, }), }; } }
참고 자료
Udemy - React The Complete Guide