React(생활코딩)_22일차_ReactRedux(4)_React-Rdux 도입

Lina Hongbi Ko·2023년 10월 20일
0

React_생활코딩

목록 보기
23/23

우리는 리액트의 컴포넌트에 리덕스를 적용시켜보고 또, 종속성까지 제거해보았다.

오늘은 본격적으로 'React-Redux'에 대해 배워보자.

✏️ React-Redux

📍 What is React-Redux?

React Redux is the official Redux UI binding library for React.

Redux와 React Redux 모두 '상태관리'를 위해 쓰이는 Redux이다. 하지만, React Redux는 Redux팀에서 공식적으로 관리, 배포하고 있는 'React 전용 Redux 패키지'이다.

React Redux는 Redux를 기초로 설계되어있다.

📍 React Redux 설치하기

1. create-react-app으로 React 프로젝트와 React Redux 설치를 동시에 하기

React 프로젝를 생성하는 것부터 시작해서 React Redux를 사용할 예정일때 이 방법을 쓰면 유용하다. React Redux를 별도로 설치하지 않아도 React Redux가 포함된채 프로젝트를 시작할 수 있기 때문이다.

npx create-react-app my-app --template redux

2. React Redux만 추가로 설치하기

React 프로젝트를 생성할 당시에 React Redux를 함께 설치하지 못하거나, 이미 진행 중인 프로젝트에 뒤늦게 React Redux를 추가할 때 사용한다.

npm i react-redux

or

yarn add react-redux

이렇게 React-Redux를 설치하는 방법까지 알아보았다면, 본격적으로 사용해보자.

📍 React Redux가 필요한이유

저번시간에 우리는 종속성을 제거하는 실습을 통해 컨테이너 컴포넌트를 만드는 작업을 해보았다. 그리고 이 실습을 통해 컨테이너 컴포넌트로써 익명 래퍼 컴포넌트를 만들었다. 하지만 이 컴포넌트를 도입하면서 어떻게 보면 코드도 많아지고 할 일도 많아졌다.

컨테이너에서 어떤 이벤트에 디스패치 해야 하는 경우, 이벤트에 디스패치를 거는 작업을 추가로 해야했다.

// containers / AddNumber.js 파일

import React, { Component } from "react";
import AddNumber from "../components/AddNumber";
import store from "../components/store";

export default class extends Component {
  render() {
    return (
      <AddNumber
        onClick={function (size) {
          store.dispatch({ type: "INCREMENT", size: size });
        }.bind(this)}
      ></AddNumber>
	... 생략 ...
// components / AddNumber.js 파일

import React, { Component } from "react";

class AddNumber extends Component {
  state = {
    size: 1,
  };
  render() {
    return (
      <div>
        <h1>Add Number</h1>
        <input
          type="button"
          value="+"
          onClick={function () {
            this.props.onClick(this.state.size);
          }.bind(this)}
        ></input>
        <input
          type="text"
          value={this.state.size}
          onChange={function (e) {
            this.setState({ size: Number(e.target.value) });
          }.bind(this)}
        ></input>
      </div>
      ... 생략 ...

그리고 props에 상태를 전달 하는 경우에는 컨테이너 컴포넌트에서 리덕스 스토어를 구독해 데이터가 바뀌면 그때마다 렌더링이 다시 되도록 코드를 작성했다.

// containers / DisplayNumber.js 파일

import React, { Component } from "react";
import DisplayNumber from "../components/DisplayNumber";
import store from "../components/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}></DisplayNumber>;
  }
}
// components / DisplayNumber.js 파일

import React, { Component } from "react";

class DisplayNumber extends Component {
  render() {
    return (
      <div>
        <h1>Display Number</h1>
        <input type="text" value={this.props.number} readOnly></input>
      </div>
      ... 생략 ...

그리고 한 가지 더 예를 추가하자면,
DisplayNumberRoot에서 unit이라는 props에 'kg'이라는 값을 전달해서 이 값이 DisplayNumber에 반영되어야 하는 경우를 살펴보자.

// DisplayNumberRoot.js 파일

import React, { Component } from 'react';
import DisplayNumber from '../containers/DisplayNumber';

export default class DisplayNumberRoot extends Component {
	render(){
    	return(
        	<div>
            	<h1>Display Number Root</h1>
                <DisplayNumber unit="kg"></DisplayNumber>
            </div>
        );
    }
}
// components / DisplayNumber.js 파일

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>{this.props.unit}</input>
            </div>
        );
    }
}

unit이라는 props를 DisplayNumberRoot로부터 받아 DisplayNumber의 텍스트 입력상자 뒤에 출력하려고 한다. props값이 DisplayNumber에 무사히 전달되려면, containers/DisplayNumber에도 props를 전달해야한다.

// containers / DisplayNumber.js 파일

import DisplayNumber from '../components/DisplayNumber';
import React, { Component } from 'react';
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 <DisplayNumbr number={this.state.number} unit={this.props.unit}></DisplayNumber>
    }
}

중간에 익명 래퍼 컨테이너가 끼었기 때문에 props를 전달하는 작업을 거쳐야 한다는 것이다. 그렇다면 예를 들어 props가 백만개라면, 백만개 모두 전달해야하는 불편함이 생긴다.

하지만, react-redux를 이용하면, 이러한 것들을 자동화시켜주고 편리하게 도와준다.

📍 React Redux 사용

자, 이제 본격적으로 react-redux를 사용해보자.

1. Provider 사용

react-redux 설치가 끝나면 어플리케이션에 react-redux를 도입해야 한다.

그렇게 하기 위해서는 우선,
'스토어'를 컴포넌트에 공급해야한다. react-redux에서는 Provider를 통해 스토어를 공급받는다.

index.js파일을 수정해보자.

// index.js 파일

import React 'react';
import ReactDOM From 'react-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
	<Provider store={store}>
    	<App />
    </Provider>
, document.getElementById('root'));

최상위에 있는 App 컴포넌트를 Provider 컴포넌트로 감싼다. 이 Provider 컴포넌트는 UI가 있는 컴포넌트가 아니고, App 컴포넌트가 Provider의 맥락 안에 존재하기 때문에 Provider 컴포넌트의 지배를 받는다.

그리고 store이라는 props를 요구하는데, 이 store에 import한 store을 넣어주면 된다.

이렇게하면, 최상위인 App 컴포넌트를 포함한 모든 하위 컴포넌트들이 Provider에서 공급한 store에 접근할 수 있게 된다. 그렇게 되면 하위 컴포넌트에서는 따로 store에 직접 접근할 필요가 없다.

2. connect 사용

이제, DisplayNumber를 react-redux로 변경해보자.

// containers / DisplayNumber.js 파일

import DisplayNumber from '../components/DisplayNumber';
import {connect} from 'react-redux';

export default connect()(DisplayNumber);

connect를 import하였다.
그리고 기존에는 DisplayNumber를 래핑한 익명 컴포넌트를 export 했었는데, 지금은 connect 함수의 결과값인 함수의 반환값을 export 했다.

export default connect()(DisplayNumber);

connect 함수를 호출한 다음, 그 결괏값을 다시 호출한 결과를 export한 것이다. connect 뒤에 괄호가 두개이고, 두번째 괄호 안에 DisplayNumber를 인자로 호출한다.

connect함수의 결괏값 함수가 이전에 우리가 구현했던 익명 래퍼 컴포넌트 (컨테이너 컴포넌트)를 대신하는 것이다.

다시한번 자세히 설명하자면,

connect라는 함수는 반환값이 있다. 그 반환값 또한 함수이다. 그렇기 때문에 실행할때 connect 함수의 반환값을 다시 호출해서 그 인자에 DisplayNumber 컴포넌트를 전달했다. 그렇게 되면 결과적으로 DisplayNumber 컴포넌트를 래핑한 컴포넌트가 반환된다.

자, 이제 의문이 드는 점이

그럼 앞에서 언급했던, DisplayNumberRoot에서 전달한 props는 어떻게 components / DisplayNumber로 전달할까?
이전에는 containers / DisplayNumber에 직접 props를 넣어서 전달해줘야 했지만, react-redux에서는 이 작업을 conncet함수가 자동으로 해준다.

즉, 아무런 처리를 하지 않아도 부모인 DisplayNumberRoot가 props로 전달하면 그것을 자식인 DisplayNumber 컴포넌트에 자동으로 전달한다.

AddNumber도 react-redux 방식으로 변경해보자.

import AddNumber from '../components/AddNumber';
import {connect} from 'react-redux';

export default connect()(AddNumber);

.
.

💡 connect 함수의 두번째 괄호 안에 인자로 컴포넌트를 주면, 그 컴포넌트를 래핑하는 컴포넌트를 만드는 함수가 connect함수!!💡

.
.
이렇게만 했을때에는 정상적으로 동작하지 않는다.
이제 AddNumber 컨테이너에게 디스패치하는 작업을 하고, DisplayNumber 컨테이너에서는 props를 전달받는 작업을 해보자.

3. mapStateToProps 사용

connect 함수를 이용해서 래핑 컴포넌트를 만드는 법을 알았다.

그리고,

connect 함수에는 첫번째 인자와 두번째 인자에 들어가는 함수가 있는데,

connect 함수의 첫번째 인자에는 "mapStateToProps 함수"가 들어간다. (두번째 인자에는 "mapDispatchToProps함수"가 들어가지면 이건 밑에서 설명)

connect 함수는 인자가 없을 수도 있지만, 만약에 하나만 있다면 그것은 mapStateToProps이다.

이제 mapStateToProps함수를 구현해보자.

// containers / DisplayNumber.js 파일

import DisplayNumber from '../components/DisplayNumber';
import { connect } from 'react-redux';

function mapReduxStateToReactProps(state) {
	return {
    	number : state.number;
    };
}

export default connect(mapReduxStateToReactProps)(DisplayNumber);

생코님은 책에서 mapStateToProps 함수를 설명할때, 처음 이 함수에 대해 배울때 그 이름이 헷갈리기 쉬우니 위처럼 작성했다.

위에서 작성한 그대로, 이 함수는
1. connect함수의 첫번째 인자로 들어가며
2. 리덕스의 state를 리액트의 props로 연결하는 역할을 한다.

mapReduxStateToReactProps 함수는 어떤 객체를 반환하고, 이 객체는 DisplayNumber 컴포넌트의 props로 전달된다. 프로퍼티 이름은 컴포넌트에 전달하고자하는 props의 이름으로 지정하면 된다.

여기서는 DisplayNumber에 props 이름이 number 이었기 때문에 반환값 객체 프로퍼티 이름을 number로 지정했다.

그리고 mapReduxStateToProps 함수는 리덕스의 스토어 값이 변경될때마다 호출되도록 약속되어 있다. 그래서 리덕스 스토어의 값이 변경됐을때 변경된 값을 받아 컴포넌트의 props로 전달하면 되는 것이다.

또한, 변경된 스토어값은 이 함수의 인자로 전달되고, 인자로 전달된 state를 통해 변경된 number값을 사용한다.

따라서, mapStateToProps 함수(위의 코드의 이름에선 mapReduxStateToReactProps)를 사용했기 때문에 전에 작성했던 구독하기, state쓰기 등등을 사용하지 않아서 편리하다 :)

💡 react-redux의 connect함수의 첫 번재 인자인 mapStateToProps는 리덕스 스토어의 변경사항을 통보받아 컴포넌트의 props로 전달하는 역할을 하는 함수이다.

4. mapDispatchToProps 사용

이제 conncet함수의 두번째 인자인 mapDispatchToProps 함수에 대해 알아보자.

AddNumber의 컨테이너 컴포넌트를 수정해보자.

// containers / AddNumber.js 파일

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 컴포넌트는 전달되는 이벤트 props만 존재하고, 상태를 젇날하는 props가 없기 때문에 connect 함수의 첫번째 인자를 null로 지정하고 두번째 인자로 mapDispatchToProps라는 함수를 전달했다.

mapDispatchToProps 함수의 첫번째 인자는 'dispatch 함수'이다. 그리고 이 dispatch 함수는 리덕의 dispatch 함수이다.

react-redux가 mapDispatchToProps 함수를 호출할 때, 인자로 dispatch 함수를 전달하는 것이다.

💡 mapDispatchToProps 함수 내에서 dispatch 함수를 호출해 리덕스 스토어에 dispatch 작업을 할 수 있게 한다.

mapDispatchToProps의 반환값도 객체이다. 이 반환값 객체는 mapStateToProps 함수와 마찬가지로 컴포넌트의 props로 전달되고, AddNumber 컴포넌트의 props 이름이 onClick 이므로, 객체에 onClick 이라는 프로퍼티를 지정한 것이다.

💡💡 정리하자면,
connect 함수의 첫번째 인자인 mapStateToProps는 리덕스의 store를 리액트 컴포넌트의 props로 매핑하는 함수이고,
두번째 인자인 mapDispatchToProps는 리덕스의 dispatch를 리액트 컴포넌트인 props로 연결하는 함수이다.

📍 수업을 마치며

드디어 이 책의 마지막 섹션이다.

책에서는 리덕스를 만든 분이 react-redux를 설명하기 위해 코드로 간단하게 설명해놓은 구현체를 살펴보는 내용이 나와 있다.

const ConnectedCounter = connect(
	// Given Redux state, return props
    state => ({
    	value: state.counter,
    }),
    // Given Redux dispatch, return callback props
    dispatch => ({
    	onIncrement() {
        	dispatch({ type: 'INCREMENT' })
        }
    })
)(Counter)

위 코드는 connect를 사용하는 예제이다. connect를 호출하고, 호출할때 두 개의 인자를 전달하고 connect 함수의 반환값을 다시 호출하였다.

이때 Counter라고 하는 컴포넌트를 인자로 전달하였는데, 함수 2개가 전달되었다.

첫번째 함수를 살펴보면, 인자가 state 이고, 리턴값이 {value: state.counter}인 함수이다.

state => ({
	value: state.counter,
}),

두번재 함수는, 인자가 dispatch 이고, 리턴값이 {onIncrement(){ dispatch({type:'INCREMENT'})}}인 함수이다.

dispatch => ({
	onIncrement() {
    	dispatch({ type: 'INCREMENT' })
    }
})

connect 함수가 하는 일은 새로운 함수를 반환하는 것 뿐이다. 그리고 새로운 함수는 인자로 WrappedComponent를 받는데, 이게 Counter 컴포넌트이다.

그리고 connect 함수의 결괏값 함수가 반환한 값이 바로 컨테이너(wrapper) 컴포넌트이고, 이 컴포넌트가 Counter 컴포넌트를 래핑하고 있는 것이다.

function connect(mapStateToProps, mapDispatchToProps){

	// connect 함수는 함수를 반환한다.
    return function(WrappedComponent) {
    	
        // connect 함수의 결괏값 함수는 컨테이너 컴포넌트를 리턴한다.
        return class extends React.Component {
        	
            // 컨테이너 컴포넌트의 render 메서드에서는 WrappedComponent를 반환한다.
            render() {
            	return {
                	
                    // taht renders your component
                    <WrappedComponent>
                    
                    	// 아래 코드로 컨테이너 컴포넌트로 주입된 props를 Wrapper 컴포넌트에 전달한다.
                        {...this.props}
                        
                        // 리덕스 store의 state를 WrapperComponent의 props로 전달한다.
                        {...mapStateToProps(store.getState(), this.props)}
                        
                        // 이벤트를 WrapperComponent의 props로 전달한다.
                        {...mapDispatchToProps(store.dispatch, this.props)}
                        
                        // 컴포넌트가 적용됐을때 호출된다. 리덕스 스토어를 구독한다.
                        componentDidMount() {
                        	this.unsubscribe = store.subscribe(this.handleChange.bind(this))
                        }
                        
                        // 컴포넌트가 종료됐을때 호출된다. 구독을 취소한다.
                        componentWillUnmount() {
                        	this.unsubscribe()
                        }
                        
                        // 리덕스 스토어가 변경되면 강제로 render를 호출한다.
                        handleChange() {
                        	this.forceUpdate()
                        }
                        
                    </WrappedComponent>
                }
            }
        }
    }
}

위의 코드에서 {...this.props} 부분은 컨테이너 컴포넌트로 주입된 props를 WrappedComponent에 전달하는 작업이다.

그리고 앞에서 언급했던 세번째 예제를 보자.

// components / DisplayNumberRoot.js 파일

import React, { Component } from 'react';
import DisplayNumber from '../containers/DisplayNumber';

export default class DisplayNumberRoot extends Component {
	render() {
    	return(
        	<div>
            	<h1>Display Number Root</h1>
                <DisplayNumber unit="kg"></DisplayNumber>
            </div>
        );
    }
}

DisplayNumberRoot 컴포넌트에 unit 이라는 props 값으로 'kg'을 전달한다. 이 'kg'값은 react-redux를 사용하지 않을 때는 수동으로 DisplayNumber에 전달해야 했다.

// containers / DisplayNumber.js 파일

import DisplayNumber from '../components/DisplayNumber';
import React, { Component } from 'react';
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>
    }
}

이러한 작업을 자동을 처리하는 코드가 바로 WrappedComponent의 {...this.props} 부분인 것이다.

그리고,

 {...mapStateToProps(store.getState(), this.props)}

리덕스 스토어의 상태를 리액트 컴포넌트의 props로 전달한다. 그리고 호출할때는 첫번째 인자로 리덕스 스토어의 state를 주입한다.

 {...mapDispatchToProps(store.dispatch, this.props)}

이벤트를 리액트 컴포넌트의 props로 전달하는 역할을 한다. 그리고 호출할때는 첫번째 인자로 dispatch 함수를 전달한다.

// containers / DisplayNumber.js 파일

import DisplayNumber from '../components/DisplayNumber';
import {connect} from 'react-redux';

function mapStateToProps(state) {
	return { number: state.number };
}

export default connect(mapStateToProps)(DisplayNumber);

위 mapStateToProps 함수의 반환값객체이다.
객체의 프로퍼티값은 우리가 생성하고자 하는 컴포넌트의 props 이름이다.
그리고 인자인 state리덕스 스토어의 state값이다.
이 함수의 첫번째인자에는 state값이 들어가고 두번째 인자는 'this.props'이다. 필요한 경우, 두번째 인자를 통해 전달된 이 props를 사용할 수 있다.

그리고 mapDispatchToProps 함수.
이 함수는 인자스토어의 dispatch 함수를 전달한다. 이 dispatch 함수가 있으면 컨테이너 컴포넌트에서 redux를 import하고 store에서 dispatch 함수를 가져올 필요가 없기 때문에 편리하다.

다음으로,
componentDidMount는 이 컴포넌트가 적용됐을때 호출되도록 약속되어 있는 메서드이고 componentWillUnmount는 컴포넌트가 제거될때 호출되도록 약속돼 있는 메서드이다.

componentDidMount가 호출될때 스토어를 구독하고 handleChange를 인자로 전달한다. handleChange는 컴포넌트를 강제로 업데이트해서 render 메서드가 호출되게 하는 코드이다. 그래서 이 코드로 인해 리덕스 스토어의 state가 바뀌면 handleChange가 호출될 것이고, 그러면 컴포넌트가 렌더링 되면서 여기에 있는 값이 새롭게 주입되는 것이다.

그리고 컴포넌트가 더는 사용되지 않으면 구독취소를 통해 어플리케이션의 성능을 높인다. 이를 대신해주는 것이 react-redux의 connect 함수이다.

자, 우리는 이 코드들을 통해 conncet라는 것이 불편한 작업을 많이 줄여주는 API 라는 것을 알았다. 이밖에도 중요한 효과는

  1. 우리가 등록해놓은 props에 한해서만 구독하기 때문에 불필요한 render함수의 호출이 적다.
  2. shouldComponentUpdate를 통해 수동으로 해야 할 일도 react-rdux가 대신한다.

책에서는 이밖에도 '타임 트래블링(time traveling)'과 '핫 리로드(hot reload)'라는 기능도 있다고 말한다.

타임 트래블링은 데이터의 상태를 보관하는 기능이고, 핫 리로드는 코드를 바꿔도 어플리케이션 상태가 독립적으로 마지막 상태를 그대로 유지시키는 기능이다.

그리고 Undo, Redo 같은기능도 리덕스 쉽게 구현할 수 있도록 도와준다고 한다.

이밖의 기능들에 대해서는 추후에 살펴보도록 해야겠다.


출처
생활코딩! 리액트 프로그래밍 책
https://react-redux.js.org/introduction/why-use-react-redux
https://velog.io/@iamhayoung/React-Redux-React-Redux-%EC%9E%85%EB%AC%B8-Provider-Connect-mapStateToProps-mapDispatchToProps#%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%BD%94%EB%93%9C

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글