리액트
#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