React에서 상태 관리와 Redux

률루랄라·2020년 5월 22일
18
post-thumbnail

1. React에서의 상태 관리

React로 application 만들 때 필수적으로 상태를 관리하는 상황을 마주치는데 기본적으로 state에 기본 값을 지정하고 setState를 사용하여 data를 추가,제거 그리고 수정한다. 독립적인 component내에서 뿐만 아니라 부모-자식, 부모-자식-자식의 자식- 자식의 자식의 자식, 혹은 그 역순과 사돈/팔촌 관계끼리도 state를 공유하고 수정하고 다시 공유할 수 있다.

stateprops로 서로 넘겨주며 data를 공유하고 setState를 통해 data를 수정하는 등 간단한 방법으로 React에서 상태 관리를 할 수 있지만 application의 규모가 복잡해지고 커질수록 상태 관리는 더욱 더 힘들어진다.

무엇이 이 간단한 방법을 복잡하고 어렵게 만드는지 알아보기 위해선 우선 React의 특성을 알아볼 필요가
있다.

1.1. React의 특성: 렌더링 조건


React의 특성 중 하나인 렌더링 조건을 살펴보면 아래와 같다.
1. Props가 변경되었을 때
2. State가 변경되었을 때
3. forceUpdate() 를 실행하였을 때
4. 부모 컴포넌트가 렌더링되었을 때

1번, 2번, 4번을 주목해보자.
어플리케이션 속 각각의 컴포넌트들은 propsstate가 변경될 때 그리고 그 컴포넌트의 부모 컴포넌트가 렌더링되었을 때 다시 렌더링이 된다.
자세한 예로 살펴보자.

//App.js

import React from 'react';
import './App.css';

import RightButton from './components/rightButton.component';
import LeftButton from './components/leftButton.component';
import Result from './components/result.component';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 0,
    };
  }

  handleIncrease =() => {
    this.setState({
      value: this.state.value +1
    })
  }
  handleDecrease =() => {
    this.setState({
      value: this.state.value -1
    })
  }

  render() {
    return (
      <div className="App">
        <Result sum={this.state.value}/>
	//현재 값을 보여주는 component
        <LeftButton decreasing={this.handleDecrease}/>
          //클릭 할 때 마다 숫자를 1씩 감소시키는 컴포넌트
        <RightButton increasing={this.handleIncrease} />
          //클릭 할 때 마다 숫자를 1씩 증가시키는 컴포넌트
      </div>
    );
  }
}

export default App;


// result.js
import React from 'react';

const Result = props => {
  return <div>{props.sum}</div>;
};

export default Result;



//leftButton.js
import React from 'react';

const LeftButton = props => {
  return (
    <button type="button" onClick={props.decreasing}>
      빼기
    </button>
  );
};

export default LeftButton;



//rightButton.js
import React from 'react';

const RightButton = props => {
  return (
    <button type="button" onClick={props.increasing}>
      더하기
    </button>
  );
};

export default RightButton;

위 처럼 <Result />, <LeftButton /> 그리고 <RightButton />라는 3개의 자식 컴포넌트를 가지고 있는 App 컴포넌트로 구성된 어플리케이션이 있다.
이 어플리케이션의 유저가 view에서 RightButton 컴포넌트속에 있는 "더하기" 버튼을 클릭하면 세 가지의 일이 발생한다.

  1. handleIncrease 라는 eventhandler가 작동하여 App 컴포넌트의 statevalue의 값이 0 에서 1로 변경되고 그럼으로 App 컴포넌트가 다시 렌더링 된다.

  2. Result 컴포넌트의 propsthis.state.value가 변경되었음으로 sum의 이름으로 넘어가는 props가 변경된다.그럼으로 Result 컴포넌트가 렌더링 된다.

  3. App컴포넌트가 렌더링 되었음으로 그 자식 컴포넌트들인 <Result />, <LeftButton /> 그리고 <RightButton /> 모두가 다시 렌더링 된다.

이렇게 렌더링 조건 중
1번 Props가 변경되었을 때 2번 State가 변경되었을 때 4번 부모 컴포넌트가 렌더링되었을 때
이 세가지를 충족하기 때문에 위와 같은 렌더링이 발생하게 되는 것이다.

1.2. React 특성의 부작용: 불필요한 렌더링


렌더링이란 컴포넌트를 view로 보여주는 작업이다.
위의 일련의 과정을 살펴보면 view에, 즉 렌더링 결과에 영향을 받는 컴포넌트는
App컴포넌트와 <Result /> 컴포넌트인 것을 알 수 있다.
<LeftButton /> 그리고 <RightButton />의 view는 state가 아무리 변한다 한들 아무 영향이 없지만 단순히 그들의 부모 컴포넌트인 App컴포넌트가 렌더링이 되었다는 이유로 다시 렌더링이 되었다.
이렇게 view 결과, 즉 렌더링 결과의 영향이 없음에도 불구하고 렌더링되는 것은 매우 불필요한데 그 이유는 application의 성능 손실을 발생시키기 때문이다.
지금이야 당장에야 2개의 컴포넌트의 성능 손실이라고 생각할 수 있겠지만 만약 그 수가 수백개 수천, 수억개가 된다면 application의 퍼포먼스는 굉장히 낮은 퀄리티가 될 것이다.
더 나아가 부모-자식 관계의 컴포넌트가 지금과 같은 depth가 아닌
1번-2번-3번-4번-5번의 depth로 되어있고 state가 렌더링의 영향을 주는 컴포넌트가 1번과 5번 뿐이라고 가정해보자.
2,3,4번 컴포넌트는 단지 1번의 stateprops로 넘겨주기만 하고 그들의 view에 영향을 주지 않음에도 1번의 state가 변경된다면 props가 변경되었다는 조건 때문에 불필요한 렌더링이 발생하게 되고 이는 성능 손실로 이어진다. 이뿐만 아니라 규모가 커질수록 data flow는 복잡해지기도 하며 버그나 오류에 대한 트랙킹이 불가능해질 수 있다.
그럼 이러한 복잡성과 성능손실을 어떻게 개선할 수 있을까?

2. Redux


Redux는 React (뿐만 아니라 다른 라이브러리나 프레임워크)에서 사용 가능한 application 상태관리 library이다. 여러개의 middleware들도 제공하며 대표적으로 logger, thunk, saga, persist 등이 있다.

2.1. Redux는 무엇인가?


Redux는 application 전체의 상태(state)를 편리하게 관리하기 위해 사용하는 라이브러리 중 하나이다.
앞서 말한 data flow의 복잡성 개선과 불필요한 렌더링을 막아주는 장점이 있다.
처음 사용할 때 많은 두려움이 있지만 컨셉 자체는 어렵지 않다.
리덕스 사용 전과 후를 식당을 예로 들어 한번 설명해보겠다.

2.2. Redux 사용전


4개의 테이블(컴포넌트)와 4명의 종업원들(view)이 있고 주방에서 음식(props)이 완료되면 매니저(state)가 음식을 서버에게 전달하고 종업원 서빙(rendering)을 하는 식당(어플레이케이션)이 있다.
하지만 이 식당에는 하나의 불변의 규칙이 있는데 음식의 조리가 완료가 되면 모든 종업원들이 와서 본인 담당 테이블의 음식이 맞는지 확인을 하여야한다.

그리고 3번 테이블에서 손님이 핫도그를 주문(event)을 주문하였고 그 핫도그가 주방에서 완성(변경된 state)되어 현재 매니저 손에 들려있다. 그래서 매니저가 모든 종업원을 불렀고 4명의 종업원은 본인 테이블 주문이 맞는지 확인하고 담당하는 종업원이 서빙을 하였다.
여기서 이 식당의 비효율성이 보여졌다. 그 불변의 규칙으로 인해 1, 2, 4번 종업원은 불필요하게 움직임(불필요한 rendering)이 생겼고 이는 인력낭비(성능저하)란 결과로 이어졌다.

2.3. Redux 사용 후

이를 지켜보던 식당의 사장은 조금 더 효율적인 운영을 위해 리모델링을 결정한다.
누군가에 의해 개발된 새로운 시스템(library)를 도입하는데 바로 AI 자동화 시스템(Redux)를 도입한다.

먼저 사장은 테이블 마다 원격으로 주문할 수 있는 원격 주문 시스템을 각각 설치하고 이 주문기계는 wifi를 통해서 AI로봇과 연동되어 있다. 손님이 테이블에서 원격주문을 하게 되면 모든 정보가 AI로봇에게 입력된다. 주방에서 음식 조리가 완료되면 하나의 로봇이 테이블로 서빙해주는데 이 로봇의 성능은 매우 뛰어나서 일일히 각 테이블에 가서 맞는지 확인할 필요도 없이 해당 테이블로 서빙을 해준다. (전원 공급만 연결 시켜주면 된다.)

이게 내가 생각해본 Redux의 비슷한 모습이다. 단 하나의 연동된 로봇으로 인해 하나의 테이블에 관련한 일에 대해서 다른 테이블의 담당자가 알 필요가 없을 뿐더러 관여하지도 않는다. 결국 식당은 효율적으로 운영될 것이다.

3. Redux 사용하기

Redux를 사용함에 있어서 필수적으로 알아야할 개념들을 알아보자.
이해를 돕고자 앞서 설명한 식당 리모델링을 예제 삼아 직접 설치 & 적용해보며 설명해보겠다.
중간에 error가 발생하더라도 끝까지 하다보면 에러는 없어질 것이다.


3.1. Redux 설치

npm install --save redux react-redux
기본적으로 자바스크립트로 작성된 라이브러리에서 모두 사용 가능한 redux와
리액트와 리덕스의 공식 바인딩 패키지인 react-redux를 함께 설치해준다.

3.2. Action

state의 변화에 대한 내용이다.
식당 예제에서 손님이 모니터를 통해 원격으로 주문내용이 action이라고 간단히 생각해볼 수 있다.

action은typepayload의 프로터리를 포함하는 하나의 객체를 반환하는 함수다.
type에는 어떤 행동을 설명하는 string으로 지정해준다.
payload는 state에 추가하거나 수정할 데이터값이 있다면 같이 지정해주고 없다면 type만 지정해주면 된다.
사용예제는 아래와 같다.

...

const increaseCurrentValue = () => ({
  type: 'INCREASE_VALUE'
})

const decreaseCurrentValue =() => ({
  type: 'DECREASE_VALUE'
})

그다음 우리가 앞서 사용한 예제를 다시 보자.


//rightButton.js
import React from 'react';

const RightButton = props => {
  return (
    <button type="button" onClick={props.increasing}>
      더하기
    </button>
  );
};

export default RightButton;
  // props.increasing에는 App.js에 작성한 handleIncrease함수를 지정되어있다.
  // 함수는 아래와 같다
  // handleIncrease =() => {
  //   this.setState({
  //     value: this.state.value +1
  //   })
  // }


//leftButton.js
import React from 'react';

const LeftButton = props => {
  return (
    <button type="button" onClick={props.decreasing}>
      빼기
    </button>
  );
};

export default LeftButton;
  // props.decreasing에는 App.js에 작성한 handleDecrease함수를 지정되어있다.
  // 함수는 아래와 같다
  // handleDecrease =() => {
  //   this.setState({
  //     value: this.state.value -1
  //   })
  // }

이렇게 더하기 버튼의 onClick 이벤트는 App.js에서 props로 넘어온 이벤트핸들러로 지정 되어 있다. 더하기 버튼을 눌렀을 때 props로 넘어온 increasing의 값을 사용하라는 뜻이다.

하지만 redux에서는 하나의 state에서 관리하기 때문에 그 state에 취할 행동과 정보를 (필요하다면) 지정해주는 것이 action이다.

action의 활용 방법은 밑에서 다시 살펴볼 것이다.

3.3. Reducer

Reducer는 하나의 state가 관리되는 곳에서 직접 state를 변경 혹은 추가 시키는 함수이다.
간단하게 말하자면 reducer는 손님이 주문한 음식을 조리해주는 주방의 역할이라고 보면 된다.

Reducer 함수는 이전의 stateaction을 매개변수로 갖는다.
앞서 지정된 action의 타입에 따라 각기 다른 state의 변화를 줄 수 있다.
추가로 actionpayload가 지정되어 있다면 그 payload를 적용할 수 있다.
추가하거나 변경한 state는 반드시 하나의 객체로서 반환되어야 한다.
그 이유는 리덕스의 특징 중 하나 때문인데 바로 reducer는 pure function(순수 함수)으로 이루어져야 한다는 것이다. 순수 함수란 같은 입력 값을 넣으면 같은 출력 값을 반환해 내는 함수를 뜻한다. reducer는 이전의 state와 action을 받아서 하나의 변화한 next state를 반환한다. 즉 새로운 하나의 state를 반환하기 때문에 next state에 현재의 action과 상관없는 현재 state의 값들도 넣어주어야하는데 알아서 이전의 state를 보존해주는 setState와는 조금 다르는 점을 명심하자.

reducer의 사용 예제는 아래와 같다.

// app.js
...
const INITIAL_STATE = {
    value: 0
}
// 사용하고자 하는 초기 state를 적어준다
export const valueReducer = (state=INITIAL_STATE, action) => {
  // 바로 다음에 설명할 store에 넣어줘야 하기 때문에 export해주는것을 잊지 말자.
  // 또한 state=INITIAL_VALUE 대신에 null로 사용 가능하지만
  // 원하는 값이 있다면 지정 후 INITIAL_STATE를 값으로 넣어주자
  switch (action.type) {
    // switch는 if 와 같다고 보면 된다.  
    // action의 property인 action의 값이 존재한다면 다음으로 단계로 함수를 실행한다는 뜻이다.
    case 'INCREASE_VALUE':
      // case도 if와 같다. 
      // 만약 action에 작성된 action함수의 type이 case뒤에 오는 string값이라면 
      // 밑에있는 return안의 객체를 반환하겠다고 하는것이다
      // payload가 있다면 반환하는 객체에 적용할 수 있다.
      return {
        ...state,
        // Reducer는 전체 state를 하나로 반환해야하기 때문에
        // ...state로 현재 이 액션과 상관없는 state값들을 보존시켜줘야한다.
        value: state.value + 1,
        // 실제로 변경시킬 state값을 변경시켜주었다.
      };
    case 'DECREASE_VALUE':
      return {
        ...state,
        value: state.value - 1,
      };
    default:
      return state;
      // 위의 action.type검사에 하나도 해당하지 않는다면 현재의 state를 그대로 반환한다는 뜻이다.
  }
};

앞서 작정한 action을 trigger하면 reducer 함수가 작동된다. 그리고 그 함수는
actiontype을 검사하여 작성된 코드에 맞게 새로운 state를 반환한다.
그렇기 때문에 하나의 reducer만들로도 여러개의 action을 핸들할 수 있다.
또한 가독성이 좋아지기 때문에 하나로 관리하는 것을 추천한다.

3.4. Store

간단하게 store는 식당의 모든 주문 정보를 알고 있는 AI로봇이라고 생각하면 된다.

store는 application에 오직 하나만 있는데 이 유일한 store를 사용해 application 전체의 상태(state)를 관리한다. 또한 바로 다음에 설명할 Provier에 꼭 필요한 녀석이다. (Provider라는 컴포넌트이 props로 넘겨줘야한다.)
그렇기 때문에 모든 application을 통괄하는 최상단 root에 설정해주자.

사용 예제는 아래와 같다.

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { createStore } from 'redux';
// redux로 부터 createStore를 import해준다
import { valueReducer } from './App';
// reducer를 작성한 위치에서 해당 reducer를 import 해준다
import App from './App';
import * as serviceWorker from './serviceWorker';

const store = createStore(valueReducer);
// createStore함수를 사용해서 원하는 reducer를 인수로 넣어준다. 

ReactDOM.render(<App />, document.getElementById('root'));

3.5. Provider

식당을 예제로 들면 AI로봇에 전원을 공급해주는 것이라고 보면 된다.

모든 컴포넌트의 부모 격으로 앞에서 나온 Store에 관련한 모든 것들에 대한 접근 권한을 허용해 준다고 보면 된다. application의 최상단 컴포넌트를 감싸서 사용해준다.

이 글 초반에 예제로 사용한 코드를 예제로 사용해보면

//`index.js`

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { Provider } from 'react-redux';
// react-redux 라이브러리로부터 Provider 컴포넌트를 import 해준다.
import { createStore } from 'redux';
import App, { valueReducer } from './App';

import * as serviceWorker from './serviceWorker';

const store = createStore(valueReducer);

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

이런식으로 모든 application을 감싸주어야 한다.

3.6. Redux와 컴포넌트간의 연동

이제 사전 작업은 완료되었다고 보면 된다.
그전에 식당 리모델링 설명을 앞서 배운 개념들을 적용해서 다시 잠깐 보도록 하겠다.

사장은 바로 AI 자동화 시스템(Redux) 이라는 새로운 시스템(library)를 도입하였다.
테이블 마다 원격주문을 할 수 있는 원격 주문 시스템을 각각 설치하고 이 주문기계는 wifi를 통해서 AI로봇과 연동되어 있다. 손님이 테이블에서 원격주문(action)을 하게 되면 모든 정보가 AI로봇(store)에게 입력된다. 주방에서 음식조리(reducer)가 완료되면 하나의 로봇이 테이블로 서빙해주는데 이 로봇의 성능은 매우 뛰어나서 일일히 각 테이블에 가서 맞는지 확인할 필요도 없이 해당 테이블로 서빙을 해준다. (전원 공급<Provider>만 연결 시켜주면 된다.)

여기서 중요하게 봐야할 것은 바로 wifi이다. 빨간 점선이 바로 wifi 연결 선인데 만약 wifi가 연결되어 있지 않다면 AI로봇과 주방은 원격 테이블의 내용을 알 일이 없다.
그래서 store에 적용된 reducer를 필요한 컴포넌트에서 사용할 수 있도록 연동을 해보자.
이 연결이 바로 컴포넌트와 우리가 만든 action, reducer 간의 연동이다.

3.6.1. Connect HOC

연동을 위해서는 connect(=wifi)라는 HOC를 사용해야 한다. HOC에 대해 잘 모르면 지금은 넘어가도 괜찮다. 그냥 단순한 사용법만 알아도 리덕스를 사용함에 있어 큰 문제는 없다.
간단하게 하나의 컴포넌트를 HOC로 감싸서 redux와 관련된 접근을 허용시켜준다고 생각하면 된다.

connect(mapStateToProps, mapDispatchToProps)(ExampleComponent)
또한 위 코드처럼 connect함수는 mapStateToProps함수와 mapDispatchToProps함수를 순서대로 매개변수를 갖고 컴포넌트를 인수로 받는 HOC이다.
mapStateToPropsmapDispatchToProps 함수는 각각의 목적에 따라 객체를 반환한다. 그렇게 반환된 객체속 key값은 connect HOC로 감싸진 컴포넌트내에서 props로 사용할 수 있게되는 일련의 연동 작업이라고 생각하면 된다. 자세히 살펴보자.

3.6.2. mapDisPatchToProps

먼저 state를 수정하는 역할을 하는 컴포넌트에 action을 발행(dispatch)하는 작업(연동)을 mapDisPatchToProps함수와 HOC를 이용해서 해보자.

mapDisPatchToProps함수는 컴포넌트에서 액션을 발행(dispatch)을 한 뒤 컴포넌트 내에서 액션을 컴포넌트에서 props중 하나로 받아서 사용한다.
예제는 아래와 같다.

const mapDispatchToProps =dispatch => {
  variableName: (argumentName)=>dispatch(actionName(argumentName))
  // arguement는 액션을 사용할 때 같이 넘기고 싶은 인수가 있다면 넣어주고
  // 없다면 빈칸으로 남겨주면 된다.
}

먼저 RightButton 컴포넌트에 아래처럼 connect함수를 import할 것이다.
그 다음 RightButton컴포넌트 전체를 connect함수의 매개변수로 넘겨줄 것이다.
그리고 action을 컴포넌트 내에서 dispatch하여 실제로 trigger할 수 있게
mapDispatchToProps함수를 작성하여 connect HOC의 매개변수로 넘겨줄 것이다.
다 적용한다면 아래와 같다.
(위에 action에서 작성한 action은 각 행동이 행해지는 컴포넌트로 코드를 옮겨주자)

// rightButton.js
//1️⃣2️⃣3️⃣4️⃣5️⃣ 순서대로 코드 작성

import React from 'react';
import { connect } from 'react-redux';
// 1️⃣connect HOC import
const increaseCurrentValue = () => ({
  type: 'INCREASE_VALUE',
});

const RightButton = (4️⃣{ plueOne }) => {
    // mapDispatchToProps함수에서 key값인 pluesOne을 props로 받아오면서 비구조화할당을 해주었다.
    // 그리고 아래에 onCick 이벤트 핸들러에 값으로 할당 해주었다.
  console.log('더하기 Button 컴포넌트 렌더');
  return (
    <button type="button" onClick={5️⃣plueOne}>
      더하기
    </button>
  );
};

2️⃣ const mapDispatchToProps = dispatch => ({
  // mapDispatchToProps 함수는 dispatch를 매개변수로 받는다
  plueOne: () => dispatch(increaseCurrentValue()),
  // plusOne은 실제로 우리가 현재 컴포넌트 내에서 사용하고자 하는 변수로서의 이름 즉 props로 넘어가는 이름
  // plueOne이라는 변수에 위와 같이 dispatch함수의 매개변수로 사용하고자 하는 액션 함수를 넣어준다.
});

export default 3️⃣connect(null, mapDispatchToProps)(RightButton);

connect HOC의 첫번째 인수는 앞서 말한거와 같이 mapStateToProps함수이다.
mapDispatchToProps는 선택적(optional)인것에 반해 mapStateToProps함수는 필수적이다.
그렇기 때문에 state의 값을 받아오는 mapStateToProps함수가 필요없는 컴포넌트라면 위 예제처럼 null 값을 넘겨줘야한다.

위의 예제를 하나하나 살펴보자.
1️⃣2️⃣3️⃣4️⃣5️⃣순서대로 코드를 작성하였는데
1️⃣. 먼저 connect HOC를 import 해준다.
2️⃣. 그 다음 connect HOC의 두번째 인수인 mapDispatchToProps 함수를 작성해준다.
이 함수는 dispatch라는 매개변수를 받아서 하나의 객체를 반환한다. 반환되는 객체속 key값들은 실제로 connect가 감싸고 있는 컴포넌트 내에서 props 중 하나로 넘어가게 된다.
그리고 key의 value는 dispatch함수의 인수로 action을 넣어서 할당해주면 된다.
3️⃣. connect함수로 RightButton 컴포넌트를 감싸준다. 그리고 그 컴포넌트의 상황에 맞게 connect의 인수로 필요한 함수들을 넣어준다.
4️⃣. RightButton 컴포넌트가 props를 받아서 사용할 수 있도록 props를 지정해준다. 사용하기 편하게 비구조화할당까지 해주었다.
5️⃣. 실제로 plusOne이라는 action을 props로 받아서 사용하였다.

그리고 똑같이 LeftButton 컴포넌트에 알맞는 action함수를 넣어서 바꿔주자.

3.6.3. mapStateToProps

connect의 첫 번째 매개변수인 mapStateToPropse는 영어그대로, store의 state를 해당 컴포넌트의 props로 전달하겠다는 의미이다.
사용예제는 아래와 같다.

const mapStateToProps = state => ({
  variableName: state.certainValue
});

variableName은 컴포넌트내에서 사용할 props의 이름이고
이 변수의 값은 우리가 원하는 state내의 값이다.
즉 state의 값을 받아와야 하는 컴포넌트에서 사용하면 된다.
우리 예제에서는 result.js에서 사용하면 된다.
아래처럼 바꿔보자.

// result.js
//1️⃣2️⃣3️⃣4️⃣5️⃣ 순서대로 코드 작성
import React from 'react';
1️⃣import { connect } from 'react-redux';
// connect HOC 불러오기

const Result = (4️⃣{ sum }) => {
  console.log('Result 컴포넌트 렌더');

  return <div>{5️⃣sum}</div>;
};

2️⃣ const mapStateToProps = state => ({
  sum: state.value,
  //sum으로 컴포넌트 내에서 사용할 props에 현재 state.value의 값(+와-로 변경되는)을 할당해준다.
});

export default 3️⃣connect(mapStateToProps)(Result);
// connect HOC의 첫번째 매개변수인 mapStateToProps는 필수적이지만
// 두번째 매개변수인 mapDispatchToProps를 사용하지 않는다면 생략해도 된다.

위의 예제를 하나하나 살펴보자.
1️⃣2️⃣3️⃣4️⃣5️⃣순서대로 코드를 작성하였는데
1️⃣. 먼저 connect HOC를 import 해준다.
2️⃣. 그 다음 connect HOC의 첫번 째 인수인 mapStateToProps 함수를 작성해준다.
이 함수는 state라는 매개변수를 받아서 하나의 객체를 반환하는데 컴포넌트에 state를 정의해놓지 않았어도 걱정하지말자. 우리는 이미 store라는 하나의 state 관리소를 만들어놓았다. 반환되는 객체속 key값들은 실제로 connect가 감싸고 있는 컴포넌트 내에서 props 중 하나로 넘어가게 된다.
그리고 key의 value는 state에서 가져오려고 하는 프로퍼티를 할당해준다.
3️⃣. connect함수로 Result 컴포넌트를 감싸준다. 그리고 그 컴포넌트의 상황에 맞게 connect의 인수로 필요한 함수들을 넣어준다.
4️⃣. Result 컴포넌트가 props를 받아서 사용할 수 있도록 props를 지정해준다. 사용하기 편하게 비구조화할당까지 해주었다.
5️⃣. 실제로 state의 value의 값을 sum이라는 props로 받아서 사용하였다.

이렇게 Redux와 application간의 연동이 끝이났다. 이제 app.js에서 불필요한 state와 이벤핸들러 그리고 props들을 다 없애주자

//app.js

import React from 'react';
import './App.css';

import RightButton from './components/rightButton.component';
import LeftButton from './components/leftButton.component';
import Result from './components/result.component';

class App extends React.Component {
  render() {
    console.log('App 컴포넌트 렌더');
    return (
      <div className="App">
        <Result />
        <LeftButton />
        <RightButton />
      </div>
    );
  }
}
// 여담으로 state를 사용하지 않거나 constructor가 필요없는 컴포넌트는
// 함수형으로 바꿔주자

export default App;

const INITIAL_STATE = {
  value: 0,
};
export const valueReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'INCREASE_VALUE':
      return {
        ...state,

        value: state.value + 1,
      };
    case 'DECREASE_VALUE':
      return {
        ...state,
        value: state.value - 1,
      };
    default:
      return state;
  }
};

마지막으로 local화면에 application을 띄워보고 더하기/빼기 버튼을 눌렀을 때
렌더링이 어떻게 일어나는지 콘솔로 확인해보자!

처음에 모든 컴포넌트가 렌더링 되면서 각각 콘솔이 찍혔고
그 다음 더하기 버튼을 누르니 현재 state값을 보여주는 result 컴포넌트의 콘솔만 찍혀있다.
이렇게 Redux를 사용하여 불필요한 렌더링을 막아주었다.
다시 말하지만 예제같은 조그만한 application에서는 오히려 더 귀찮은 일을 더 하는 것처럼 느껴질 수 있지만
어플리케이션의 규모가 커질 수록 redux는 더 편하게 느껴질 것이다!

4. Redux-logger

redux를 사용하기 전에 상태관리를 하며 항상 콘솔로그를 찍어가며 개발을 했었다.
그러다 redux를 사용하고 나니 state tracking이 힘들었다.
우연히 발견한 redux-logger라는 middleware가 있는데
액션이 일어날 때 마다 콘솔에 해당 액션과 변화를 tracking해주는 기능을 제공한다.
middleware란 redux 라이브러리를 더 다양하게 사용하게 해주는데 library의 library정도 생각하면 될 것 같다.

적용하는 방법도 간단하니 한번 정리해보도록 하겠다.
npm i --save redux-logger 커맨드와 함께 프로젝트 root에 설치해준다.

그 다음 store를 설치한 파일로 가서 middleware와 logger를 적용해주자.

//index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { Provider } from 'react-redux';
import { applyMiddleware, createStore } from 'redux';
//middlewarwe를 적용할 수 있게해주는 applyMiddleware를 import
import logger from 'redux-logger'
// redux-logger로 부터 logger를 Import
import App, { valueReducer } from './App';

import * as serviceWorker from './serviceWorker';
const middlewares = [logger];
// middlewares라는 변수에 logger를 할당해 주는데 배열 형태로 해준다.
const store = createStore(valueReducer,applyMiddleware(...middlewares));
// createStore함수의 두번째 매개변수로 applyMiddleware함수를 넣어주고
// 그의 인자로 선언한 middlewares를 넣어준다

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

아래와 같이 나오면 성공!

trigger된 action의 type와 이전 state 그리고 변경된 state의 값등 눌러보면 자세한 내용도 나온다. 또한 action에 payload값이 있다면 어떤 data가 어떻게 바뀌는지 확인이 가능하다.
개발을 편리하게 만들어주고 설치 & 적용도 정말 쉽다.

5. 리덕스 코드 정리하기.

마지막에 적용시킨 리덕스 코드들은 조금 중구난방이라고 볼 수 있다.
다시말하지만, 지금이야 4개의 컴포넌트만 존재하고 관리하려는 state의 값이 하나뿐이니 상관없겠지만
그 수가 많아지면 재사용에 있어서 또는 유지보수에 있어서 큰 어려움이 있을 것이다.
그럼 관리하고자 하는 state를 하나 더 만들어서 이쁘게 정리해보자.
먼저 현재 뿔뿔히 무분별하게 작성된 redux 코드들을 알맞게 파일을 만들고 폴더를 만들어서 정리하고
어플리케이션에 음식 버튼을 추가하면 리스트에 추가되는 기능을 추가해보자.


5.1 기존 코드 정리


이렇게 src 폴더안에 redux폴더를 만들고 그안에 데이터 성격에 맞는 이름의 폴더를 만들고 그 안에types/actions/reducer 파일들을 만들어준다.

count.types.js 파일에는 count action에 사용되는 타입들을 객체로 만들어 놓을 것이다.

//count.types.js
const UserActionTypes = {
  INCREASE_VALUE: 'INCREASE_VALUE',
  DECREASE_VALU: 'DECREASE_VALU',
};
export default UserActionTypes;

그 다음 count.actions.js 파일에는 각각 컴포넌트에 흩어진 action 함수들을 모아서 작성하고
함수 작성시 action type은 count.types.js 파일에서 import 해서 사용할 것이다. 그리고 모든 함수들은 export 해준다.

// count.actions.js

import UserActionTypes from './count.types';

export const increaseCurrentValue = () => ({
  type: UserActionTypes.INCREASE_VALUE,
});

export const decreaseCurrentValue = () => ({
  type: UserActionTypes.DECREASE_VALUE,
});
// 이런식으로 UserActionTypes을 import 하여 끌어서 사용한다. 유지보수면에서 유용할 것이다.

다음으로 count.reducer.js파일로 reducer함수를 옮겨준다

const INITIAL_STATE = {
  value: 0,
};

const valueReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'INCREASE_VALUE':
      return {
        ...state,

        value: state.value + 1,
      };
    case 'DECREASE_VALUE':
      return {
        ...state,
        value: state.value - 1,
      };
    default:
      return state;
  }
};

export default valueReducer;
//store에 import 해줘야 함으로 마찬가지로 export 해준다.

그리고 우리는 또 다른 데이터를 관리하는 기능을 추가할 것이다. 이럴때 reducer를 각각 성격에 맞는 데이터에 하나씩 사용하는게 총합적인 reducer를 사용하는것보다 효율적일 것이다.
또한 그 reducere들을 한곳에 모아서 store에 Import 한다면 코드는 더 깔끔해질것이다.

그럼 모든 reducer를 한곳에 모아놓아보자
redux 폴더안에 root-reducer.js 파일을 만들어서 그곳에서 하나로 합칠 것이다.

import { combineReducers } from 'redux';
// combineReducers는 리덕스에서 제공하는 메서드로, 각 리듀서를 하나로 모아주는 역할을 한다.
import countReducer from './count/count.reducer';
// count와 관련된 reducer를 Import 
const rootReducer = combineReducers({
  count: countReducer,
});


export default rootReducer;

이런식으로 rootReducer를 cobineReducers 함수로 만들면된다.
하나 유의할 점은 우리의 state에 약간의 변화가 생긴다는 것이다.

state = {
  count: {
    value: 0
  }
}

이런식으로 한단계의 depth가 더 생긴다. 그럼 이제 이 root-reducer를 store에 적용시켜주자

redux 폴더안에 store.js 파일을 만들어주고 아래와 같이 작성해준다. (이전것을 옮겨오는거와 똑같다)

//store.js
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import rootReducer from './root-reducer';

const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));

export default store;

마지막으로 <Provider /> 컴포넌트의 store로 넘겨줄 이 store를 export 하고 index.js에서 import 하여 적용해보자.
그리고 옮겨진 redux 관련 코드들은 깔끔하게 지워줘도 된다.

마지막으로 각 컴포넌트에 작성된 action 관련 reducer들도 지우고 올바르게 Import 해주자.

// rrightButton.js
import React from 'react';
import { connect } from 'react-redux';
import { increaseCurrentValue } from '../redux/count/count.actions';

const RightButton = ({ plueOne }) => {
  console.log('더하기 Button 컴포넌트 렌더');
  return (
    <button type="button" onClick={plueOne}>
      더하기
    </button>
  );
};

const mapDispatchToProps = dispatch => ({
  plueOne: () => dispatch(increaseCurrentValue()),
});

export default connect(null, mapDispatchToProps)(RightButton);


// leftButton.js
import React from 'react';
import { connect } from 'react-redux';

import { decreaseCurrentValue } from '../redux/count/count.actions';

const LeftButton = ({ minusOne }) => {
  console.log('빼기 Button 컴포넌트 렌더');
  return (
    <button type="button" onClick={minusOne}>
      빼기
    </button>
  );
};

const mapDispatchToProps = dispatch => ({
  minusOne: () => dispatch(decreaseCurrentValue()),
});

export default connect(null, mapDispatchToProps)(LeftButton);


//result.js
import React from 'react';
import { connect } from 'react-redux';
// connect HOC 불러오기

const Result = ({ sum }) => {
  console.log('Result 컴포넌트 렌더');

  return <div>{sum}</div>;
};

const mapStateToProps = state => ({
  sum: state.count.value,
  // root-reducer를 만든관계로 이전의 value가 카값count의 value값이 되었음으로 이렇게 수정
});

export default connect(mapStateToProps)(Result);

수정하고 local에 띄워진 화면에 클릭해보면 잘 작동하는 것을 알 수 있다.

5.2 새로운 reducer 추가

코드 정리가 끝났으니 어플리케이션에 음식 버튼을 추가하면 리스트에 추가되는 기능을 추가해보자.
음식이름의 버튼들이 있는 컴포넌트와 클릭한 음식의 이름이 보여지는 컴포넌트를 만들어보자.
기능은 버튼을 누르면 해당 버튼의 이름이 state의 값으로 추가가되고 바뀐 state의 값을 받아서 보여지는것이다.
그리고 foodlist 옆에 clear 버튼을 눌러서 보여지는 음식 리스트들 지워볼 것이다.
위에서 정리한대로 해보자
redux는 food라는 폴더를 만들어서 food-redux관련 파일들을 만들고 그안에 코드를 작성할 것이다.
또한 root-reducer에 food reducer를 추가해주는것을 잊지말자.

//food.types.js
const FoodActionTypes = {
  ADD_FOOD: 'ADD_FOOD',
  CLEAR_LIST: 'CLEAR_LIST',
};

export default FoodActionTypes;



//food.actions.js
import FoodActionTypes from './food.types';

export const AddFoodToList = fruit => ({
  type: FoodActionTypes.ADD_FOOD,
  payload: fruit,
});

export const RemoveFromList = () => ({
  type: 'FoodActionTypes.CLEAR_LIST',
});


//food.reducer.js
import FoodActionTypes from './food.types';

const INITIAL_STATE = {
  food: [],
};

const foodReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case FoodActionTypes.ADD_FOOD:
      return {
        ...state,
        food: state.food.concat(action.payload),
      };
    case FoodActionTypes.CLEAR_LIST:
      return {
        ...state,
        food: [],
      };
    default:
      return state;
  }
};

export default foodReducer;


//root-reducer.js
import { combineReducers } from 'redux';

import countReducer from './count/count.reducer';
import foodReducer from './food/food.reducer';

const rootReducer = combineReducers({
  count: countReducer,
  foodList: foodReducer,
});

export default rootReducer;




//food.js
import React from 'react';
import { connect } from 'react-redux';
import { AddFoodToList } from '../redux/food/food.actions';

const Food = ({ handleAddFood }) => {
  return (
    <div>
      <button type="button" onClick={() => handleAddFood('사과')}>
        사과
      </button>
      <button type="button" onClick={() => handleAddFood('바나나')}>
        바나나
      </button>
    </div>
  );
};

const mapDispatchToProps = dispatch => ({
  handleAddFood: fruit => dispatch(AddFoodToList(fruit)),
});
export default connect(null, mapDispatchToProps)(Food);


//foodlist.js
import React from 'react';
import { connect } from 'react-redux';
import { RemoveFromList } from '../redux/food/food.actions';

const FoodList = ({ foodlist, removeAll }) => {
  return (
    <div>
      {foodlist}
      <button type="button" onClick={removeAll} />
    </div>
  );
};

const mapStateToProps = state => ({
  foodlist: state.foodList.food,
});

const mapDispatchToProps = dispatch => ({
  removeAll: () => dispatch(RemoveFromList()),
});

export default connect(mapStateToProps, mapDispatchToProps)(FoodList);

이렇게 정리해서 작성하니 문제가 생겨도 찾아들어가기 쉽고 눈에도 더 잘 들어온다.
Food와 FoodList 컴포넌트를 여기저기 옮겨가며 확인해보아도
불필요한 랜더링은 전혀 일어나지 않는다.
state에 변화가 생기지 않는 이상
부모가 렌더링이 되어도 자식은 랜더링이 되지 않았다.
redux의 가장 큰 장점인 성능저하를 막아주는 큰 장점을 발견할수있었다.

이렇게 코드정리에 미들웨어까지 알아봤는데
다음에는 비동기처리를 도와주는 redux middleware인 thunk와 saga 그리고 효율적인 데이터 보관및 관리를 더해주는 reselect와 persist에 대해 알아보도록 하겠다!

profile
💻 소프트웨어 엔지니어를 꿈꾸는 개발 신생아👶

3개의 댓글

comment-user-thumbnail
2020년 11월 13일

감사합니다 잘 읽었습니다!

답글 달기
comment-user-thumbnail
2021년 1월 16일

좋은 글 감사합니다

답글 달기
comment-user-thumbnail
2021년 3월 12일

비유가 찰떡이네요...

답글 달기