본문 바로가기

FrontEnd/React

[React] 리듀서 활용하기

리듀서 합치기

 redux 패키지에서 제공하는 combineReducers() 함수는 여러 리듀서를 통합하여 새로운 리듀서를 만들어 준다.

 

 아래 코드는 combineReducers() 함수의 선언문이며 ReducersMapObject 타입 객체를 입력 매개변수로 받는다.  그리고 타입 변수 S는 상태를 의미하며 여기서는 AppState가 해당된다.  combineReducers() 함수의 매개변수 reducers는 ReducermapObject타입 객체이다.

export function combineReducers<S> (reducers: ReducersMapObject<S, any>): Reducer<CombinedState<S>>

 

 

 ReducersMapObject 선언문은 상태 객체의 각 키를 처리하는 리듀서의 타입을 매핑하는 역할을 한다.  그러므로 코드를 보면 상태 타입의 키에 설정되는 값은 Reducer<State[key], Action> 타입의 함수여야 한다.

export type ReducersMapObject<State = any, A extends Action = Action> = {
    // 여기서 [Key in keyof State] 중 keyof State는 State 객체의 모든 키를 가져온다.  
    // Key in은 in연산자를 통해 State의 모든 키를 순회한다.
    [Key in keyof State] : Reducer<State[key], A>
}

 

 

앱 상태를 구성하는 멤버 상태 구현하기

리듀서를 합치기 위해 앱 수준 상태 AppState를 clock, counter, remoteUser, cards라는 이름으로 독립적으로 동작하는 멤버 상태로 구성한다.

import * as Clock from './clock'
import * as Counter from './counter'
import * as R from './remoteUser'
import * as Cards from './cards'

export type AppState = {
  clock: Clock.State
  counter: Counter.State
  remoteUser : R.State
  cards: Cards.State
}

 

 

위 코드 AppState에서는 4개의 멤버 상태를 구성했으므로 이를 각각 처리할 4개의 리듀서가 필요하다.  clock, counter, remoteUser, cards 4개의 각 디렉터리 안에 최소한으로 구현된 리듀서를 준비한다.

import * as T from './types'

const initialAppState: T.State = {}
export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
  return state
}

 

 

 combineReducers() 함수로 '상태_이름: 해당_리듀서' 형태의 조합을 모두 결합해 새로운 루트 리듀서를 만든다.  이 함수는 리덕스 관련 코드를 어떤 기계적인 패턴으로 구현할 수 있게 해준다.

 

 combineReducers() 함수의 매개변수 reducers는 ReducersMapObject 타입이고 이 타입 선언문의 [Key in keyof State] : Reducer<State[key], A> 부분을 고려하면 clock, counter 등의 멤버 상태는 모두 AppState 이므로 [Key in keyof State] 조건을 만족한다.

 

 또한 각 키 설정값의 타입은 Reducer<state[key],A> 리듀서 함수여야 하므로 Clock.reducer로 설정하면 된다.

import {combineReducers} from 'redux'
import * as Clock from './clock'
import * as Counter from './counter'
import * as R from './remoteUser'
import * as Cards from './cards'

export const rootReducer = combineReducers({
  clock: Clock.reducer,
  counter: Counter.reducer,
  remoteUser: R.reducer,
  cards: Cards.reducer
})

 

 

 

시계만들기

 먼저 AppState clock 멤버 상태에 대한 타입을 선언한다.  AppState.clock의 타입을 Date로 변경하고 Action을 선언한다.

import type {Action} from 'redux'

export type State = Date

// @clock/setClock, payload는 리덕스 커뮤니티의 관행
export type SetClockAction = Action<'@clock/setClock'> & {
  payload: State
}
export type Actions = SetClockAction

 

 

 다음으로는 SetClockAction 타입의 액션 객체를 생성하는 setClock이란 '액션 생성기(action creator)' 를 추가한다.

import type * as T from './types'

// 리듀서의 action으로 설정되어 있다.
export const setClock = (payload: T.State): T.SetClockAction => ({
  type: '@clock/setClock',
  payload
})

 

 

 다음으로는 clock의 reducer를 추가한다.  앞서 State의 타입이 Date이므로 초기값을 new Date()로 초기화한다.  또한 action.payload의 타입인 State 또한 Date이므로 action의 playload값을 그대로 반환한다.

import * as T from './types'

const initialAppState: T.State = new Date()

export const reducer = (state: T.State = initialAppState, action: T.Actions) => {
  switch (action.type) {
    case '@clock/setClock':
      // action.payload는 Date속성이다.  
      // 리듀서에서는 액션을 처리하는 과정에서 payLaod에 담긴 값의 상태를 변경하는 역할을 한다.
      // action.payload == new Date()
      return action.payload
  }
  return state
}

 

 

  위에 선언한 것들을 비롯해 시계가 동작하는 코드를 추가한다.  

import {UseSelector, useDispatch, useSelector} from 'react-redux'
import {Title} from '../components'
import {useInterval} from '../hook'
import type {AppState} from '../store'
import * as C from '../store/clock'

export default function ClockTest() {
  const clock = useSelector<AppState, C.State>(state => state.clock)
  const dispatch = useDispatch()

  // 1) C.setClock(new Date())을 호출해 액션 객체를 생성한다.
  // 2) 생성된 액션 객체를 dispatch(action)을 호출해 액션 객체를 Redux 스토어로 보낸다.
  // 3-1) 스토어는 디스패치된 액션을 리듀서에 전달 한다.
  // 3-2) 리듀서는 tpye을 확인 후 payload 값을 기반으로 상태를 업데이트한다.
  // 4) 리듀서가 반환한 새로운 값(newState)는 Redux스토어에 저장되며 렌더링된다.
  useInterval(() => dispatch(C.setClock(new Date())))
  return (
    <section className="mt-4">
      <Title>ClockTest</Title>
      <div className="flex flex-col items-center mt-4">
        <p className="text-2xl text-blue-600 text-bold">{clock.toLocaleTimeString()}</p>
        <p className="text-lg text-blue-400 text-bold">{clock.toLocaleDateString()}</p>
      </div>
    </section>
  )
}

 

 

 '@이름/' 접두사와 payload라는 변수 이름을 사용하는 이유 알기

  combinReducers()는 여러 개의 리듀서를 하나로 결합해 주는 함수이다.  이 리듀서에 @clock/setClock, @counter/setCounter 타입의 액션이 유입되면 특정 리듀서만 아니라 combineReducers()가 결합된 모든 리듀서에 액션이 전송된다.

 

  그러기에 액션 타입을 평범하게 setClock, setCounter 등 접두사가 없는 이름으로 만들 시 type이 겹칠 수 있다.  이런 이름충돌을 막기 위해 @이름/접두사를 type 이름 앞에 붙이는 것이다.

 

  payload 이름 또한 AppState를 구성하는 멤버 상태 타입이 수시로 변하기 때문에 동일한 명칭으로 사용함이 용이하다.

 

 

 리듀서는 순수 함수여야 한다.

  리덕스는 리덕스 저장소에 저장된 과거 상태와 리듀서 함수가반환하는 현재 상태를 if(과거_상태 !== 현재_상태) 방식으로 비교한다.  이런 형태의 비교가 가능하려면 리듀서 함수 내부에서 현재 상태는 과거 상태를 깊은 복사해야 하며, 깊은 복사를 위해 리듀서는 반드시 순수 함수여야 한다.

 

 함수형 언어 분야에서 순수 함수(pure function)는 다음 조건을 만족해야 한다.

  • 함수 몸통에서 입력 매개변수 값을 변경하지 않는다. (매개변수는 상수나 읽기전용으로만 사용한다)
  • 함수는 함수 몸통으로 만들어진 결과를 즉시 반환한다.
  • 함수 내부에 전역 변수(global variable)나 정적 변수(static variable)를 사용하지 않는다.
  • 함수가 콜백 함수 형태로 구현되어 있거나, 함수 몸통에 콜백 함수를 사용하는 코드가 없다.
  • 함수 몸통에 Promise처럼 비동기(asynchronous) 방식으로 동작하는 코드가 없다.

 

  아래와 같이 매개변수의 값을 직접 변경 시 불순 함수이고 입력 매개변수 값을 유지했을 경우 순수 함수이다.

const impureReducer = (state, action) => {
    state += action.payload  // 매개변수 state의 값을 변경(불순 함수)
    return state
}
const pureReducer = (state, action) => {
    return state + action.payload // 입력 매개변수 state의 값을 유지(순수 함수)
}

const impureReducer2 = (state, action) => {
    state.name = 'Jack'  // 입력 매개변수 state값을 변경(불순 함수)
    return state
}
const pureReducer2 = (state, action) => {
    // 전개연산자를 통한 state 깊은복사 후 새로운 state 객체를 만들어 name 속성만을 변경(순수 함수)
    return { ...state, name : 'Jack'}  
}

const impureReducer3 = (state, action) => {
    state.push({name: 'Jack', age: 32})  // 입력 매개변수 state의 값을 변경(불순 함수)
    return state
}
const pureReducer3 = (state, action) => {
    // 전개연산자를 통한 깊은 복사로 새로운 state배열을 만들어 새 아이템 추가(순수 함수)
    return [...state, {name: 'Jack', age: 32}]
}

const impureReducer4 = (state, action) => {
    const index = 0  
    state.splice(index, 1)  // 입력 매개변수 state의 값을 변경(불순 함수)
    return state
}
const pureReducer4 = (state, action) => {
    // 배열의 filter() 메서드를 사용해 index값이 0인 아이템을 제거한 새로운 배열을 반환(순수 함수)
    return state.filter((item, index) => index != 0)
}