0%

Redux

Introduction

많은 상태를 관리할 필요가 생겨났다.

상태

서버 응답, 캐시 데이터, 지역적으로 생성해서 사용하고 있지만 아직 서버에 저장되지 않은 데이터,

활성화된 라우트, 선택된 탭, 로딩을 보여줄지 여부, 페이지네이션 컨트롤 등 다양한 UI 상태

모델이 다른 모델을 업데이트하고, 그리고 뷰가 모델을 업데이트 할 수 있고, 이 뷰가 다시 다른 모델을 업데이트하고, 이에 따라 또 다른 뷰가 업데이트 된다.

더하여, 프론트엔트 제품 개발에 있어서 새로 갖춰야할 복잡한 요건들이 늘어나고 있다. 낙관적 업데이트(Optimistic update), 서버 렌더링, 라우트가 일어나기 전에 데이터 가져오기 등이 이에 해당한다. 이러한 복잡함은 변화(mutation)비동기(asyncronicity) 와 같이 사람이 연동하여 추론하기 어려운 개념을 섞어서 사용한다 는 데서 비롯된다.

비관적(pessimistic) 업데이트

사용자 입력 -> 수정 요청 -> 성공 시 화면 갱신

  • 사용자에게는 불편하지만, 개발자에게는 쉬움

낙관적(optimistic) 업데이트

사용자 입력 -> 바로 화면 먼저 갱신 -> 수정 요청

  • e.g. slack, trello

Redux

  • 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너

  • 일관적, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동, 테스트하기 쉬운 앱 작성을 도와준다.

1
2
npx create-react-app my-app --template redux
npm install redux

  1. 애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장

    • 범용적인 애플리케이션(universal application, 하나의 코드 베이스로 다양한 환경에서 실행 가능한 코드)을 만들기 쉽다.
    • 서버로부터 가져온 상태 는 연결되거나(serialized) 수화되어(hydrated) 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있다.
  2. 상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는 지를 묘사하는 액션 객체를 전달하는 방법뿐

    • 뷰나 네트워크 콜백 등에서 상태를 직접 바꾸지 못 하도록 보장한다.
  3. 변화는 순수 함수로 작성되어야

    • reducer이전 상태액션 을 받아 다음 상태를 반환하는 순수 함수이다. 이 때, 이전 상태를 변경하는 대신 새로운 상태 객체를 생성해서 반환해야한다.
    • 처음에는 하나의 reducer 만으로 충분하지만, 애플리케이션이 성장해나가면 상태 트리의 특정한 부분들을 조작하는 더 작은 개별적인 리듀서들로 나누는 것도 가능하다. (React 의 경우 하나의 루트 컴포넌트에서 시작해서 여러 작은 컴포넌트의 조합으로 나누는 것과 동일)

Concepts

상태

1
type State = any
  • 상태(상태 트리)는 넓은 의미의 단어이지만, Redux API에서는 보통 저장소에 의해 관리되고 getState()에 의해 반환되는 하나의 상태값 을 지칭한다.

  • (넓은 의미의) 상태는 Redux 애플리케이션의 전체 상태 를 나타내며, 보통 깊게 중첩되어 있는 객체입니다.


액션

1
type Action = Object
  • 액션은 상태를 변화시키려는 객체이다.
  • 어떤 형태의 액션이 행해질지 표시하는 type 필드를 가져야 한다. type은 상수로 정의되고 다른 모듈에서 임포트할 수 있다.

리듀서

1
type Reducer<S, A> = (state: S, action: A) => S
  • 리듀서(리듀싱 함수)는 누적(state)값(누적될) 값 을 받아서 새로운 누적값을 반환 하는 함수이다. 이들은 값들의 컬렉션을 받아서 하나의 값으로 줄이는데 사용된다.

  • Redux에서 누적”값”은 상태 객체이고, 누적”될” 값은 액션이다. 리듀서는 주어진 이전 상태와 액션에서 새로운 상태를 계산한다.

  • 반드시 같은 입력이 있으면 같은 출력을 반환 하는 순수 함수여야 한다.


![image-20210527130716974](/Users/JungHyunLah/Library/Application Support/typora-user-images/image-20210527130716974.png)


디스패치 함수

1
2
3
4
// 기본 디스패치 함수
type BaseDispatch = (a: Action) => Action
// 비동기 디스패치 함수 (보통 디스패치 함수)
type Dispatch = (a: Action | AsyncAction) => any
  • 디스패치 함수는 액션이나 비동기 액션을 받는 함수이다.

    1. 저장소 인스턴스가 미들웨어를 거치지 않고 제공하는 기본 dispatch 함수

      • 반드시 동기적으로 저장소의 리듀서에 액션을 보내야 한다. 그러면 리듀서는 저장소가 반환한 이전 상태와 함께 새 상태를 계산한다.
      • 👾 리듀서를 사용하기 위해서 액션은 평범한 객체여야 한다.
    2. 비동기 dispatch 함수 (보통 dispatch 함수)

      • 미들웨어를 통해 디스패치 함수는 비동기 액션을 처리할 수도 있다. 미들웨어가 기본 디스패치 함수를 감쌈으로써 액션이나 비동기 액션을 다음 미들웨어에 넘기기 전에, 변환하거나, 지연시키거나, 무시하거나, 해석할 수 있다.

액션 생산자

1
type ActionCreator<A, P extends any[] = any[]> = (...args: P) => Action | AsyncAction
  • 액션 생산자는 단지 액션을 만드는 함수이다. 이 때, 액션은 정보의 묶음이고, 액션 생산자는 액션을 만드는 곳이다.

  • 액션 생산자를 호출하면 액션을 만들어낼 뿐 dispatch하지는 않는다. 따라서 저장소를 변경하기 위해서는 dispatch 함수를 호출해야 한다.

    • 액션 생산자를 호출해 그 결과를 저장소 인스턴스로 바로 dispatch하는 함수를 바인드된 액션 생산자라고 부르기도 한다.
    • 비동기 액션은 디스패치 함수로 보내지는 값이지만 아직 reducer에게 받아들여질 준비가 되어 있지 않아, 기본 dispatch() 함수로 전달되기 전에 미들웨어를 통해 액션(이나 일련의 액션들)으로 바뀌어야 한다.
      • 이들은 종종 PromiseThunk 와 같은 비동기 기본형으로, 리듀서에게 직접 전달되지는 않지만, 작업이 완료되면 액션을 보낸다.

미들웨어

1
2
type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch

중간 (단계) 역할자

  • 미들웨어는 dispatch 함수를 결합해서 새 dispatch 함수를 반환하는 고차함수이다.
  • 종종 비동기 액션을 액션으로 전환한다.

저장소

1
2
3
4
5
6
type Store = {
dispatch: Dispatch,
getState: () => State,
subscribe: (listener: () => void) => () => void,
replaceReducer: (reducer: Reducer) => void
}
  • 저장소는 애플리케이션의 상태 트리를 가지고 있는 객체이다. reducer 수준 (상태 관리) 에서 결합이 일어나기 때문에, Redux 앱에는 단 하나의 저장소만 있어야 한다.

저장소 생산자

1
type StoreCreator = (reducer: Reducer, preloadedState: ?State) => Store
  • Redux 저장소를 만드는 함수

저장소 인핸서

1
type StoreEnhancer = (next: StoreCreator) => StoreCreator
  • 저장소 생산자를 결합하여 강화된 새 저장소 생산자를 반환하는 고차함수
  • 미들웨어와 비슷하게 조합가능 한 방식으로 저장소 인터페이스를 바꿀 수 있도록 한다.

Do I really need?

Redux를 사용하기 적절한 때는,

  • 계속해서 바뀌는 상당한 양의 데이터가 있다
  • 상태를 위한 단 하나의 근원 (루트 저장소) 이 필요하다
  • 최상위 컴포넌트가 모든 상태를 가지고 있는 것은 더 이상 적절하지 않다.

Basic Example

  1. 앱의 상태 전부는 하나의 저장소(store)안에 있는 객체 트리에 저장된다.

  2. 상태 트리를 변경하는 유일한 방법은 무엇이 일어날지 서술하는 객체인 액션(action)을 보내는 것 뿐이며,

  3. 액션이 상태 트리를 어떻게 변경할지 명시하기 위해 리듀서(reducer)를 작성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { createStore } from 'redux'

/*
* 이것이 (state, action) => state 형태의 순수 함수인 리듀서입니다.
* 리듀서는 액션이 어떻게 상태를 다음 상태로 변경하는지 서술합니다.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

// 앱의 상태를 보관하는 Redux 저장소를 만듭니다.
// API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)

// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는,
// 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 현재 상태를 localStorage에 영속적으로 저장할 때도 편리합니다.
store.subscribe(() => console.log(store.getState())))

// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1

상태를 바로 변경하는 대신, 액션이라 불리는 평범한 객체를 통해 일어날 변경을 명시한다.

그리고 각각의 액션이 전체 애플리케이션의 상태를 어떻게 변경할지 결정하는 특별한 함수인 리듀서를 작성합니다.

  • 보통의 Redux 앱에는 하나의 루트 리듀서 함수를 가진 단 하나의 저장소가 있다.

Sample

action은 setter 가 없는 모델(클래스)과 같은 객체이다. 다른 코드가 임의로 이를 수정할 수 없기 때문에, 버그 발생률을 낮춘다.

1
2
3
4
5
6
7
8
9
10
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}

state을 바꾸기 위해서는 action을 dispatch 해야 한다. action은 (플레인) 자바스크립트 객체로 전달한다.

1
2
3
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

이렇게 모든 변화를 action으로 표현하는 것은 앱의 세부적인 변화부터 전체적인 변화까지 파악할 수 있도록 한다.

이러한 state과 action을 묶기 위해서, reducer를 사용한다. 다만, reducer은 큰 규모로 다루는 것보다 작은 규모로 나누어서 함수당 하나의 역할을 담당 하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// reducer 1
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}

// reducer 2
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
}
}

최종적으로, 위와 같이 잘게 나눈 reducer를 통합하여 전체적인 reducer 기능을 하는 루트 reducer를 생성한다.

1
2
3
4
5
6
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}