Redux 사용하기

공부하고 반복해서 복습하고 마지막에는 스스로 정리해보는 시간을 갖는것이 스스로에게 많은 도움이 되는것 같습니다. 그래서 이번에는 **ReactNative에서 Redux를 사용하기**라는 주제로 포스팅을 하려합니다. Flux에서 Redux 그리고 ReactNative까지 정말 자세하고 친절하게 설명해주신 분들이 많습니다. 제가 공부하며 참고했던 좋은 글들과 강의는 React Native 글 모음에 따로 정리를 하였습니다. 이 글에서는 제가 그동안 Redux 공부하며 이해가 잘 되지 않았던 부분들에 중점을 맞춰서 정리해보고 React Native에 Redux를 적용시키는 것으로 마무리하려 합니다. 아직은 Redux에 대해 잘 안다고 말할 수 없지만, 이번 기회로 정리하며 다시 다잡으려 합니다. 혹시나 제가 잘못이하거나 틀린 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

Flux와 Redux

Redux의 시작은 Flux

Redux는 페이스북에서 MVC 패턴의 단점을 보완하고자 만든 아키텍처인 Flux의 구현체중 하나입니다. Flux의 구현체는 Redux 외에도 Reflux, rx-flux 등… 여러 구현체가 있지만 그중 Redux가 가장 널리 사용되고 있습니다.

저는 Redux를 처음 접했을때 Flux와 많이 혼동했습니다. Redux와 Flux의 구성 요소와 역할에 대해 혼란스러웠고 정리가 잘 되지 않았는데 이는 Redux와 Flux의 다른점에 대해 집중하지 않았기 때문입니다. Redux는 Flux와 마찬가지로 애플리케이션의 상태를 예측 가능하게 합니다. 그러나 Redux는 Flux와 다른 특징이 있습니다. 바로 **핫 리로딩(hot reloading)**과 **시간 여행 디버깅(time travel debugging)**입니다. 이 두가지의 특징으로 인해 Redux는 Flux의 구성요소에는 없는 리듀서(reducer)가 생겨나고, 스토어(Store)의 역할이 조금 변하게 됩니다.

리듀서(Reducer)

Flux에서 스토어는 (1) 상태 변환을 위한 로직, (2) 현재 애플리케이션의 상태를 포함하고 있습니다. 스토어가 이 두 가지를 모두 갖고 있기 때문에 핫 리로딩시 문제가 발생합니다. 상태 변환을 위한 로직을 수정하기 위해 스토어 객체를 리로딩하면 스토어에 저장된 기존의 상태와 뷰를 비롯한 나머지 시스템과의 이벤트 구독이 사라지게 되어 문제가되기 때문입니다. 이를 해결하기 위해 Redux에서는 리듀서(reducer)가 새로운 구성요소로 추가됩니다.

리듀서는 스토어가 갖고 있던 상태 변환을 위한 로직을 대신 갖게됩니다. 따라서 스토어는 액션이 발생했을 때 어떤 상태 변화를 만들어야 하는지 알기위해 리듀서에게 요청합니다. 이렇게 기존 스토어에서 상태 변환을 위한 로직이 분리되었기 때문에 이제 핫 리로딩이 가능하게 됩니다.

리듀서는 첫 번째 인수로 기존 상태의 값 두 번째 인수로는 액션을 가집니다. 리듀서를 작성할 때는 주의사항이 있는데, 첫 번째 인수로서 기존 상태를 갖고 있는 state는 수정하지 않고 상태를 수정할 때는 새롭게 생성하여야 합니다. 이 주의사항으로 지킴으로써 각각의 액션이 발생할 때마다 새로운 상태의 객체가 생성되어 결과적으로는 Redux의 특징인 시간 여행 디버깅이 가능하게 됩니다.

스토어(Store)

Redux의 스토어는 Flux의 스토어와는 다소 차이가 있습니다. 먼저 Flux에서는 다수의 스토어를 가질 수 있었고, 각 스토어는 자신의 범위에 있는 애플리케이션의 상태를 변환할 수 있는 로직을 포함하고 있었습니다. 그러나 Redux는 하나의 스토어만을 가집니다. 또한 Redux의 스토어는 상태 트리(state tree) 전체를 유지하는 책임을 가지며, Flux의 디스패쳐(dispatcher)의 역할도 대신합니다.(Flux의 디스패쳐는 모든 스토어를 갖고 있고, 액션 생성자로부터 액션을 넘겨받으면 스토어에 전달합니다.) 따라서 Redux에서는 스토어에서 제공하는 dispatch 함수로 디스패쳐의 동작을 대신합니다.

Redux 구성요소

지금까지 Flux와 Redux의 차이점과 그로 인해 Redux가 가질 수 있게된 특징에 대해 알아보았습니다. 지금부터는 Redux의 구성요소를 살펴보겠습니다.

액션(Action)

액션은 애플리케이션의 상태를 갖고 있는 스토어로 전달하는 데이터 묶음입니다. store.dispatch() 를 통해 스토어에 액션을 전달할 수 있습니다.

액션은 평범한 자바스크립트 객체이며, type 속성을 갖고 있습니다. 이 type 속성은 액션을 전달받은 스토어가 애플리케이션의 상태변환 로직을 갖고 있는 리듀서를 참조할때 사용하게 됩니다.

액션 생성자(Action Creator)

액션 생성자는 액션을 만드는 함수입니다. Flux에서는 함수 내부에서 dispatch 함수를 통해 액션을 전달하지만 Redux의 액션 생성자는 단지 액션을 반환하기만 합니다. 실제 액션을 전달할때는 결과값을 dispatch 함수에 전달하거나 생성된 액션을 자동으로 보내주는 바인드된 액션 생성자를 만듭니다.

리듀서(Reducer)

액션은 무언가 일어나 상태가 변할것이라는 사실을 말할뿐, 그 결과 실제 애플리케이션의 상태가 어떻게 바뀌는지는 리듀서가 담당합니다. 리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 역할을 하는 순수 함수입니다. 따라서 항상 다음 상태를 계산해서 반환하는 역할만 합니다. API 호출이라던가, Date.now()나 Math.random() 과 같은 순수하지 않은 함수를 호출하는 일은 해서는 안됩니다.

Redux는 처음에 리듀서를 undefined 상태로 호출하여 초기 상태를 반환합니다. 리듀서는 서로 독립적으로 수행된다면 분리될 수 있고, 이 분리된 리듀서는 *루트 리듀서라는 하나의 객체로 조합될 수 있습니다. 결과적으로 처음에 undefined 상태로 호출되면 각각의 자식 리듀서들이 초기 상태를 반환하게 되고, 각각의 리듀서는 전체 상태에서 자신의 부분만을 관리합니다. 모든 리듀서의 state 매개변수는 서로 다르고, 자신이 관리하는 상태 부분에 해당합니다.

스토어(Store)

스토어는 “무엇이 일어날지”를 표현하는 액션과 이 액션에 따라 애플리케이션의 상태를 어떻게 수정할지를 나타는 리듀서를 함께 가져오는 객체입니다. 스토어는 다음과 같은 일들을 합니다.

  • 애플리케이션의 상태를 저장
  • getState()를 통해 상태에 접근
  • dispatch(action)을 통해 상태를 수정할 수 있게 함
  • subscribe(listener)를 통해 리스너를 등록

Redux의 스토어는 Flux와는 달리 하나의 스토어만을 가질 수 있기 때문에 데이터를 다루는 로직을 나누고 싶다면 여러개의 리듀서를 조합하여 대신할 수 있습니다.

뷰 레이어 바인딩(The view layer binding)

뷰 레이어 바인딩은 생성된 스토어를 뷰에 연결하기 위해 필요합니다. 뷰 레이어 바인딩은 connect() 을 통해 컴포넌트(뷰)가 애플리케이션의 상태 업데이트를 받을 수 있도록 모든 연결을 만들어줍니다.

루트 컴포넌트(Root component)

모든 React 애플리케이션은 루트 컴포넌트를 가집니다. 루트 컴포넌트는 계층 구조에서 가장 위에 위치하는 컴포넌트이며, 스토어를 생성하고 어떤 리듀서를 사용할지 알려주며 뷰 레이어 바인딩과 뷰를 불러옵니다.

Redux 사용 준비

애플리케이션을 생성하며 Redux의 구성요소들이 서로 연결됩니다.

  1. **combineReducers()**를 통해 다수의 리듀서를 하나로 묶은 후 루트 컴포넌트가 **createStore()**를 이용해 스토어를 생성할때 전달합니다.
  2. 루트 컴포넌트는 공급 컴포넌트와 스토어 사이를 연결함으로써 스토어와 컴포넌트 사이의 커뮤니케이션을 준비합니다. (이후 컴포넌트에서 connect()를 통해 상태 업데이트를 받을 수 있습니다.)

Redux 데이터 흐름

Redux의 아키텍쳐는 엄격한 일방향 데이터 흐름에 따라 전개되며 데이터의 흐름은 4단계에 따라 진행됩니다.

1. 액션 생성 후 스토어에 전달
액션은 무언가 일어나 상태가 변할 것이라는 내용을 담고 있는 객체입니다. 액션 생성자를 통해 액션을 생성한 후 store.dispatch(action)을 통해 스토어에 전달합니다.

2. 스토어가 리듀서를 호출
스토어는 리듀서에 현재의 상태 트리와 전달받은 액션을 두 가지 인수로 전달합니다.

3. 루트 리듀서가 각 리듀서의 출력을 합쳐 하나의 상태 트리 생성
각각의 상태를 다루는 리듀서에 의해 생성된 결과를 하나로 합쳐 루트 리듀서가 하나의 상태 트리를 생성합니다.

4. Redux 스토어가 루트 리듀서에 의해 반환된 상태 트리를 저장
새로운 상태 트리가 앱의 다음 상태입니다. store.subscribe(listener)를 통해 등록된 모든 리스너가 불러내지고 이들은 현재 상태를 얻기 위해 store.getState()를 호출합니다. connect()를 통해 컴포넌트에 스토어가 연결되어 있다면 컴포넌트는 이를 반영하고 자신의 setState()나 forceUpdate() 메소드를 실행해 자동적으로 render() 메소드를 호출합니다.

React Native에서 Redux 사용하기

이제 React Native에서 Redux를 사용해보겠습니다. React Native를 위한 개발환경 설정은 공식홈페이지를 참조하시기 바랍니다. 이 예제 프로젝트에서는 간단하게 Redux를 사용하는 법에 초점을 맞춰 진행해보겠습니다.
간단히 카운팅하는 앱을 만들어보려 합니다. 완성 화면은 다음과 같습니다.
result

프로젝트 생성

먼저 React Native 프로젝트를 생성합니다. (현재 버전은 0.41.2 입니다)

1
react-native init example

다음으로 redux를 사용하기 위해 필요한 모듈들을 설치합니다.

1
npm install redux react-redux --save

이제 Redux의 구성요소를 참고하여 다음과 같이 새로운 폴더 구조를 생성합니다. (앞으로 사용할 폴더와 파일만 명시하였습니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
example
├── __tests__/
├── android/
├── ios/
├── node_modules/
└── src/
├── actions/
├── index.js
├── countAction.js
└── types.js
├── components/
└── Count.js
├── reducers/
├── index.js
└── countReducer.js
└── app.js
├── index.android.js
├── index.ios.js
└── package.json

루트 컴포넌트 생성 & 설정

먼저 app.js 파일을 작성합니다. App 컴포넌트는 루트 컴포넌트로 사용될 것이며, 액션과 리듀서 파일을 작성후 스토어를 추가할것입니다.

다음은 Android와 iOS의 진입파일인 index.android.jsindex.ios.js을 수정하여 App 컴포넌트를 루트 컴포넌트로 등록합니다.

지금까지의 코드를 작성한 후 화면은 다음과 같습니다.
result1

Count 컴포넌트 생성

이제 Count.js 파일을 작성하여 Count 컴포넌트를 생성하겠습니다. Count 컴포넌트는 현재 카운트 되고 있는 숫자를 보여주는 텍스트와 카운트를 증가시키는 버튼으로 구성됩니다.

Count 컴포넌트를 app.js에서 사용합니다.

Count 컴포넌트를 추가한 후 화면입니다.
result2

Count 액션 생성

액션은 애플리케이션의 상태를 갖고 있는 스토어로 전달하는 데이터 묶음이라고 했습니다. 이 카운팅 앱에서는 액션 객체의 type을 통해서 증가인지 감소인지, payload를 통해서는 증가 혹은 감소할 값을 전달할 것입니다.

먼저 actions/types.js를 작성합니다. types.js 에는 액션의 타입으로 사용될 값을 상수로 정의합니다. 타입은 후에 리듀서에서도 사용되기 때문에 미리 상수로 정의하는것이 실수를 줄일수도 있고, 후에 액션을 관리하는데에도 유용합니다.

이제 countAction.js를 작성합니다. type과 payload로 이루어진 액션 객체 액션 생성자를 통해 생성되어 스토어로 전달됩니다.

마지막으로 index.js를 작성합니다. index.js 에서는 여러개의 액션을 하나의 객체로 묶어 컴포넌트 파일에서 쉽게 사용할 수 있도록 해주는 역할을 합니다.

Count 리듀서 생성

액션을 전달받은 스토어가 상태를 변경하기 위해 리듀서에게 어떠한 상태변환을 해야하는지 요청합니다. 리듀서에서는 이 요청을 처리할 수 있도록 코드를 작성해야 합니다.
countReducer.js를 작성합니다. 리듀서는 함수입니다. 첫 번째 인자로 이전의 상태를 전달받고, 두 번째 인자로는 액션을 전달받습니다. 전달 받은 액션의 type을 통해 새로운 상태를 반환하는것이 리듀서의 역할입니다.
애플리케이션 실행 후 Redux는 처음에 리듀서를 undefined 상태로 호출합니다. swtich문에서 default인 상태에서 초기 상태를 설정합니다.

액션과 마찬가지로 index.js를 작성합니다. 여러개의 리듀서를 묶어 컴포넌트 파일에서 쉽게 사용할 수 있도록 해주는 역할입니다.
redux 모듈의 combineReducer는 트리 구조로 분리된 여러개의 상태를 하나의 단일 상태 트리로 조합합니다.

루트 컴포넌트에 스토어 연결

생성한 액션과 리듀서를 애플리케이션에서 사용할 수 있도록 루트 컴포넌트에 설정해주어야 합니다. 처음 작성했던 app.js를 다음과 같이 수정합니다.
redux 모듈에 있는 createStore를 통해 스토어를 생성할 수 있습니다. store 생성시 인자로 리듀서를 필요로 합니다.
생성된 스토어를 React 에서 사용하기 위해 react-redux 모듈에서 Provider를 사용합니다.

Count 컴포넌트 바인딩

뷰 레이어 바인딩은 생성된 스토어를 뷰에 연결하기 위해 필요하다고 설명했습니다. 이제 생성한 액션과 리듀서를 Count 컴포넌트에서 사용할 수 있도록 connect를 통해 연결을 만들어줍니다.
connect 메서드는 Store의 state를 컴포넌트의 props로 전달하고 상태의 변화가 있을 때 자동으로 컴포넌트의 render를 재호출합니다. connect 메서드는 다음의 인자를 가집니다.

  • mapStateToProps : 스토어의 state를 해당 컴포넌트의 props로 전달(mapping)합니다.
  • mapDispatchToProps : 스토어의 dispatch를 props에 전달합니다. dispatch를 통해 액션생성자에서 생성한 액션을 스토어로 전달할 수 있습니다.

간단한 카운팅앱이 완성되었습니다. 버튼을 통해 count의 값을 변경할 수 있습니다.

포스팅을 마치며

MVC 패턴에 익숙해져있던 저에게 Redux는 머리로는 이해할 수 있었지만 가슴으로는? 이해기 쉽지 않았었습니다. 이번 기회에 다시 하번 정리를 하며 제가 처음에 혼란스러워했던 부분들을 최대한 설명해드리려 노력했습니다. 위의 예제가 좋은 예제라고는 말할 수 없지만 그래도 React Native에서 Redux가 어떻게 사용되는지 전체적인 구조를 보는데에는 나쁘지 않다고 생각합니다.
액션과 리듀서를 생성하고 사용하는 부분에 있어서는 여러가지의 방법이 있지만 최대한 Redux를 쉽게 이해할 수 있도록 작성해보았습니다. 좀 더 효율적으로 작성하는 법은 다음에 기회가 된다면 포스팅해보겠습니다.