본문 바로가기

카테고리 없음

리덕스 개념 정리

Redux는 JavaScript 애플리케이션에서 데이터 상태와 UI 상태를 모두 관리하기 위한 툴이다. 이번 시간에 리덕스를 어떻게 리액트 앱에 적용할 수 있는지 배울 것이다. 예제를 통해 리덕스의 개념들을 하나씩 끄집어낼 것이다. 리덕스의 개념들이 어떤식으로 사용되었는지 관찰하며 리액트 앱에 리덕스 적용하는 법을 배워보자.

 

주의: 현재 작성 중인 포스트입니다. 중간 중간 미완결 상태의 글들이 있습니다.

 

 

세상에서 가장 간단해보이는 리액트-리덕스 Counter 예제를 구했다.

codesandbox.io/s/oqm11m570z?from-embed=&file=/src/Counter.js 에 들어가서 카운터 앱에서 리액트-리덕스가 어떻게 적용되는지 알아보자.

 

src/index.js

import React, { Component } from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import counter from "./reducer";
import App from "./App";
import "./index.css";

const destination = document.querySelector("#container");
const store = createStore(counter);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  destination
);

Provider

리액트와 리덕스를 연결해 주기 위해서 Provider를 사용한다. Provider에 createStore를 통해 만든 store 정보를 넣어준다. 그렇기 때문에 createStore를 먼저 만들어준다.

 

The <Provider /> makes the Redux store available to any nested components that have been wrapped in the connect() function.

Since any React component in a React Redux app can be connected, most applications will render a <Provider> at the top level, with the entire app’s component tree inside of it.

Normally, you can’t use a connected component unless it is nested inside of a <Provider>

CreateStore

스토어를 만드는 함수이다. import { createStore } from "redux" 를 통해 사용한다.

주의사항:

앱에 하나 이상의 store를 생성하지 않도록 한다. 그러고 싶을경우엔, combineReducers를 사용하여 하나의 root 리듀서를 사용하여야한다. 뒤쪽에 combineReducers에 대한 설명이 있다.

 

src/reducer.js

// reducer
function counter(state, action) {
  if (state === undefined) {
    return { count: 0 };
  }

  const count = state.count;

  switch (action.type) {
    case "increase":
      return { count: count + 1 };
    case "decrease":
      return { count: count - 1 };
    default:
      return state;
  }
}

export default counter;

 

Actions

액션이란 무엇인가?

액션이란 어플리케이션이 스토어로 보내는 데이터 묶음이다. 스토어는 받은 데이터 묶음을 열어보고 데이터를 변경할 수 있다.

액션은 type이라는 필드를 가진 plain 자바스크립트 object이다. 액션은 어플리케이션에서 일어난 것을 묘사하는 이벤트라고 생각할 수 있다. 

 

액션들 리스트 예:

  • todo 앱에서 완료된 것 toggle하기
  • todo 지우기
  • todo 카테고리 색깔 선택하기
  • 등등

 


위 코드에서 counter 함수는 reducer이다. 

Reducer

리듀서란 무엇일까? 감을 잡기 쉽지 않다.

한마디로 리듀서는 '어플리케이션의 state를 변화시키는걸 결정하는 함수'이다. state를 변화시키기 위해 이전 state와 action이라는 파라미터를 사용한다.

 

왜 리듀서를 사용하는가? -> 왜 리덕스를 사용하는가와도 연관되는 질문인데,

Consistent하게 행동하도록 만들기 위해서 하나의 Signle store만을 이용해서 어플리케이션의 state변화를 managing 하기 위해서이다.

 

왜 리듀서를 얘기하는데 리덕스를 꺼내는가? 

리덕스는 리듀서에 크게 의지하고 있다. 리듀서 함수의 기본 원리는 새로운 next state를 만들어내기위해 1. previous state와 2. 액션을 받는다. 그리고 이런 리듀서의 원리가 리덕스의 원리에 매우 중요한 개념적 위치를 갖고 있다.

 

* reducers는 현재의 state와 action을 arguments로 가진다. 그리고 새로운 new state를 리턴한다.

(state, action) => newState.

 

 

위의 src/reducer.js 코드를 보면 counter앱은 +, - 버튼을 누름에따라 state를 변화시키기위해서 만든 함수임을 알 수 있다. 리덕스에서는 state를 변화시키기위해 리듀서를 사용하기 때문에 counter를 reducer로 작성한 것이다. 기존의 state를 첫번째 argument로 받고, +, -버튼에따라 어떻게 작동할건지를 결정하는 것이기 때문에, 그것을 의미하는 action값을 두번째 argument로 받는것을 알 수 있다.  

 

 

Reducer의 법칙

reducers는 반드시 특정한 규칙을 따라야한다:

  • state와 action arguments에 기반해 오직 new state 값만을 계산해야한다.
  • 기존에 존재하는 state를 변경시켜서는 안된다. 항상 immutable하게 update해야한다. 존재하고있는 state를 copy하고 카피된 값을 변경시키는 식으로 하여야한다.
  • 리듀서는 asynchronous 비동기 로직같은 사이드이펙트(API를 호출하거나 지역객체, 지역변수가 아닌 것을 수정하는 등)가 생기면 안된다.

위의 src/reducer.js 코드에서 argument로 받은 state를 변형시켜버리면(ex. state+=1) , 기존에 존재하는 state를 변경시키면 안된다는 규칙(immutable)에 어긋나게된다. 그렇기 때문에

const counter = state.count;

return {count: count+1};

로 작성함을 볼 수 있다.

 

combineReducers

const rootReducer = combineReducers({
  theDefaultReducer,
  firstNamedReducer,
  secondNamedReducer
})

const store = createStore(rootReducer)

리덕스 앱은 *오직 하나의 리듀서 함수를 가진다. "root reducer" 함수라고 부른다.

root reducer를 향후 createStore에 넘긴다. 그 오직 하나의 root reducer 함수는 dispatch 되는 모든 actions 들을 핸들링하는 책임을 갖는다. 

import { combineReducers, createStore } from 'redux'을 통해 사용한다.

이런식으로 세가지 store를 사용하고싶을때 combineReducers를통해 rootReducer를 만들고, createStore에 rootReducer를 넣어 store를 생성한다.

 

dispatch

리덕스 store는 dispatch라는 메써드가 있다. 

dispatch를 왜 쓰는가?

state를 업데이트하는 오직 유일한 방법이 store.dispatch()를 부르는 것이다! 그리고 action object을 pram에 넣는다.

 store.dispatch()를 부르면 store는 reducer 함수를 run 할것이다. 그리고 new state 값을 저장한다.

예를들면 다음과 같다.

store.dispatch({ type: 'counter/incremented' })

actions을 dispatch 한다는게 무엇일까?

어플리케이션에서 이벤트를 발생하도록 촉발하는것이라고 생각할 수 있다. 무엇인가 일어났고, store가 무엇이 일어났는지 알길 바란다. Reducers는 이벤트 리스너처럼 행동한다. action이 발생했다는걸 알게 되었을 때, 관심을 갖고 state를 업데이트 시킨다.

 

 

 

The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value:

store.dispatch({ type: 'counter/incremented' })

 

console.log(store.getState())

// {value: 1}

You can think of dispatching actions as "triggering an event" in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.

 

src/App.js

import { connect } from "react-redux";
import Counter from "./Counter";

// Map Redux state to component props
function mapStateToProps(state) {
  return {
    countValue: state.count
  };
}

// Action
const increaseAction = { type: "increase" };
const decreaseAction = { type: "decrease" };

// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
  return {
    increaseCount: function() {
      return dispatch(increaseAction);
    },
    decreaseCount: function() {
      return dispatch(decreaseAction);
    }
  };
}

// HOC
const connectedComponent = connect(mapStateToProps, mapDispatchToProps)(
  Counter
);

export default connectedComponent;

 

connect

import { connect } from "react-redux";

왜 connect를 사용하는가?

리액트 컴포넌트를 리덕스 스토어에 말그대로 connect 하기 위해서 사용한다.

connect()는 연결된 컴포넌트

 

리액트-리덕스에선, 컴포넌트가 절대 스토어에 직접적으로 접근하지 않는다. connect를 거쳐서 접근한다.

 

 

 

mapStateToProps 

function mapStateToProps(state) {
  return {
    countValue: state.count
  };
}

 

The connect() function connects a React component to a Redux store.

It provides its connected component with the pieces of the data it needs from the store, and the functions it can use to dispatch actions to the store.

It does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in.

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

The mapStateToProps and mapDispatchToProps deals with your Redux store’s state and dispatch, respectively. state and dispatch will be supplied to your mapStateToProps or mapDispatchToProps functions as the first argument.

The returns of mapStateToProps and mapDispatchToProps are referred to internally as stateProps and dispatchProps, respectively. They will be supplied to mergeProps, if defined, as the first and the second argument, where the third argument will be ownProps. The combined result, commonly referred to as mergedProps, will then be supplied to your connected component.

 

 


아래는 dev.to/chrisachard/redux-crash-course-with-hooks-a54 중 일부를 번역하였습니다.

1.

Redux는 JavaScript 앱의 상태(데이터)를 저장할 수 있는 중앙 위치 제공한다.
리액트와 함께 가장 많이 사용된다 (react-redux를 통해)
트리의 어떤 구성 요소든 액세스하거나 상태를 변경할 수 있게 한다.

 

 

2.

state는 리덕스의 중심 store에 위치한다.
그 store는 reducer라는 함수에의해 만들어진다.
리듀서는 state과 action을 파라미터로 가지고, 같은 '새로운' state를 return한다.

3.

store는 react-redux의 Provider를 사용하여 주어진다.
앱에 있는 어떤 컴포넌트던지 접근할 수 있게 하기위해, 전체 앱을 감싸는 provider를 사용한다.

7.

store에서 데이터를 변경하려면, reducer를 작성한다.

Reducers는 많은 경우 switch와 case statements를 사용한다. (꼭 그래야만하는건 아니지만)

Reducers는 state과 action을 파라미터로 받고, 새로운 state를 return한다.

8.

리듀서가 '새로운' New state object를 return하는건 매우 중요하다. 

이전의 state를 변경시키지(mutate)하지 않는것 또한 중요하다.

컴포넌트들이 무언가 변화될때, re-render하기 때문이다.

 

 

 


 

리액트는 컴포넌트를 통해 데이터가 흐른다(flow). 좀 더 구체적으로 단방향의 데이터 이동(unidirectional data flow)이라고 한다. 데이터는 부모에서 자식으로 흐른다. 리액트는 부모 자식간의 관계가 아닌 컴포넌트들 사이에 데이터의 이동이 (non-obvious)명확하지가 않다. 

 

부모-자식 관계가 아닌 두 컴포넌트들 간의 통신을 위해, 독자적으로 global 이벤트 시스템을 설정할 수 있다. Flux 패턴은 이것을 배열할 수 있는 방법 중 하나이다. Redux는 여기에서 유용하게 쓰인다. Redux는 모든 애플리케이션 상태를 "스토어"라고 불리는 한 곳에 저장하는 솔루션을 제공한다. 컴포넌트들은 state 변화를 store에 "dispatch"한다. 상태의 변화들을 알아야할 필요성이 있는 컴포넌트들은, store에 "subscribe"할 수 있다.