-
React.js(5)_React ReduxSPA/React.js 2022. 9. 28. 15:25
해당 게시글은 2022.04.28에 깃허브로 작성되었습니다.
1. Redux란?
Redux는 Javascript 앱을 위한 예측 가능한 상태 저장소다.
Redux의 본질은 Node.js 모듈이며, 아래 기본 세 가지 원칙부터 알아보자.
2. Redux의 기본 개념: 세 가지 원칙
(1) Single source of truth
동일한 데이터는 항상 같은 곳에서 가져온다.
store라는 하나뿐인 데이터 공간이 있다.
(2) State is read-only
React에서 setState 메소드를 활용해야만 상태 변경이 가능했듯이
Redux에서도 action이라는 객체를 통해서만 상태 변경이 가능하다.
(3) Changes are made with pure functions
변경은 순수 함수로만 가능하다.
reducer와 연관되는 개념이다.
3. Redux의 동작 원리
(1) Store
Store(스토어)는 상태가 관리되는 오직 하나의 공간이다.
Component와는 별개로 Store라는 공간이 있어서 그 Store 안 앱에서 필요한 상태를 담는다.
Component에서 상태 정보가 필요할 때 Store에 접근한다.
- state
실제 정보가 담겨있는 그릇
- Reducer
현재 상태와 Action 객체를 받아 필요하다면 새로운 state를 return하는 함수이다.const initialState = { counter: 1, } function reducer(state = initialState, action) { switch (action.type) { case INCREMENT: return { counter: state.counter + 1, } default: return state } }
- dispatch
(1) reducer를 호출함으로써 action을 통해 변경된 state값을 변경해준다.
(2) subscribe를 호출함으로써 state값이 변경될 때마다 render를 호출하여 갱신한다.
이벤트 트리거라고 생각하면 이해하기 쉽다.
- subscribe
render를 등록하여 state값이 변경될 때마다 render 함수를 호출하여 업데이트한다.
- getState
state의 값을 가져와 render에게 전달한다.(2) Action
상태에 변화가 필요할 때 Action을 일으켜야 한다.
Action은 Javascript 객체 형식으로 되어있으며 type 필드를 반드시 가지고 있어야 한다.
{ type: 'ACTION_CHANGE_USER', // 필수 payload: { // option name: '강동원', age: 100 } }
Action 생성 함수는 Action 객체를 만들어주는 함수이며 화살표 함수로도 표현 가능하다.
function addTodo(data) { return { type: 'ADD_TODO', data, } }
그럼 아래 그림을 보며 Redux의 동작 흐름을 한번 정리해보자.
1. state에는 정보가 담겨있고 이를 render에서 요청하면 getState를 통해 데이터를 전달한다.
이렇게 state - getState - render 는 데이터를 요청하고 전송하는 관계이다.
2. 다음으로 사용자가 Submit 버튼을 누르면 action이 실행되어 dispatch에게 변경된 state값을 전달한다.
3. 전달된 state값은 dispatch가 reducer에게 전달함으로써 state의 값을 변경해준다.
4. render가 subscribe에 등록되면 state의 값이 변경될 때마다 subscribe가 render를 호출하여 반영한다.
4. Redux는 언제 쓰는 게 좋을까?
- 앱의 여러 위치에서 필요한 많은 양의 상태들이 존재할 때(전역 상태가 필요하다고 느껴질 때)
- 상태들이 자주 업데이트될 때
- 상태를 업데이트하는 로직이 복잡할 때
- 앱이 중간 또는 큰 사이즈의 코드를 갖고 있고 많은 사람들에 의해 코드가 관리될 때
- 상태가 업데이트되는 시점을 관찰할 필요가 있을 때
5. React Redux
React는 소문같고 Redux는 언론같다는 표현이 있다.
React가 Component로 만들어진 사회라고 생각했을 때
어떤 Component에서 변화가 생겼을 때 아래 이미지와 같이 모든 Component로 변화가 전파된다.
그러나 이렇게 되니 모든 사람이 필요없는 소문을 듣게 된다는 단점이 생긴다.
또 소문이 전파되기 위해서는 집집마다 연결돼있어야 한다. 즉, Component끼리 서로 props와 이벤트를 매개로 연결돼있어야 한다.
이런 때에 필요한 게 Redux이다.
앞서 말했듯 Redux는 언론과도 같아 어떤 Component가 구성원들에게 전달하고 싶은 정보가 있으면 Redux라는 언론사에 제보를 하고
Redux는 전체 Component에게 방송을 한다.
그러나 그렇다고 해서 모든 문제가 해결되는 것은 아니다.
방송은 필요하지 않은 사람에게도 전달된다는 비효율성을 여전히 갖고 있다.
이때 React와 Redux를 연결해주는 react-redux 라이브러리를 사용하면 그 소식이 필요한 Component에게만 소식을 전할 수 있다.
(1) 기본 세팅
우선 실습을 위해 React Redux 코딩을 진행할 폴더를 생성해주고 본인이 사용하는 코드 에디터에 해당 폴더를 열어준다.
그 후, 터미널을 열어 npx create-react-app . 명령을 실행함으로써 React 프로젝트를 새로 생성 해보자.
그 후, Redux를 사용하기 위해 다음 명령어로 Redux를 설치해보자.
npm install redux
index.js와 App.js, index.css 파일을 제외하고 src 디렉토리에 있는 모든 파일을 삭제한다.
그 후, 각 div 간 명확히 구분이 되도록 index.css를 다음과 같이 수정하자.
div {border: 5px solid #764abc; margin: 10px; color: #764abc;}
(2) Component 생성
src 디렉토리에 components 폴더를 생성하고
그 안에 AddNumber.jsx, AddNumberRoot.jsx, DisplayNumber.jsx, DisplayNumberRoot.jsx 파일을 생성하자.
각 Component는 아래와 같이 작성한다.
// src/components/AddNumberRoot.jsx import React, { Component } from 'react'; import AddNumber from './AddNumber'; export default class AddNumberRoot extends Component { render() { return ( <div> <h1>Add Number Root</h1> <AddNumber/> </div> ) } }
// src/components/DisplayNumberRoot.jsx import React, { Component } from 'react'; import DisplayNumber from './DisplayNumber'; export default class DisplayNumberRoot extends Component { render() { return ( <div> <h1>Display Number Root</h1> <DisplayNumber/> </div> ) } };
우선 AddNumber와 DisplayNumber Component가 각각 AddNumberRoot와 DisplayNumberRoot Component의 내부에 위치하도록 작성했다.
이어서 AddNumber Component를 작성해보자.
// src/component/AddNumber.jsx import React, { Component } from 'react' import store from '../store' export default class AddNumber extends Component { state = {size: 1} render() { return ( <div> <h1>Add Number</h1> <input type="button" value="+" onClick={function(){ store.dispatch({type:'INCREMENT', size: this.state.size}); }.bind(this)}/> <input type="text" value={this.state.size} onChange={function(e){ this.setState({size:Number(e.target.value)}) }.bind(this)}/> </div> ) } }
redux를 사용할 수 있게 해줄 store.js를 import한다. 순서상 store.js는 아직 생성하지 않았다.
그 후, AddNumber에서 조절할 값인 size state의 기본값을 1로 설정하고
클릭할 때마다 값이 증가되는 + 버튼을 생성한다.
이어서 사용자가 원하는 size를 입력할 수 있게끔 text type의 input 또한 생성하고
input값이 변경될 때마다 실행되는 onChange 함수를 삽입하여 변경된 값을 매개변수인 e를 이용하여 e.target.value로 가져오고
setState로 size state를 대입하여 변경해준다.
변경된 size state를 + 버튼에게 넘기고 redux 메소드인 dispatch를 실행시켜 해당 state의 size값으로 변경한다.
해당값은 text type이므로 Number() 함수로 변경하는 작업을 거쳐야 한다.
이어서 DisplayNumber Component를 작성해보자.
// src/components/DisplayNumber.jsx import React, { Component } from 'react'; import store from '../store'; export default class DisplayNumber extends Component { state = {number: store.getState().number} constructor(props){ super(props); store.subscribe(function(){ this.setState({number:store.getState().number}); }.bind(this)); } render() { return ( <div> <h1>Display Number</h1> <input type="text" value={this.state.number} readOnly/> </div> ) } }
역시 store와 연결하여 redux를 사용하고
getState()를 이용해 현재 store에 저장되어있는 number의 값을 받아와 state에 저장한다.
subscribe는 constructor 내부에서 실행되는데
store를 등록함으로써 state의 값이 변경될 때마다 해당 값을 받아와 적용시키도록 한다.
그 후 number의 값을 input 창을 통해 보여지도록 한다.
(3) Store 생성 및 연결
그 다음 Redux Store를 만들기 위해 src 디렉토리 안에 store.js 파일을 생성하고 다음과 같이 작성한다.
// src/store.js import {createStore} from 'redux'; export default createStore(function(state, action) { if(state === undefined){ return {number:0} } if(action.type === 'INCREMENT'){ return {...state, number: state.number + action.size} } return state; }, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
redux로부터 store를 생성하는 createStore를 import한 뒤
createStore 함수의 첫번째 인자로 reducer 함수를 받는다.
이 reducer 함수는 첫번째 인자로 현재 상태인 state,
두번째 인자로 변경되는 상태인 action을 받는다.
state가 undefined일 때 {number:0}으로 state를 초기화해준다.
createStore의 두번째 인자는 크롬 확장 프로그램인 Redux DevTool을 사용하기 위해 지정한 값이다.
위 코드에서 이 부분을 다시 보자.
if(action.type === 'INCREMENT'){ return {...state, number: state.number + action.size} }
AddNumber Component에서 지정한 dispatch의 type을 INCREMENT로 설정한 이유이다.
+ 버튼을 클릭할 때마다 해당 조건에 부합하여 실행되고
return값의 ...state는 기존 state의 모든 값을 새로 만들어지는 객체에 그대로 추가하되
number의 값만 변경할 때 사용하는 문법이다.
즉, 기존 상태에서 number에 DisplayNumber Component를 통해 변경된 number값에
AddNumber Component를 통해 변경된(action) action가 적용되어 더해지는 것이다.
이렇게 Redux를 통해 변경되는 number와 number값을 store를 통해 연결시켜줌으로써
어디에서나 number와 size값이 변경되면 바로 반영되게끔 효율적으로 처리했다.
원래라면 props와 state를 연결하고 또 연결해서 복잡하게 이루어질 작업이 Redux를 통해 간편화되었다.
(4) Redux에 종속된 기능 제거
더 나아가 현재 AddNumber Component를 보면 Redux의 store를 필요로 한다.
즉, 애플리케이션에서 사용하는 Redux의 상태에 의존하기 때문에 AddNumber Component를 다른 곳에서 사용할 수 없다.
이렇게 되면서 AddNumber Component는 재사용 가능한 Component가 아닌 상태가 되고 우리가 React를 사용하는 가장 큰 이유를 잃게 된다.
이 문제를 해결하는 방법은 Component를 래핑(wrapping)하는 것 이다.
AddNumber Component를 감싸는 새로운 Component를 만들고 그 Component에 Redux의 store를 핸들링하는 기능을 부여한다.
React Ajax에서 배웠던 Presentational Component와 Container Component와 동일한 의미이다.
그럼 이제 AddNumber Component를 감쌀 컨테이너인 래퍼 Component를 만들어보자.
src 디렉토리에 containers라는 폴더를 생성하고 그 안에 AddNumber.jsx 파일을 생성하여 다음과 같이 작성한다.
// src/containers/AddNumber.jsx import React, { Component } from 'react'; import AddNumber from '../components/AddNumber'; export default class extends Component { render() { return ( <AddNumber/> ) } }
이 Component는 AddNumber Component를 감싸는 익명 Component이고
return에 감쌀 Component인 AddNumber Component를 반환한다.
그리고 AddNumberRoot Component에서는 이제 AddNumber Component를 직접 사용하지 않고
AddNumber를 감싸는 익명 Component를 사용하도록 import 부분을 변경한다.
// src/components/AddNumberRoot.jsx import AddNumber from '../containers/AddNumber';
큰 구조는 변경하였고 이제 기능을 분리해보자.
// src/containers/AddNumber.jsx import React, { Component } from 'react'; import AddNumber from '../components/AddNumber'; import store from '../store'; export default class extends Component { render() { return ( <AddNumber onClick={function(size){ store.dispatch({type:'INCREMENT', size:size}); }.bind(this)}></AddNumber> ) } }
// src/components/AddNumber.jsx <input type="button" value="+" onClick={function(){ this.props.onClick(this.state.size); }.bind(this)}/>
container AddNumber Component에서는 store를 import하고
presentational Component에서 store를 다뤘던 onClick부분의 dispatch를 가져왔다.
이어서 presentational AddNumber Component에서는 import했던 store를 지우고
container Component로부터 넘겨받은 props로 화면에 보여지게끔 하였다.
이어서 DisplayNumber Component도 똑같이 적용하자.
container 디렉토리에 DisplayNumber Component를 생성하고 다음과 같이 작성한다.
// src/containers/DisplayNumber.jsx import React, { Component } from 'react'; import DisplayNumber from '../components/DisplayNumber'; import store from "../store"; export default class extends Component { state = { number:store.getState().number } constructor(props){ super(props); store.subscribe(function(){ this.setState({number:store.getState().number}); }.bind(this)); } render() { return ( <DisplayNumber number={this.state.number} unit={this.props.unit}></DisplayNumber> ) } }
이어서 presentational DisplayNumberComponent에는store import와 state부분의 코드를 모두 제거했다.
// src/components/DisplayNumber.jsx import React, { Component } from 'react'; export default class DisplayNumber extends Component { render() { return ( <div> <h1>Display Number</h1> <input type="text" value={this.props.number} readOnly/> </div> ) } }
그리고 DisplayNumberRoot.jsx의 DisplayNumber import 부분을 아래와 같이 수정한다.
import DisplayNumber from '../containers/DisplayNumber';
그럼 이전과 똑같이 실행되지만 Redux와 분리함으로써 Presentational Component로의 의미를 가지고 재사용성을 높였다.
(5) React Redux 도입
드디어 React Redux를 도입해보자.
터미널에 다음 명령어를 입력하여 react-redux를 설치하자.
npm install react-redux
react-redux에서는 우선 store를 통해 Provider를 Component에 공급해야 한다.
index.js를 다음과 같이 수정하자.
// src/index.js import React from 'react'; import ReactDOM from 'react-dom/client'; import './App.css'; import App from './App'; import { Provider } from 'react-redux'; import store from './store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> <App /> </Provider> );
최상위에 있는 App Component를 Provider Component로 감쌌다.
Provider Component는 store라는 props를 요구한다.
이렇게 하면 App Component를 포함한 모든 하위 Component는 Provider에서 공급한 store에 접근할 수 있게 되고 하위 Component에서 따로 store에 접근할 필요가 없어졌다.
다음으로 Container DisplayNumber Component를 다음과 같이 수정하여 react-redux를 적용해보자.
// src/containers/DisplayNumber.jsx import DisplayNumber from '../components/DisplayNumber'; import { connect } from 'react-redux'; export default connect()(DisplayNumber);
connect를 react-redux로부터 import하고
connect 함수의 결괏값을 export한다.
connect.js에 들어가면 react-redux의 초창기 모습을 간단한 코드로 표현한 문서를 볼 수 있다.
보면서 원리를 이해해보는 것도 좋은 경험인 것 같다.
이제 Container AddNumber Component도 react-redux를 적용해보자.
// src/containers/AddNumber.jsx import AddNumber from '../components/AddNumber'; import { connect } from 'react-redux'; export default connect()(AddNumber);
현재까지는 connect 함수의 두번째 괄호의 인자로 Component를 주면 그 Component를 래핑하는 Component를 만드는 과정을 진행했다.
(6) mapStateToProps
이어서 첫번째 괄호의 인자에 대해 알아보고 적용해보도록 하자.
공식 문서에는 첫번째 괄호의 인자의 첫번째 인수로 mapStateToProps 함수, 두번째 인수로 mapDispatchToProps 함수가 온다고 되어있다.
우선 mapStateToProps 함수를 구현해보자.
// src/container/DisplayNumber.jsx import DisplayNumber from '../components/DisplayNumber'; import { connect } from 'react-redux'; function mapReduxStateToReactProps(state) { return { number: state.number }; } function mapReduxDispatchToReactProps() { return {}; } export default connect(mapReduxStateToReactProps, mapReduxDispatchToReactProps)(DisplayNumber);
이 함수는 Redux의 store값이 변경될 때마다 호출되도록 약속돼있다.
따라서 Redux store값이 변결될 때 변경된 값을 받아 Component의 props로 전달하면 되며
react-redux의 connect 함수의 첫번째 인자인 mapStateToProps는 Redux store의 변경사항을 통보받아 Component의 props로 전달하는 역할을 하는 함수다.
(7) mapDispatchToProps
이번에는 connect 함수의 두번째 인자인 mapDispatchToProps 함수에 대해 알아보자.
Container AddNumber Component를 다음과 같이 수정하자.
// src/containers/AddNUmber.jsx import AddNumber from '../components/AddNumber'; import { connect } from 'react-redux'; function mapDispatchToProps(dispatch) { return { onClick:function(size){ dispatch({type:'INCREMENT', size: size}); } } } export default connect(null, mapDispatchToProps)(AddNumber);
AddNumber Component는 전달되는 이벤트 props만 존재하고 상태를 전달하는 props가 없기 때문에 connect 함수의 첫번째 인자로 null을 전달했다.
첫번째 인자를 작성하지 않고 mapDispatchToProps만 전달하면 mapDispatchToProps가 첫번째 인자(mapStateToProps)로 작동하기 때문이다.
mapDispatchToProps의 인자는 react-redux의 dispatch 함수이다.
이를 통해 Redux store에서 dispatch 작업을 할 수 있다.
이렇게 대략적으로 React와 그와 관련된 추가 라이브러리에 대해 배워봤다.
앞으로는 응용방법에 대해서 배우고 활용해나가는 일만 남았다.
'SPA > React.js' 카테고리의 다른 글
[React] Firebase로 CRUD 구현하기 (0) 2022.11.04 [React] Firebase로 회원가입/로그인, 로그아웃 (0) 2022.10.26 React.js(4)_React Ajax (0) 2022.09.28 React.js(3)_Ajax (0) 2022.09.28 React.js(2)_React Router (0) 2022.09.28