본문 바로가기

프로그래밍(TA, AA)/자바스크립트

[React] 리덕스 알아보기

리덕스의 목표는 플럭스로 기반으로 애플리케이션 안에서 변경된 데이터가 어떻게 흘러가는지 명확히 표현해주는 것이다. 리덕스는 플럭스와 비슷하지만 플럭스와 같지는 않다. 리덕스에는 액션, 액션 생성기, 스토어가 있고 상태를 바꿀때 사용하는 액션 객체가 있다. 리덕스는 디스패처를 없애고 애플리케이션 상태를 불변 객체 하나로 표현함으로써 플럭스의 개념을 더 단순화했다. 리덕스에는 플럭스 패턴에는 없는 리듀서 부분이 들어있다. 리듀서는 현재 상태와 액션에 따라 새로운 상태를 반환하는 함수다. 리듀서의 타입을 굳이 쓴다면 '(상태, 액션) => 새 상태'라고 할 수 있다.

 

리덕스를 알아보기 전에, 리덕스의 원형이된 플럭스에 대해 먼저 살펴본다.

 


플럭스

플러스는 데이터 흐름을 한방향으로 유지하기 위해 페이스북에서 설계한 디자인 패턴이다. 플러스가 만들어지기 전까지는 다양항 MVC 디자인 패턴이 웹개발 아키텍처를 주도했다. 플럭스는 MVC의 대안으로 전혀 다른 디자인 패턴이며 함수형 접근 방법을 보완해준다.

 

리액트나 플럭스는 함수형 자바스크립트와 어떤 관계가 있을까? 상태가 없는 함수형 컴포넌트는 순수 함수다. 상태가 없는 함수형 컴포넌트는 컴포넌트의 내용이나 구성방식을 표현하는 프로퍼티를 입력으로 받아서 UI 엘리먼트를 출력한다. 리액트 클래스도 마찬가지로 상태나 프로퍼티를 입력으로 받아서 UI 엘리먼트를 만든다. 여러 리액트 컴포넌트를 한 컴포넌트로 조합할 수 있다. 이런 컴포넌트에 변경 불가능한 데이터를 입력으로 제공하면 UI 엘리먼트가 반환된다.

const Countdown = ({count}) => <h1>{count}</h1>

 

플럭스는 리액트가 작동하는 방식을 보완하는 웹 애플리케이션의 아키텍처를 잡는 방법을 제공한다. 구체적으로 말해 플럭스는 리액트가 UI를 만들기 위해 사용하는 데이터를 공급할 방법을 제공한다.

 

플럭스는 애플리케이션 상태 데이터를 스토어에 저장해서 리액트 밖에서 관리한다. 스토어는 데이터를 저장하고 변경하며 플럭스 내부에서 뷰를 갱신할 수 있는 유일한 존재다. 사용자가 웹페이지와 상호작용해서 버튼을 누르거나 폼을 제출했다고 가정한다. 이런 일이 벌어지면 사용자의 요청을 표현하는 액션이 만들어진다. 액션은 변화를 일으키는 명령과 데이터를 제공한다. 디스패처라는 중앙제어 컴포넌트가 액션을 가져와 분배한다. 디스패처는 액션을 대기열(queue)에 넣고 대기열에 있는 액션을 빼내서 적절한 스토어에 보내기 위해 만든 컴포넌트이다. 스토어는 액션에 있는 명령과 데이터를 사용해 상태를 바꾸고 뷰를 갱신한다. 데이터는 일정한 방향(액션 → 디스패처 → 스토어  뷰)으로만 흐른다.

 

플럭스 컴포넌트 설명
스토어 상태데이터 저장 객체
액션 변화를 일으키는 명령데이터 제공 (ex. 사용자의 요청)
디스패처 중앙 액션 제어 컴포넌트 (액션 분배 역할)

 

플럭스의 액션과 상태 데이터는 변경 불가능하다. 액션은 뷰로부터 디스패치되거나 웹서버 등 다른 곳으로부터 도착할 수 있다. 변경이 일어나려면 액션이 필요하다. 모든 액션은 변경 명령을 제공한다. 액션은 어떤 변경이 필요하고 그 변경에 어떤 데이터를 활용하고 그 액션이 어디에서 비롯되었는지 알려주는 증빙 역할도 필요하다. 이 패턴에는 아무런 부수 효과도 일어나지 않는다. 상태 변경이 일어나는 부분은 스토어뿐이다. 스토어는 데이터를 갱신하고 뷰는 변경을 통지받아 UI에 렌더링한다. 그리고 액션은 그런 변경이 어떻게 왜 이루어져야 하는지 알려준다.

 

애플리케이션의 데이터 흐름을 이런 디자인 패턴으로 제한하려면 애플리케이션을 고치거나 규모확장하기 훨씬 쉬워진다. 디스패치된 모든 액션에 대해 콘솔을 남겨보면 이러한 흐름을 명확히 확인할 수 있다. 액션은 상태가 어떻게 바뀌었는지 알려준다.

 

플러스 디자인 패턴을 사용해 이런 카운트다운 앱을 만드는 방법을 살펴본다. 플럭스 디자인 패턴의 각 요소를 소개하고 각 부분이 카운트다운 앱을 구성하는 단방향 데잍터 흐름 아래에서 어떤 역할을 하는지 살펴본다.

 


상태가 없는 리액트 컴포넌트인 뷰를 먼저 살펴본다. 플럭스는 애플리케이션 상태를 우리 대신 관리해준다. 따라서 생애주기 함수를 사용할 필요가 없다면 클래스 컴포넌트를 만들 필요가 없다. 카운트다운 뷰는 표시할 count를 인자로 받는다. 또한 tick과 reset이라는 함수를 받는다.

const Countdown = ({count, tick, reset}) => {
    if (count) {
      setTimeout(() => tick(), 1000)
    }
    
    return (count) ?
      <h1>{count}</h1> :
      <div onClick={() => reset(10)}>
        <span>축하합니다!!!</span>
        <span>(처음부터 다시 시작하려면 클릭하세요)</span>
      </div>
}

이 뷰가 렌더링되면 count가 0이 될때까지 수를 표시한다. count가 0이 되면 '축하합니다!!!'라는 메시지를 표시한다. count가 0이 아니면 타이머를 설정해 1초 기다린 다음 tick을 호출한다.

 

count가 0이면 뷰는 사용자가 UI의 메인 div를 클릭할 때까지 아무 일도 하지 않는다. 사용자가 div를 클릭하면 reset을 호출한다. reset은 카운트를 10으로 변경하고 처음부터 카운트다운 과정을 다시 시작한다.

 

컴포넌트 안의 상태
플럭스를 사용한다고 해서 뷰 컴포넌트 안에 상태를 유지할 수 없는 것은 아니다. 플럭스를 사용한다는 말은 애플리케이션 상태를 뷰 컴포넌트가 관리하지 않는다는 뜻이다. 예를 들어 플럭스가 타임라인을 구성하는 날짜와 시간을 관리할 수 있다. 하지만 그런 경우에도 애플리케이션의 타임라인을 시각화하기 위해 내부에서 상태를 사용하는 타임라인 컴포넌트를 여전히 활용할 수 있다.

하지만 컴포넌트에서는 상태를 희소하게 사용해야 한다. 이는 내부에서 자체 상태를 관리하는 재사용 가능한 컴포넌트만 활용해 꼭 필요할 때만 상태를 활용하는 것을 권유한다. 그런 컴포넌트들을 제외한 애플리케이션의 다른 부분에서는 자식 컴포넌트 안에 상태가 있다는 사실을 눈치 채지 못해야 한다.

 


액션과 액션 생성기

액션은 스토어가 상태를 변경할때 사용할 명령과 데이터를 제공한다. 액션 생성기(action creator)는 액션을 만들때 필요한 이런저런 사항을 추상화해주는 함수다. 액션 자체는 type이라는 필드만 들어있으면 되는 객체일 뿐이다. 액션의 type은 그 액션의 유형을 알려주는 문자열이며 보통 대문자로만 이루어진다. 액션은 스토어에 필요한 정보를 담을 수 있다.

const countdownActions = dispatcher => 
  ({
      tick(currentCount) {
        dispatcher.handleAction({ type: 'TICK' })
      },
      reset(count) {
        dispatcher.handleAction({
          type: 'RESET',
          count
        })
      }
  })

카운트다운 액션 생성자를 생성할때는 디스패처를 인자로 전달해야 한다. tick이나 reset메서드는 디스패처의 handleAction 메서드에 TICK이나 RESET 타입의 액션을 전달한다. handleAction 메서드는 액션 객체를 '디스패치(할당)'한다. 

 


스토어

스토어는 애플리케이션의 로직과 상태 정보를 담는 객체다. 스토어는 MVC 패턴의 모델과 비슷하지만 데이터를 반드시 한 객체 안에 담아야 한다는 제약은 없다. 물론 여러 다른 데이터 타입을 처리하는 단일 스토어로 애플리케이션을 구축하는 것도 가능하다.

 

스토어의 현재 상태 데이터는 프로퍼티를 통해 얻을 수 있다. 스토어가 상태 데이터를 변경하기 위해 필요한 모든 정보는 액션 안에 들어 있다. 스토어는 타입에 따라 액션을 처리하고 액션을 처리하면서 상태를 적절히 갱신한다. 데이터가 바뀌면 스토어는 이벤트를 발생시켜서 자신을 구독하는 뷰에 데이터가 변경되었다는 사실을 통지한다.

import { EventEmitter } from 'events'

class CountdownStore extends EventEmitter {
  constructor(count=5, dispatcher) {
    super()
    this._count = count
    this.dispatcherIndex = dispatcher.register(
      this.dispatch.bind(this)
    )
  }
}

get count() {
  return this._count
}

dispatch(payload) {
  const { type, count } = payload.action
  switch(type) {
    case "TICK":
      this._count = this._count - 1
      this.emit("TICK", this._count)
      return true;
    case "RESET":
      this._count = count
      this.emit("RESET", this._count)
      return true
  }
}

 

이 스토어는 카운트다운 애플리케이션의 상태인 카운트를 저장한다. 카운트는 읽기 전용 프로퍼티를 통해 접근할 수 있다(단, 스토어 내부에서는 _count를 사용해 카운트를 직접 변경할 수 있다) 액션이 디스패치되면 스토어는 그 액션을 사용해 카운트를 변경한다. TICK 액션은 카운트를 감소시킨다. RESET 액션은 액션 안에 들어있는 count로 카운트를 재설정한다. 상태가 바뀌고 나면 스토어는 자신이 리슨하는 모든 뷰에 이벤트를 발생(emit)시킨다.

 


플럭스 요소 연결하기

const appDispatcher = new CountdownDispatcher()
const actions = countdownActions(addDispatcher)
const store = new CountdownStore(10, appDispatcher)

const render = const => ReactDOM.render(
  <Countdown count={count} {...actions} />
  document.getElementById('react-container')
)

store.on("TICK", () => render(store.count))
store.on("RESET", () => render(store.count))
render(store.count)

일단 appDispatcher를 만든다. 그 후 appDispatcher를 사용해 모든 액션 생성기를 만든다. 마지막으로 appDispatcher와 스토어를 등록하고, 스토어의 초기 카운터 값을 0으로 설정한다.

 

render 메서드를 사용해 뷰를 렌더링한다. 이때 render 메서드는 인자로 받은 카운터를 활용해 Countdown 뷰를 만들고 액션 생성기를 뷰의 프로퍼티로 전달한다.

 

마지막으로 스토어에 리스너를 몇가지 추가하면 전체 순환 구조가 연결된다. 스토어가 TICK / RESET을 발생시키면 새로운 카운터 값이 생기고 즉시 뷰에 전달된다. 카운터가 뷰에 전달된 직후 최초의 뷰가 렌더링되고 화면에 표시된다. 뷰가 매번 새로운 TICK이나 RESET을 발생시킬때마다 그 액션은 방금 설명한 순환 구조를 따라 전달되며, 결국 화면에 렌더링할 데이터가 뷰에 다시 도착하게 된다.

 

반응형 프로그래밍(reactive programming)은 관찰자 패턴(옵저버 패턴)의 여러가지 문제(특히 리스너 등록과 리스너 통지 타이밍으로 생기는 문제)를 해결하기 위해 각 요소(객체) 간의 데이터 의존관계를 함수형으로 기술하고 라이브러리에서 알아서 데이터 리스닝과 디스패치를 해결해주는 프로그래밍 스타일이다. 리액트와 이름이 비슷하지만 둘은 전혀 다른 이야기다. 순수 반응형 프로그래밍에 대한 책으로는 "함수형 반응형 프로그래밍"을 추천한다.

플럭스 패턴을 구현체들에는 스토어, 액션, 디스패치 메커니즘이 들어있으며 뷰 레이어로는 리액트 컴포넌트를 선호한다. 이들은 플럭스 디자인 패턴의 변종이지만 단방향 데이터 흐름이라는 개념이 핵심이라는 점은 동일하다. 이 중 리덕스가 단시일 내에 가장 유명한 플럭스 프레임워크로 자리 잡았다. 이제 리덕스를 사용해 클라이언트 애플리케이션을 위한 함수형 데이터 아키텍처를 구현하는 방법을 살펴보겠다.

 


리덕스

리덕스 문서(한글버전) : https://deminoth.github.io/redux/

리덕스는 플럭스나 플럭스와 비슷한 라이브러리 중에서 가장 앞서나가고 있다. 리덕스는 플럭스와 비슷하지만 플럭스와 같지는 않다. 리덕스에는 액션, 액션 생성기, 스토어가 있고 상태를 바꿀 때 사용하는 액션 객체가 있다. 리덕스는 디스패처를 없애고 애플리케이션 상태를 불변 객체하나로 표현함으로써 플럭스의 개념을 더 단순화했다. 리덕스에는 플럭스 패턴에는 없는 리듀서 부분이 들어있다. 리듀서는 현재 상태와 액션에 따라 새로운 상태를 반환하는 함수다.

 

 

상태

상태를 한 장소에 저장한다는 아이디어는 낯설지 않다. 일반적인 방법 중 하나는 상태를 애플리케이션 루트 컴포넌트에 저장하는 것이다. 순수 리액트나 플럭스 앱에서는 가능하면 상태를 더 적은 객체에 담으라고 권장한다. 리덕스에서는 그것이 규칙이다. (리덕스의 3가지 원칙: https://redux.js.org/introduction/three-principles)

 

상태를 한 곳에 저장해야 한다는 말은 아마 불합리한 요구사항처럼 느껴질 것이다. 특히 저장해야 하는 데이터의 유형이 다양하다면 더더욱 그럴 것이다. 상태가 여러 다른 컴포넌트에 골고루 퍼져있는 아래의 앱을 살펴보자. 이 앱 자체에는 user의 상태가 들어있다. 모든 메시지는 Messages 컴포넌트 안에 들어있다. Messages에 포함된 각 메시지에는 자체 상태가 있고, 모든 포스팅은 Posts라는 컴포넌트 안에 저장된다.

 

컴포넌트마다 각자 상태가 있는 리액트 앱

이와 같은 구조의 앱도 잘 작동할 수 있다. 하지만 크기가 커짐에 따라 애플리케이션 전체 상태를 결정하기 어려워질 수 있으며, 각 컴포넌트가 setState를 호출해서 자신의 상태를 변경하기 때문에 왜 갱신이 이루어졌는지 이유를 알아내기 힘들어진다.

 

어떤 메시지가 확장 가능한가? 어떤 포스팅을 읽었나? 이런 자세한 내용을 알고 싶다면 컴포넌트 트리를 타고 내려가서 각 컴포넌트 안의 상태를 추적해야만한다.

 

리덕스는 모든 상태 데이터를 한 객체에 모으게 함으로써 애플리케이션에서 상태를 보는 관점을 단순하게 유지해준다. 애플리케이션에서 알아야 하는 내용은 한 곳에 있다. 그 부분이 바로 데이터 상태의 유일한 근원인다. 위 그림과 같은 일을 하는 애플리케이션을 리덕스로 만들 수도 있다. 모든 상태 데이터를 한곳에 모으면 된다.

위 예제를 보면 사용자, 메시지, 포스팅의 상태를 한 객체에서 관리하는 모습을 볼 수 있다. 이 객체는 리덕스 스토어다. 이 객체는 현재 편집중인 메시지에 대한 정보나 어떤 메시지가 펼쳐져 있는지, 어떤 포스트를 읽었는지 등의 정보도 가지고 있다. 이런 정보는 구체적인 레코드를 가리키는 ID를 담고 있는 배열에 들어있다. 모든 메시지와 포스팅은 이 상태 객체에 캐시된다. 그러므로 데이터는 이곳에 존재한다.

 

리덕스를 사용하면 리액트에서 상태 관리를 완전히 가져올 수 있다. 이제는 리덕스가 상태를 관리할 것이다. 리덕스 앱을 만들때는 가장 먼저 상태를 어떻게 할지 생각해야 한다. 상태를 한 객체 안에 정의하려 시도하라. 보통은 각 위치에 적당한 가상 데이터가 들어있는 JSON 샘플을 만들어보는게 좋다.

 

다음 샘플은 각 색에 대한 정보와 색을 정렬하는 방법에 대한 정보를 담은 상태 데이터이다.

{
  colors: [
    {
      "id": ...,
      "title": "바닷빛 파랑",
      "color": "#0070ff",
      "rating": 3,
      "timestamp": ...
    },
    {
      "id": ...,
      "title": "토마토",
      "color": "#d10012",
      "rating": 2,
      "timestamp": ...
    }
  ],
  sort: "SORTED_BY_DATE"
}

이제 애플리케이션 상태의 기본 구조를 파악했으니 이 상태를 액션으로 어떻게 갱신하고 변경하는지 살펴본다.

 


액션

애플리케이션의 상태를 변경 불가능한 한 객체 안에 저장해야 한다는 중요한 리덕스 규칙을 소개했다. 변경 불가능이란 말은 상태 객체 내부가 바뀌지 않는다는 뜻이다. 상태를 바꿀 때는 객체 전체를 바꿔치기하는 방식을 사용한다. 그렇게 하기 위해서는 바꿀 대상을 지정해야 하는 명령이 있어야 한다. 액션(action)이 그런 명령을 제공한다. 액션은 애플리케이션 상태 중에서 어떤 부분이 바꿀지 지시하고 그런 변경에 필요한 데이터를 제공한다.

 

리덕스 애플리케이션에서 상태를 갱신하는 유일한 방법은 액션이다. 액션은 어떤 것을 바꿀지 알려준다. 하지만 액션을 변화 이력에 대한 증빙으로 생각할 수도 있다. 사용자가 색을 하나 제거하고, 두 개 추가하고, 두 색의 평점을 매기고, 정렬 방식을 바꾸면 각 벼화 정보의 자취가 남는다. (https://redux.js.org/basics/actions)

 

보통 객체 지향 애플리케이션을 만들때는 먼저 객체를 구별하고 각 객체의 프로퍼티를 정리한 다음 객체들이 서로 협력하는 방식을 생각한다. 이 경우 우리의 사고는 명사 위주다. 하지만 리덕스 애플리케이션을 구축할 때는 사고방식을 동사 위주로 바꿔야 한다. 액션이 상태 데이터에 어떻게 영향을 끼칠까? 일단 애플리케이션에서 사용할 액션을 정리하면 constants.js 파일에 그 액션들을 나열할 수 있다.

const constants = {
  SORT_COLORS: "SORT_COLORS",
  ADD_COLOR: "ADD_COLOR",
  RATE_COLOR: "RATE_COLOR",
  REMOVE_COLOR: "REMOVE_COLOR"
}
export default constants

색관리 앱의 경우 사용자는 색을 추가하거나, 색에 평점을 매기거나, 색을 제거하거나, 색 목록을 정렬할 수 있다. 여기서는 각각의 액션 유형에 해당하는 문자열 값을 정의한다. 액션에는 적어도 type 필드가 꼭 존재해야 한다.

 

액션 유형은 어떤 일을 할지 지정하는 문자열이다. ADD_COLOR는 애플리케이션 상태에 있는 색의 리스트에 새로운 책을 추가하는 명령이다. 문자열을 사용하는 경우 타이핑 실수를 하기 쉽다.

{ type: "ADD_COLOR" }

이런 유형의 오류는 원인을 찾기 힘들따. 이때 앞에서 문자열을 상수로 정의한 모듈인 constants가 도움이 된다.

import C from "./constants"

{ type: C.ADD_COLOR }

위와 같이 하면 똑같이 액션을 지정하지만 문자열이 아니라 자바스크립트 상수를 사용해 지정한다는 점이 다르다. 자바스크립트 변수 이름을 잘못 입력하면 브라우저가 오류를 발생시킨다. 상수를 꼭 사용할 필요는 없지만 문자열 리터럴 대신 항상 상수를 사용하는 습관을 들이는 것은 나쁜 생각이 아니다.

 


액션의 페이로드 데이터

액션은 상태 변화에 필요한 명령을 제공하는 자바스크립트 리터럴이기도 하다. 대부분의 상태 변화에는 데이터가 필요하다. 어떤 레코드를 삭제해야 할까? 새 레코드에 어떤 정보를 넣어야 할까?

 

이런 데이터를 페이로드(payload)라고 부른다. 예를 들어 RATE_COLOR와 같은 액션을 디스패치할 때는 평점을 매길 색의 아이디와 새로 매길 점수를 알아야 한다. 이런 정보를 액션과 같은 자바스크립트 리터럴 안에 포함시킬 수 있다. 아래는 액션 유형인 RATE_COLOR와 특정 색의 평점을 4점으로 바꾸기 위해 필요한 모든 정보가 들어있다.

{
  type: "RATE_COLOR",
  id: "...",
  rating: 4
}

 

새로운 색을 추가하려면 추가할 색에 대한 상세 정보가 필요하다.

{
  type: "ADD_COLOR",
  color: "#FFFFFF",
  title: "밝은 하양",
  rating: 0,
  id: "...",
  timestamp: "..."
}

이 액션은 밝은 하양이라는 이름의 색을 상태에 추가하라고 리덕스에 명령한다. 새 색의 모든 정보는 이 액션에 들어있다. 액션은 리덕스가 상태를 어떻게 변경해야 할지 알려주는 작은 꾸러미이다. 그 꾸러미 안에는 리덕스가 상태를 변경할 때 필요한 모든 데이터가 들어있다.

 


리듀서

전체 상태 트리는 한 객체 안에 들어있다. 아마도 그로 인해 상태를 충분히 모듈화하지 못했다는 불만을 품는 독자도 있을 것이다. 특히 객체 간의 계층 구조를 잘 조직하는 것을 모듈화라고 생각하면 더더욱 그럴 것이다. 하지만 리덕스는 함수로 모듈화를 제공한다. 리덕스에서는 함수를 사용해 상태 트리 일부를 갱신한다. 이런 함수를 리듀서라고 한다.

 

리듀서는 현재 상태와 액션을 인자로 받아 새로운 상태를 만들어 반환하는 함수다. 리듀서는 상태 트리 중 특정 부분을 갱신하기 위해 만든 함수다. 그 부분은 트리의 가지(중간 노드)일수도 있고 잎(말단 노드)일 수도 있다. 이런 리듀서를 합성하면 어떤 액션에 대한 앱 전체 상태 갱신을 담당하는 리듀서를 만들 수 있다.

 

색관리 앱은 모든 상태 데이터를 한 트리에 담는다. 리덕스를 색관리 앱에 사용하기 위해 이 상태 트리의 가지나 잎을 대상으로 하는 몇가지 리듀서를 만들 수 있다.

 

상태트리에는 colors와 sort라는 가지가 있다. sort 가지는 잎이다. 그 안에 다른 자식이 들어있지는 않다. colors 가지에는 여러 색이 들어있다. 각 색 객체는 잎이다. 이 상태 트리의 각 부분을 처리하기 위한 리듀서를 각각 따로 만든다. 각 리듀서는 일반 함수에 지나지 않는다. 아래처럼 함수 틀을 정의해 둘 수 있다.

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 ""
}

color 리듀서는 객체인 state를 받아서 객체를 반환한다. 반면 colors 리듀서는 배열인 state를 받아서 배열을 반환한다. sort 리듀서는 문자열인 state를 받아서 문자열을 반환한다. 한 리듀서는 상태 트리의 한 부분만 담당하기 때문에 각 리듀서가 반환하는 값이나 이전 상태 값의 타입은 그 리듀서가 처리하는 상태 트리 부분의 타입과 같다. 여러 색을 배열에 저장해야 하고, 각 색은 객체며, sort 프로퍼티는 문자열이다.

 

각 리듀서는 자신이 상태 트리에서 맡은 부분을 갱신할 때 필요한 액션만 처리하도록 설계되었다. color 리듀서는 새로운 색이나 변경된 색 객체가 필요한 액션인 ADD_COLOR와 RATE_COLOR만 처리한다. colors 리듀서는 colors 배열을 다루어야하는 액션인 ADD_COLOR, REMOVE_COLOR, RATE_COLOR만 처리한다. 마지막으로 sort 리듀서는 SORT_COLORS 액션만 처리한다.

 

각 리듀서를 합성 또는 조합해서 스토어 전체를 사용하는 리듀서 함수를 만든다. colors 리듀서는 배열 안의 각 색을 처리하기 위한 color 리듀서와 결합된다. sort 리듀서는 colors 리듀서와 함께 조합해서 단일 리듀서 함수를 만든다. 이렇게 만든 최종 리듀서는 전체 상태 트리를 갱신할 수 있고 상태 트리에 전달되는 모든 액션을 처리할 수 있다.

 

color와 colors 리듀서는 ADD_COLOR와 RATE_COLOR를 처리한다. 하지만 두 리듀서는 트리에서 서로 다른 부분을 변경한다. RATE_COLOR를 처리할때 color 리듀서는 개별 색의 평점 값을 바꾸지만 colors 리듀서는 배열에서 평점을 바꿔야 할 색을 찾아낸다. ADD_COLOR를 처리할때 color 리듀서는 입력받은 값을 프로퍼티로 하는 새 색 객체를 반환하지만, colors 리듀서는 배열에 새로운 색 객체를 추가한다. color와 colors는 함께 협력해야 한다. 각 리듀서는 상태 트리에서 각자 맡은 가지나 잎의 각 액션이 의미하는 바를 처리한다.

 

리덕스는 특정 임무를 담당하는 범위가 더 좁은 리듀서를 만들어서 큰 리듀서로 합성하라고 강제하지 않는다. 원한다면 앱에서 발생하는 모든 액션을 처리하는 큰 리듀서 함수를 만들 수도 있다. 하지만 그런 함수를 만들면 함수형 프로그래밍의 장점이나 모듈화의 장점을 살릴 수 없다.

 


Color 리듀서

리듀서를 작성하는 방법은 여러가지다. 하지만 스위치 문을 사용하면 리듀서가 담당하는 여러 액션을 쉽게 처리할 수 있다. color 리듀서는 action.type을 switch 문으로 검사하고 case 문에서 각 액션 유형을 처리한다.

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
  }
}

 

color 리듀서의 동작은 다음과 같다.

 

  • ADD_COLOR: 액션의 페이로드 데이터로 만든 색 객체를 반환한다.
  • RATE_COLOR: 새 평점을 지정한 새 객체를 반환한다. ES7의 객체 스프레드 연산자를 사용하면 현재 상태의 여러 프로퍼티 값을 새 객체에 쉽게 대입할 수 있다.

리듀서는 항상 어떤 값을 반환해야 한다. 리듀서에 잘못된 액션이 전달된 경우에는 default 문에서 현재 상태를 그대로 반환한다. 이렇게 만든 color 리듀서를 사용해 새로운 색을 만들거나 기존 색의 평점을 바꿀 수 있다.

 


Colors 리듀서

color 리듀서는 상태 트리에 있는 colors 가지의 잎 부분을 처리해준다. 반면 colors 리듀서는 colors 가지 전체를 처리한다.

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
  }
}

 

colors 리듀서는 색을 추가하거나, 색의 평점을 변경하거나, 색을 제거하는 액션을 처리한다.

  • ADD_COLOR: 기존 상태 배열의 모든 값 뒤에 새 색 객체를 덧붙인 새로운 배열을 만든다. 이때 빈 상태 객체를 color 리듀서에 넘겨서 새 색을 만든다.
  • RATE_COLOR: 지정한 색에 새 평점이 매겨진 새로운 색 배열을 반환한다. colors 리듀서는 색 배열에 들어있는 모든 색 객체에 map 함수를 사용해 color 리듀서를 적용한다. color 리듀서는 RATE_COLOR의 페이로드에 있는 ID와 액션의 ID가 일치하는 경우에만 평점을 반영하고 일치하지 않는 경우에는 전달받은 상태를 그대로 반환하므로 결과적으로 RATE_COLOR에 지정된 ID와 ID가 일치하는 색만 평점이 바뀐다.
  • REMOVE_COLOR: 제거할 색을 filter를 사용해 걸러낸 나머지 배열을 반환한다. 

 

colors 리듀서는 색의 배열을 처리한다. colors 리듀서는 색의 배열을 처리하면서 개별 색을 처리할때 color 리듀서를 활용한다. 모든 리듀서에서 상태를 불변 객체로 다룰 필요가 있다. state.push({})나 state[index].rating을 사용하고 싶더라도 그런 욕구를 억눌러야 한다.

 

리듀서는 예상 가능해야 한다. 상태 데이터를 관리할 때만 리듀서를 사용한다. 액션을 리듀서에 보내기 전에 타임스탬프와 ID값을 미리 정했다는 사실에 유의해야 한다. 임의의 데이터를 만들어 내거나 API를 호출하거나 기타 비동기 처리를 수행하는 일은 리듀서 밖에서 이루어져야 한다. 리듀서 안에서는 상태를 변경하거나 부수 효과를 발생시키지 말 것을 권장한다.

 


sort 리듀서

sort 리듀서는 상태에서 문자열 변수 하나만을 담당하는 함수다.

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

 

sort 리듀서는 sort 상태 변수를 변경한다. 이 리듀서는 액션에 있는 sortBy 필드의 값에 따라 sort 상태를 바꾼다 (상태를 제공받지 않은 경우에는 디폴트로 SORTED_BY_DATE가 지정된다)

const state = "SORTED_BY_DATE"

const action = {
  type: C.SORT_COLORS,
  sortBy: "SORTED_BY_TITLE"
}

console.log(sort(state, action)) // "SORTED_BY_TITLE"

 

상태 갱신은 리듀서에 의해 이루어진다. 리듀서는 상태를 첫번째 인자로 받고 액션을 두번째 인자로 받는 순수함수다. 리듀서는 부수 효과를 발생시키지 말고 인자를 불변 데이터로 처리해야 한다. 리덕스에서는 리듀서를 통해 모듈화를 달성한다. 여러 리듀서를 합성하거나 조합해서 전체 상태 트리를 갱신할 때 쓸 수 있는 큰 리듀서를 하나 만들어서 사용한다.

 

리듀서를 조합하는 방법을 살펴보았고, colors 리듀서가 색을 관리하기 위해 color 리듀서를 활용하는 모습을 살펴보았다. 이어서 상태 갱신을 처리하기 위해 colors 리듀서를 sort 리듀서와 함께 조합하는 방법을 살펴보겠다.

 

 


스토어

리덕스에서 스토어는 애플리케이션의 상태 데이터를 저장하고 모든 상태 갱신을 처리한다. 플럭스 디자인 패턴에서는 특정 데이터 집합에만 초점을 맞춘 여러 스토어를 허용하지만 리덕스에서는 오직 한 스토어만 허용한다.

 

스토어는 현재 상태와 액션을 한 리듀서에 전달해서 상태 갱신을 처리한다. 여러 리듀서를 조합하고 합성해서 스토어가 사요할 단일 리듀서를 만들 수 있다. colors 리듀서를 사용해 스토어를 만든다면 앱의 상태 객체는 배열(색 객체의 배열)이 된다. 스토어의 getState 메서드는 현재 애플리케이션 상태를 반환한다. 아래 예제를 통해 color 리듀서를 사용해 스토어를 만들고, 스토어를 만들때는 임의의 리듀서를 넘길수 있음을 알 수있다.

import { createStore } from 'redux'
import { color } from './reducers'

const store = createStore(color)

console.log(store.getState()) // {}

 

단일 리듀서 트리를 만들려면 colors와 sort 리듀서를 조합해야 한다. 리덕스에는 여러 리듀서를 한 리듀서로 조합할때 사용하는 combineReducers 함수가 있다. 이런 리듀서를 사용해 상태 트리를 만들 수 있다. 상태 트리의 필드 이름은 전달한 리듀서 이름과 일치한다.

 

초기상태를 지정하지 않고 스토어를 만들면 각 리듀서가 가정한 디폴트 값이 쓰인다. 스토어를 만들때 초기 상태를 지정할 수도 있다.

import { createStore, combineReducers } from 'redux'
import { colors, sort } from './reducers'

const initialState = {
  colors: [],
  sort: "SORTED_BY_TITLE"
}

const store = createStore(
  combineReducers({ colors, sort }),
  initialState
)

console.log(store.getState().colors.length) // 3
console.log(store.getState().sort)          // "SORTED_BY_TITLE"

애플리케이션의 상태를 바꾸는 유일한 방법은 스토어를 통해 액션을 디스패치하는 것뿐이다. 스토어에는 액션을 인자로 받는 dispatch라는 메서드가 있다. 스토어를 통해 액션을 디스패치하면 모든 리듀서에 액션이 전달되고 상태가 갱신된다.

store.dispatch({
  type: "ADD_COLOR",
  id: "...",
  title: "파티 핑크",
  color: "#F142FF",
  timestamp: "..."
})

store.dispatch({
  type: "RATE_COLOR",
  id: "...",
  rating: 5
})

 


액션 생성기

액션 객체는 자바스크립트 리터럴일 뿐이다. 액션 생성기는 이런 리터럴을 만들어서 반환하는 함수이다. 액션 유형별로 액션 생성기를 추가하면 액션을 생성하는 로직을 단순화할 수 있다.

import C from './constants'

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

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

이제 RATE_COLOR나 REMOVE_COLOR를 디스패치할 때마다 액션 생성기를 호출하여 필요한 정보를 함수 인자로 보내면 된다.

store.dispatch(removeColor("id001"))
store.dispatch(rateColor("id002", 5))

 

액션 생성기는 액션을 디스패치하는 작업을 단순화해준다. 단지 생성기를 호출하면서 필요한 정보를 인자로 넘기면 된다. 액션 생성기는 액션 생성과 관련한 세부사항을 감추고 생성 작업을 추상화해준다. 그에 따라 액션 생성 과정이 아주 단순해진다. 다음은 sortBy 액션 생성기에 대한 예제이다.

import C from './constants'

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

 

sortColors 액션 생성기는 "rating", "title" 그리고 디폴트 값으로 나눠서 sortedBy를 만든다. 이 생성기를 사용하면 sortColors 액션을 디스패치할 때 타이핑을 훨씬 덜해도 된다.

store.dispatch( sortColors("title") )

 

액션 생성기에 로직을 넣을 수도 있다. 로직을 넣음으로써 액션 생성시 알 필요가 없는 세부사항을 감출 수 있다. 로직을 액션 생서기 안에 넣으면 이런 상세 정보를 만드는 과정을 감추고 액션 생성과 디스패치 과정을 추상화할 수 있다.

import C from './constants'
import { v4 } from 'uuid'

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

 

addColor 액션 생성기는 유일한 ID를 생성하고 타임스탬프를 제공한다. 이제 새로운 색을 만들기 훨씬 간단해진다. 생성기가 자동으로 uuid를 사용해 ID를 생성하고 타임스탬프를 클라이언트의 현재 시간으로 만들어준다.

store.dispatch( addColor("#F142FF", "Party Pink") )

 

액션 생성기는 액션을 제대로 만들기 위해 필요한 모든 로직을 캡슐화해준다. 액션 생성기는 백엔드 API와의 통신을 집어넣어야 하는 장소이기도 하다. 액션 생성기에서 데이터를 요청하거나 API 호출을 하는 등의 비동기 로직을 수행할 수 있다.