Skip to content

FE Study

Reese Kim edited this page Jun 16, 2020 · 7 revisions
Redux

"Redux is a predictable state container for Javascript apps."

which means

  1. It is for Javascript apps
    • Can be used with React, Angular, Vue or even vanilla Javascript
    • It is a library for Javascript applications
  2. It is a state container
    • Redux stores the state of you application
    • Consider a React app - state of component
    • State of an app is the the state represented by all the individual components of that app (including data and UI logic)
    • Redux will store and manage the application state state
  3. It is predictable
    • In redux, all state transitions are explicit and it is possible to keep track of them
    • The changes to your application's state become predictable

Core Concepts

Action

  • 상태가 어떻게 바뀔 것인지 정의해놓은 자바스크립트 객체
  • dispatch함수에 인자로 전달되며 store에 어떤 상태변화가 생길 것인지 알려주는 역할을 한다.
  • Action은 반드시 type 프로퍼티를 가지고 있어야 한다.
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

Action Creator

  • action을 생성하는 함수

    function addTodo(text) {
      return {
        type: ADD_TODO,
        text
      }
    }
  • 전통적인 Flux architecture에서는 action creator가 호출되면 action을 생성함과 동시에 dispatch 함수를 호출하는 경우가 많았다.

    function addTodoWithDispatch(text) {
      const action = {
        type: ADD_TODO,
        text
      }
      dispatch(action)
    }
  • Redux에서는 위와 동일한 작업을 아래와 같이 구현한다.

    • Type 1

      dispatch(addTodo(text))
      dispatch(completeTodo(index))
    • Type 2 - Bound action creator 생성

      const boundAddTodo = text => dispatch(addTodo(text))
      const boundCompleteTodo = index => dispatch(completeTodo(index))
      
      boundAddTodo(text)
      boundCompleteTodo(index)
  • dispatch함수는 Redux의 store.dispatch()나 react-redux의 connect()로 호출할 수 있다.

  • Action creator를 사용해서 비동기 제어나 side-effect를 동반하는 작업을 수행할 수 있다. (참고 - Advanced Tutorial)


Reducer

(previousState, action) => nextState
  • (previous) state와 action을 인자로 받아서 새로운 상태를 반환하는 순수함수
  • store에 전달된 action에 의해 상태가 어떻게 바뀔지를 정한다.
  • Reducer 내부에서 절대 해서는 안되는 작업은 다음과 같이 3가지가 있다.
    • state를 직접적으로 수정하는 일

      Mutate its arguments

    • API 호출이나 Routing과 같이 side effect가 있는 일

      Perform side effects like API calls and routing transitions

    • Date.now()Math.random() 같이 순수함수가 아닌 함수를 호출하는 일

      Call non-pure functions, e.g. Date.now() or Math.random()


Given the same arguments, it should calculate the next state and return it.
No surprises. No side effects. No API calls. No mutations. Just a calculation.


import { combineReducers, createStore } from 'redux'

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
  • Reducer라는 이름의 유래
    • Array.prototype.reduce(reducer, [initialValue])에서 따온 것.
    • reduce()가 배열의 값을 특정 데이터로 변환하여 반환하는 것처럼, Reducer는 이전 상태값을 인자로 받아서 새로운 '최종 ' store 데이터를 반환한다.
  • store.combineReducers : 여러 개의 reducer를 프로퍼티로 가지는 객체를 인자로 받아서 하나의 root reducer를 반환한다.

Three Principles

Single Source of Truth

Global state는 single store 내의 object tree에 저장된다.

State is read-only

state를 바꿀 수 있는 유일한 방법은 action(상태가 어떻게 바뀔건지 명시한 자바스크립트 객체)을 내보내는 것 뿐이다.

Changes are made with pure functions

action에 의해 state tree가 어떻게 바뀔건지 구체화 하기 위해서는 순수함수인 reducer가 있어야 한다.


Data Flow

  1. You call store.dispatch(action) from anywhere in your app, including components and XHR callbacks, or even at scheduled intervals.

    { type: 'LIKE_ARTICLE', articleId: 42 }
     { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
     { type: 'ADD_TODO', text: 'Read the Redux docs.' }
  2. The Redux store calls the reducer function you gave it. The store will pass two arguments to the reducer: the current state tree and the action.

    Reducer is a pure function. It only computes the next state. It should be completely predictable: calling it with the same inputs many times should produce the same outputs. It shouldn't perform any side effects like API calls or router transitions. These should happen before an action is dispatched. (performed by middleware)

    // The current application state (list of todos and chosen filter)
    let previousState = {
      visibleTodoFilter: 'SHOW_ALL',
      todos: [
        {
          text: 'Read the docs.',
          complete: false
        }
      ]
    }
    
    // The action being performed (adding a todo)
    let action = {
      type: 'ADD_TODO',
      text: 'Understand the flow.'
    }
    
    // Your reducer returns the next application state
    let nextState = todoApp(previousState, action)
  3. The root reducer may combine the output of multiple reducers into a single state tree.

    Redux ships with a combineReducers()helper function, useful for "splitting" the root reducer into separate functions that each manage one branch of the state tree.

    function todos(state = [], action) {
      // Somehow calculate it...
      return nextState
    }
    
    function visibleTodoFilter(state = 'SHOW_ALL', action) {
      // Somehow calculate it...
      return nextState
    }
    
    let todoApp = combineReducers({
      todos,
      visibleTodoFilter
    });
    let nextTodos = todos(state.todos, action);
    let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);
    return {
      todos: nextTodos,
      visibleTodoFilter: nextVisibleTodoFilter
    }
  4. The Redux store saves the complete state tree returned by the root reducer.

    Every listener registered with store.subscribe(listener)will now be invoked; listeners may call store.getState()to get the current state.

    Now, the UI can be updated to reflect the new state. If you use bindings like React Redux, this is the point at which component.setState(newState)is called.

React-Redux

React + Redux ?

react-redux

React-Redux offers a couple of functions that will help you connect you react application with redux.

컴포넌트를 Redux와 연동하기

  • react-redux에서 제공하는 connect함수를 사용한다.

  • 기본적인 syntax는 이렇다 -> connect(mapStateToProps, mapDispatchToProps)(Component)

    • mapStateToProps : store 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수 / state를 인자로 받는다.
    • mapDispatchToProps: action creator를 컴포넌트의 porps로 넘겨주기 위해 사용하는 함수 / store.dispatch를 인자로 받는다.
  • connect함수를 호출하고 나면 또 다른 함수를 반환하는데, 이 함수에 컴포넌트를 인자로 넘겨주면 Redux와 연동된 컴포넌트가 생성된다.

    import React from "react";
    import { connect } from "react-redux";
    import { buyCake } from "../redux";
    
    const CakeContainer = props => {
      return (
        <div>
          <h2>Number of cakes : {props.numOfCakes}</h2>
          <button onClick={props.buyCake}>Buy Cake</button>
        </div>
      );
    };
    
    const mapStateToProps = state => {
      return {
        numOfCakes: state.numOfCakes
      };
    };
    
    const mapDispatchToProps = dispatch => {
      return {
        buyCake: () => dispatch(buyCake())
      };
    };
    
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(CakeContainer);

React Redux + Hooks

Redux store와 연동된 컴포넌트를 만들 때 connect함수 대신 React-Redux에서 제공하는 Hooks를 사용할 수도 있다.

useSelector

useSelector Hook을 사용하면 connect함수를 쓰지 않고도 Redux의 상태를 조회할 수 있다.

const result: any = useSelector(selector: Function, equalityFn?: Function)

첫번째 인자인 selector함수는 여러 번에 걸쳐 임의의 시점에서 실행될 수 있으므로 순수해야한다.

import React from "react";
import { useSelector } from "react-redux";
import { buyCake } from "../redux";

const HooksCakeContainer = () => {
  const numOfCakes = useSelector(state => state.numOfCakes);
  return (
    <div>
      <h2>Num of cakes : {numOfCakes}</h2>
      <button>Buy Cake!</button>
    </div>
  );
};

export default HooksCakeContainer;

useDispatch

useDitpatch Hook은 컴포넌트 내부에서 store.dispatch를 사용할 수 있게 해준다. 컴포넌트에서 action을 dispatch해야할 때 사용하면 된다.

const dispatch = useDispatch();
dispatch({ type: "SAMPLE_ACTION"});
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { buyCake } from "../redux";

const HooksCakeContainer = () => {
  const numOfCakes = useSelector(state => state.numOfCakes);
  const dispatch = useDispatch();
  return (
    <div>
      <h2>Num of cakes : {numOfCakes}</h2>
      <button onClick={() => dispatch(buyCake())}>Buy Cake!</button>
    </div>
  );
};

export default HooksCakeContainer;

useStore

useStore Hook을 사용하면 컴포넌트 내부에서 Redux store 객체를 직접 사용할 수 있다.

const store = useStore();
store.dispatch({ type: "SAMPLE_ACTION"});
store.getState();

useStore는 컴포넌트에서 정말 어쩌다가 store에 직접 접근해야 하는 상황에만 사용해야 한다. 이런 경우는 흔치 않다.

Redux Middleware

Middleware

Redux Middleware란

Redux middleware는 action을 dispatch했을 때 reducer에서 이를 처리하기에 앞서 사전에 지정된 작업들을 실행하는 역할을 한다. middleware는 action과 reducer 사이의 중간자라고 볼 수 있다.

action -> middleware -> reducer -> store
  • Redux middleware is the suggested way to extend Redux with custom functionality.

  • Provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

  • Use middleware for logging, crash reporting, performing asynchronous tasks etc.

middleware의 기본 구조

const middleware = stroe => next => action => {
  // middleware 기본 구조
}

export default loggerMiddleware;

middleware는 ''함수를 반환하는 함수''를 반환하는 함수이다. store인자는 Redux store instance를, action은 디스패치된 액션을 가리킨다. next역시 함수인데, store.dispatch와 비슷한 역할을 한다. dispatch와의 큰 차이점이 있는데, next(action)을 호출하면 그 다음 처리해야 할 middleware에게 액션을 넘겨주고, 만약 그 다음 middleware가 없다면 reducer에게 action을 넘겨준다는 것이다.

middleware 내부에서 store.dispatch를 사용하면 첫 번째 middleware부터 다시 처리한다. 만약 middleware에서 next를 사용하지 않으면 action이 reducer에 전달되지 않는다. 즉, action이 무시된다.

        ┌ - store.dispatch
        ↓         │
action ----> middleware 1 --- next ---> middleware 2 --- next ---> reducer ---> store

Actions

Synchronous Actions

​ As soon as an action was dispatched, the state was immediately updated.

Asynchronous Actions

​ Asynchronous API calls to fetch data from an end point and use that data in your application

Async action creators

axios

  • Requests to an API end point

redux-thunk

  • Define async action creators
  • Middleware
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import logger from "redux-logger";
import rootReducer from "./rootReducer";

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(logger, thunk))
);

export default store;

CodeSandBox