리액트

#7. Redux

YJH3968 2021. 4. 1. 16:13
728x90
  • Redux는 Flux와 유사하나 약간의 차이가 존재한다. Flux에 있는 action을 받아서 적절한 store에 보내는 dispatcher가 없다. 대신 애플리케이션 상태를 불변 객체 하나로 표현했다.
  • Redux는 reducer를 가지고 있다. reducer는 현재 상태와 action에 따라 새로운 상태를 반환하는 함수다. 즉, '(상태, action) => 새 상태'라고 할 수 있다.

 

1. 상태

  • Redux에는 상태를 한 장소에 저장해야 한다는 규칙이 있다.
  • 상태 데이터를 여러 component가 나눠 가지는 것은 작동을 잘 되지만 크기가 커짐에 따라 애플리케이션 전체 상태를 결정하기 어려울 수 있고, 각 component가 setState를 호출해서 자신의 상태를 변경하기 때문에 왜 갱신이 이루어졌는지 추적하기 어렵다.
  • 이때 Redux를 사용해 리액트에서 상태 관리를 완전히 가져와 한 객체에서 상태를 관리하면 위의 문제를 해결할 수 있다. 
// colors data
{
  colors: [
    {
      "id": 1,
      "title": "blue",
      "color": "#0070ff",
      "rating": 3,
      "timestamp": "Sat Mar 12 2016 16:12:09 GMT-0800 (PST)"
    },
    {
      "id": 2,
      "title": "red",
      "color": "#d10012",
      "rating": 2,
      "timestamp": "Fri Mar 11 2016 12:00:00 GMT-0800 (PST)"
    },
    {
      "id": 3,
      "title": "green",
      "color": "#67bf4f",
      "rating": 1,
      "timestamp": "Thu Mar 10 2016 01:11:12 GMT-0800 (PST)"
    },
    {
      "id": 4,
      "title": "pink",
      "color": "#ff00f7",
      "rating": 5,
      "timestamp": "Wed Mar 9 2016 03:26:00 GMT-0800 (PST)"
    }
  ],
  sort: "SORTED_BY_DATE"
}

 

2. action

  • Redux는 애플리케이션의 상태를 변경 불가능한 하나의 객체 안에 저장해야 한다는 규칙을 가지고 있는데, 변경 불가능이란 말은 상태 객체 내부가 바뀌지 않는다는 뜻이다.
  • 상태를 바꿀 때는 객체 전체를 바꾸는 방식을 사용한다. 이때 action은 애플리케이션 상태 중에서 어떤 부분을 바꿀지 지시하고 그런 변경에 필요한 데이터를 제공한다.
  • 객체 지향 애플리케이션을 만들 때는 객체를 구별하고 각 객체의 프로퍼티를 정리한 다음 객체들이 서로 협력하는 방식을 생각한다. 이때 이는 명사 위주의 사고다.
  • 반면 Redux 애플리케이션을 만들 때는 동사 위주의 사고로 action이 상태 데이터에 어떤 영향을 끼칠지를 주로 고려해야 한다.
// 애플리케이션에서 사용할 action들
const constants = {
  SORT_COLORS: "SORT_COLORS",
  ADD_COLOR: "ADD_COLOR",
  RATE_COLOR: "RATE_COLOR",
  REMOVE_COLOR: "REMOVE_COLOR"
}
export default constants

// action에는 type 필드가 반드시 존재해야 한다.
{ type: "ADD_COLOR" }

// 문자열로 type을 지정하는 대신 constants 모듈을 통해 action type을 입력한다.
import C from "./constants"

{ type: C.ADD_COLOR }
  • action type은 어떤 일을 할지 지정하는 문자열이다. 만약 문자열을 잘못 입력할 경우 오류가 발생하지 않으나 예상과 달리 상태가 바뀌지 않는 경우가 생길 수 있어 버그가 발생하고, 이러한 버그는 원인을 찾기 어렵다.
  • 그래서 위 예시처럼 constants라는 모듈을 만들어서 모듈의 변수를 사용하면 오타가 발생하면 그 변수를 찾지 못하므로 오류를 발생시켜 어디서 문제가 발생했는지 쉽게 알 수 있다.
  • payload란 상태 변화에 필요한 데이터를 의미한다. payload는 action과 같은 javascript literal 안에 포함시킬 수 있다.
// RATE_COLOR action을 javascript literal로 표현
{
  type:"RATE_COLOR",
  id: 1,
  rating: 4
}

// ADD_COLOR action을 javascript literal로 표현
{
  type: "ADD_COLOR",
  color: "#FFFFFF",
  title: "white",
  rating: 0,
  id: 5,
  timestamp: "Sat Mar 12 2016 16:12:09 GMT-0800 (PST)"
}

 

3. Reducer

  • Redux는 함수로 모듈화를 제공하고 함수를 사용해 상태 트리 일부를 갱신한다. 이 함수를 reducer라고 한다.
  • reducer는 현재 상태와 action을 인자로 받아 새로운 상태를 만들어 반환하는 함수로, 상태 트리 중 특정 부분을 갱신하기 위해 만든 함수다. 이 함수를 합성해서 어떤 action에 대한 앱 전체 상태 갱신을 담당하는 reducer를 만들 수 있다.
// 색 관리 앱에 대한 reducer의 기본 틀
import C from "./constants"

export const color = (state={}, action) => {
  return {}
}

export const colors = (state=[], action) => {
  return []
}

export const sort = (state="SORTED_BY_DATE", action) => {
  return ""
}
  • reducer는 상태 트리의 한 부분만 담당하기 때문에 각 reducer가 반환하는 값이나 이전 상태 값의 type은 그 reducer가 처리하는 상태 트리의 type과 같다. 위 예시에서도 color는 객체, colors는 배열, sort는 문자열이었으므로 그에 따라 반환값이나 상태 값의 type도 그에 맞춰 정의한다.
  • 각 reducer는 자신이 상태 트리에서 맡은 부분을 갱신할 때 필요한 action만 처리하도록 설계된다. color reducer는 ADD_COLOR와 RATE_COLOR만 처리하고, colors reducer는 colors 배열을 다루어야 하는 action인 ADD_COLOR, REMOVE_COLOR, RATE_COLOR만 처리한다. sort reducer는 SORT_COLORS action만 처리한다.
  • 각 reducer를 합성 또는 조합해서 store 전체를 사용하는 reducer 함수를 만든다. colors reducer는 배열 안의 각 색을 처리하기 위한 color reducer와 합성되고, sort reducer는 colors reducer와 조합해 단일 reducer 함수를 만든다.
  • color와 colors reducer는 ADD_COLOR와 RATE_COLOR를 처리하지만 트리에서 서로 다른 부분을 변경한다. 예를 들어 RATE_COLOR를 처리할 떄 color reducer는 개별 색의 평점 값을 변경하지만 colors reducer는 배열에서 평점을 바꿔야 할 색을 찾아낸다. ADD_COLOR를 처리할 때 color reducer는 입력받은 값을 프로퍼티로 하는 색 객체를 반환하지만 colors reducer는 배열에 색 객체를 추가한다.
// Reducer
import C from "./constants"

export const color = (state={}, action) => {
  switch (action.type) {
    case C.ADD_COLOR:
      return {
        id: action.id,
        title: action.title,
        color: action.color,
        timestamp: action.timestamp,
        rating: 0
      }
    case C.RATE_COLOR:
      return (state.id !== action.id) ?
        state :
        {
          ...state,
          rating: action.rating
        }
    default: 
      return state
  }
}

export const colors = (state=[], action) => {
  switch (action.type) {
    case C.ADD_COLOR:
      return [
        ...state,
        color({}, action)
      ]
    case C.RATE_COLOR : 
      return state.map(
        c => color(c, action)
      )
    case C.REMOVE_COLOR :
      return state.filter(
        c => c.id !== action.id
      )
    default:
      return state    
  }
}

export const sort = (state="SORTED_BY_DATE", action) => {
  switch (action.type) {
    case C.SORT_COLORS:
      return action.sortBy
    default:
      return state
  }
}

 

4. store

  • Redux에서 store는 애플리케이션의 상태 데이터를 저장하고 모든 상태 갱신을 처리한다. Flux에서는 특정 데이터 집합에만 초점을 맞춘 여러 store를 허용하지만 redux는 오직 한 store만 허용한다.
  • store는 현재 상태와 action을 하나의 reducer에 전달해서 상태 갱신을 처리한다.
// store 생성 및 reducer 조합 예제
import { createStore, combineReducers } from 'redux'
import { colors, sort } from './reducer'

const store1 = createStore(color)

console.log(store.getState()) // {}
const store2 = createStore(
  combineReducers({ colors, sort })
  // ', initialState' 를 추가하면 초기 상태를 지정할 수도 있다. 
  // 이를 지정하지 않는 경우 default 상태가 지정된다.
)

console.log(store.getState())
// {
//  colors: []
//  sort: "SORTED_BY_DATE"
// }
  • 애플리케이션의 상태를 바꾸는 유일한 방법은 store를 통해 action을 dispatch하는 것뿐이다. store에는 action을 인자로 받는 dispatch라는 method가 있어 store를 통해 action을 dispatch하면 모든 reducer에 action이 전달돼 상태가 갱신된다.
// store를 통해 action을 dispatch하는 예제
store2.dispatch({
  type: "ADD_COLOR",
  id: 5,
  title: "pink",
  color: "#F142FF",
  timestamp: "Thu Mar 10 2016 01:11:12 GMT-0800 (PST)"
})
  • store를 구독하면서 핸들러 함수를 동륵하면 action이 dispatch된 경우 통지를 받을 수 있다.
  • store의 subscribe method는 함수를 반환하는데, 나중에 그 함수를 호출하면 listener를 해제할 수 있다.
// store 구독 예제
const unsubscribeLogger = store.subscribe(() =>
  console.log('색 개수:', store.getState().colors.length)
)

// 구독을 해제하고 싶을 때 호출한다.
unsubscribeLogger()
  • store의 subscribe 함수를 사용하면 상태 변경을 listen하고 그 변경을 redux-store 키 아래의 localStorage에 저장한다. store를 만들 때 redux-store 키로 저장된 데이터가 있는지 확인해서 저장된 데이터가 있다면 그 데이터를 초기 상태로 삼을 수 있다.
// localStorage를 통해 브라우저에서 영속적인 상태 정보를 사용하는 예제
const store = createStore(
  combineReducers({ colors, sort }),
  (localStorage['redux-store']) ?
    JSON.parse(localStorage['redux-store']) :
    {}
)

store.subscribe(() => {
  localStorage['redux-store'] = JSON.stringify(store.getState())
})

 

5. action 생성기

  • action 생성기는 javascript literal인 action 객체를 만들어서 반환하는 함수다.
// action 생성기 예제
import C from './constants'

export const removeColor = id =>
  ({
    type: C.REMOVE_COLOR,
    id
  })

export const rateColor = (id, rating) =>
  ({
    type: C.RATE_COLOR,
    id,
    rating
  })

export const sortColors = sortedBy =>
  (sortedBy === "rating") ?
    ({
      type: C.SORT_COLORS,
      sortBy: "SORTED_BY_RATING"
    }) :
    (sortedBy === "title") ?
      ({
        type: C.SORT_COLORS,
        sortBy: "SORTED_BY_TITLE"
      }) :
      ({
        type: C.SORT_COLORS,
        sortBy: "SORTED_BY_DATE"
      })

// action 생성기에 로직을 넣을 수도 있다.
import { v4 } from 'uuid'

export const addColor = (title, color) =>
  ({
    type: C.ADD_COLOR,
    id: v4(),
    title,
    color,
    timestamp: new Date().toString()
  })

// 위 action 생성기를 이용해 action을 간단히 만들 수 있다.
store.dispatch(removeColor(1))
store.dispatch(rateColor(2, 5))
store.dispatch(sortColors("title"))
store.dispatch(addColor("#F142FF", "pink"))
  • action 생성기는 액션을 만들기 위해 필요한 모든 로직을 캡슐화해줘 정말 필요한 정보만 인자로 넘겨주기만 하면 action을 쉽게 만들 수 있다. 
  • action 생성기는 백엔드 API와의 통신을 집어넣어야 하는 장소이기도 하다. action 생성기에서 데이터를 요청하거나 API 호출을 하는 등의 비동기 로직을 수행할 수 있다.

 

6. 미들웨어

  • 미들웨어는 서로 다른 두 계층이나 두 소프트웨어를 붙여주는 풀 같은 역할을 하는 소프트웨어다.
  • Redux에도 미들웨어가 있는데, 이 미들웨어는 store의 dispatch 파이프라인에 작용한다. 미들웨어는 action을 dispatch하는 과정에서 연쇄 호출되는 일련의 함수들로 이루어진다.
  • 미들웨어의 각 요소는 action과 dispatch 함수에 접근할 수 있는 함수로 next를 호출한다. next는 갱신이 일어나게 만든다. next를 호출하기 전에 action을 변경할 수 있고 next를 호출하면 상태가 바뀐다.
  • factory는 store를 만드는 과정을 관리하는 함수다. 여기서 factory는 데이터 로깅과 저장 기능을 담당하는 미들웨어가 추가된 store를 만든다. storeFactory는 store를 만들 때 필요한 모든 기능을 하나로 묶어주는 함수다.
// store가 필요한 경우 storeFactory 함수를 호출해 store를 만든다.
const store = storeFactory(initialData)
  • 이 store를 만들 때 logger와 save라는 두 미들웨어를 만든다.
// storeFactory
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { colors, sort } from './reducers'
import stateData from './initialState'

const logger = store => next => action => {
  let result
  console.groupCollapsed("디스패칭", action.type)
  console.log('이전 상태', store.getState())
  console.log('액션', action)
  result = next(action)
  console.log('다음 상태', store.getState())
  console.groupEnd()
}

const saver = store => next => action => {
  let result = next(action)
  localStorage['redux-store'] = JSON.stringify(store.getState())
  return result
}

const storeFactory = (initialState=stateData) =>
  applyMiddleware(logger, saver)(createStore)(
    combineReducers({colors, sort}),
    (localStorage['redux-store']) ?
      JSON.parse(localStorage['redux-store']) :
      initialState
  )

export default storeFactory
  • logger와 saver는 미들웨어 함수로 고차 함수다. 마지막에 반환되는 함수는 action이 dispatch될 때마다 호출된다.
  • logger에서는 action이 dispatch되기 전에 새 console group을 열고 현재 상태와 현재 action을 logging하고 next 파이프를 호출하면 다음 미들웨어를 거쳐 결국 reducer까지 action이 전달된다. 그러면 상태가 갱신되므로 갱신된 상태를 다시 로그에 남기고 console group을 닫는다.
  • saver가 next에 action을 전달하면 바뀐 상태를 받을 수 있다. 이를 localStorage에 저장하고 next에서 받았던 결과를 반환한다.
  • 이 factory를 호출하면 logging과 저장 기능이 있는 store가 생성된다.

 

출처 : learning react(2018)

728x90