도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

Redux Step by Step : 실제 앱을위한 간단하고 탄탄한 워크 플로(Redux Step by Step: A Simple and Robust Workflow for Real Life Apps)

Redux는 React 앱에서 데이터 흐름(flow)을 관리하기위한 가장 널리 사용되는 Flux 구현 중 하나가 되었다. 그런데, Redux에 관한 몇몇 글은 나무를 보고 숲을 보지 못하고 있다. 아래에 제시된 것은 Redux를 사용하여 실제 앱을 구현하기위한 복잡하지 않은 워크 플로우(work flow)이다. 예제를 통해 실제 앱을 단계별로 구현해보도록 하겠다. 실용적인 방법으로 Redux의 법칙(principle)을 적용하고 프로세스를 자세히 설명한다. 


관용적인(idiomatic) Redux를 위한 독창적인(opinionated) 접근법

리덕스(Redux)는 단순한 라이브러리 이상의 전체 생태계를 구성하는 시스템이 되었다. 그것의 인기의 이유 중 하나는 다른 코딩 스타일과 많은 다른 기능(flavor)을 수용(accommodate)할 수 있기 때문이다. 만약에 비동기 action을 처리하는 기능을 찾고 있다면, thunk를 사용 할 수도 있고, promise, 또는 saga를 사용해도 된다.

리덕스를 사용할 때, 어떤 방법이 "최고"라고 할 수 있는 정답은 없다. 리덕스를 사용하는 정형화 된 방법이 없다는 것이다 . 하지만 너무 많은 선택지가 있다는 것은 때론 부담이 될 수도 있다. 나는 여기서 개인적으로 좋아하는 방법을 보여 줄 것이다. 여기서 우리는 실제로 다뤄지는 시나리오를 처리 할 수 있고, 무엇보다도 간단한 방법에 대해서 다룰 것이다.

앱을 만들어 보자!

우리는 실제 활용되는 예제가 필요하다. Reddit에서 가장 흥미로운 포스트를 보여주는 앱을 만들어 보도록 하자.

첫 번째 화면에서는 관심있는 3 가지 토픽을 사용자에게 묻는다. Reddit의 기본 프론트 페이지 하위 목록에서 토픽 목록을 가져온다.

사용자가 선택한 후, 이 세가지 토픽의 포스트 목록을 필터링 가능한(filterable) 목록 (모든 토픽 또는 세가지 중 하나만 표시)에 표시한다. 사용자가 목록의 포스트를 클릭하면 내용(contents)을 보여준다.

Setup

우리는 리액트를 웹에서 사용하기 때문에 (이후 React Native를 추가 할 수도 있음), 우리는 공식 스타터 키트 인 Create React App으로 시작한다. 우리는 또한 redux, react-reduxredux-thunk를 npm을 통해 설치 하 설치한다. 결과는 다음과 같아야한다.

boilerplate를 없애기 위해 Redux store를 초기화하고 index.js에 thunk 미들웨어를 연결한다.


import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import App from './App';
import './index.css';
import * as reducers from './store/reducers';
const store = createStore(combineReducers(reducers), applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

Redux 앱의 Flux Circle of Life

Redux 튜토리얼에서 누락 된 주요한 것들 중 하나는 Redux의 전체적인 그림과 어디에 Redux가 어울리지는(fits in)이다. Redux는 리액트 앱에서 데이터를 전달하는 패턴 인 Flux 아키텍처의 구현체 중 하나다.

전형적인 Flux에서는 앱 상태가 store 내에서 유지된다. 디스패치(dispatched) 된 action은 이 상태를 변경하게하고, 그 후에 이러한 상태(state) 변경을 리스닝(listen)하고 있는 뷰는 그에 따라 리렌더링이 된다:

Flux는 데이터 흐름을 단일 방향으로 만듦으로써 생애 주기(life)를 단순화했다. 이렇게하면 코드베이스가 커지고 복잡해지면서 생길 수 있는 스파게티 코드가 줄어들게 된다.


Redux를 이해하는데 어려움 중 하나는 reducer, selector 및 thunk와 같이 직관적이지 않은 용어가 많이 있다는 것이다. Redux의 이런 용어들이 Flux 다이어그램에 어디와 들어맞는지 보면 더 쉽게 이해 할 수 있다. 이것들은 사이클(cycle)의 다른 부분을 구현하는 다양한 Redux 구조의 기술적 인 이름일 뿐이다:


미들웨어와 saga와 같은 Redux 생태계의 다른 용어는 빠져 있다. 이는 workflow에서 중요한 역할을하지 않으므로 의도적으로 제거했다.

프로젝트 디렉토리 구조

/src 아래에 이러한 디렉토리 구조에 따라 코드를 구성 할 것이다.

  • /src/components - Redux에 대해 알지 못하는 "멍청한(dumb)" 리액트 컴포넌트
  • /src/containers - Redux store에 연결된 "똑똑한(smart)" 리액트 컴포넌트
  • /src/services - 외부 API (백엔드 서버와 같은)에 대한 추상화
  • /src/store - 모든 Redux 관련 코드는 여기에 포함된다. 여기에는 앱의 모든 비지니스 로직이 포함된다.

store 디렉토리는 도메인별로 구성되며 각각 다음을 포함한다:

  • /src/store/{domain}/reducer.js - 모든 selector를 named export로, reducer를 default export로
  • /src/store/{domain}/actions.js - 모든 도메인 action 핸들러 (thunk와 일반 객체 생성자)

상태(state) 우선 접근법

우리의 앱은 두 개의 화면을 가지고 있으며, 처음부터 시작하여 사용자가 정확히 3 개의 토픽을 선택할 수 있도록 할 것이다. Flux로 바로 시작할 수는 있지만 나는 일반적으로 상태(State)로 시작하는 것이 가장 쉽다는 것을 깨달았다.

그렇다면 토픽 별로 화면에 필요한 앱 상태는 무엇일까?

우선 서버에서 검색 한 토픽 목록을 저장해야 한다. 또한 사용자가 선택한 토픽의 ID 역시 저장해야 한다(최대 3 개). 선택한 순서대로 가지고 있으면 좋을 것이다. 그래서 이미 세개가 있는 상태에서 다른 하나가 선택된다면, 가장 오래된 것을 지우면 된다.

앱 상태를 어떻게 구성할까? 저의 이전 포스트에는 "앱 상태를 구조화 할 때의 의도치 않은 복잡성 방지(Avoiding Accidental Complexity When Structuring Your App State)"라는 유용한 팁 목록이 있다. 이에 따른 적절한 구조는 아래와 같다:


{
"topicsByUrl": {
"/r/Jokes/": {
"title": "Jokes",
"description": "The funniest sub on reddit. Hundreds of jokes posted each day, and some of them aren't even reposts! FarCraft"
},
"/r/pics/": {
"title": "pics",
"description": "I bet you can figure it out by reading the name of the subreddit"
}
},
"selectedTopicUrls": ["/r/Jokes/"]
}

토픽 URL은 고유 ID로 사용된다.

우리는 이 상태를 어디에 둬야 할까? 리덕스에서 reducer는 상태를 유지하고 업데이트 한다. 도메인별로 코드를 구성하므로 이 reducer의 자연스러운 위치는 다음과 같다. /src/store/topics/reducer.js

reducer를 만드는 boilerplate가 있는데, 여기에서 볼 수 있다. (Redux에서 요구하는 것처럼) 상태의 불변성(immutability)을 강제(enforce)하기 위해, 나는 seamless-immutable이라는 불변성 라이브러리를 사용하기로 결정했다.

첫 번째 시나리오

상태를 모델링 한 후, 나는 사용자 시나리오를 작성하고 처음부터 끝까지 구현하는 것을 좋아  한다. 우리의 경우 토픽 화면을 만들고 화면에 곧바로 몇 개의 토픽을 표시하자. 이 컴포넌트는 리덕스에 대해 알고있는 "현명한(smart)" 컴포넌트이고 reducer에 연결된다. 우리는 이것을 /src/containers/TopicsScreen.js에 저장한다.

연결된 컴포넌트를 만드는 데 필요한 boilerplate가 있다. 여기에서 볼 수 있다. 이것을 앱 컴포넌트의 내용으로 표시해 보자. 자, 모든 것이 설정되면, 우리는 몇 가지의 토픽을 가져올 수 있다.

규칙 : 똑똑한(smart) 컴포넌트는 dispath 작업을 제외한 모든 로직을 가질 수 없다.


이 시나리오는 뷰의 componentDidMount에서 시작된다. 뷰에서 로직을 직접 실행할 수 없기 때문에 토픽을 가져올 action을 dispatch한다. 이 action은 물론 비동기식이므로 thunk가 된다:


import _ from 'lodash';
import * as types from './actionTypes';
import redditService from '../../services/reddit';
export function fetchTopics() {
return async(dispatch, getState) => {
try {
const subredditArray = await redditService.getDefaultSubreddits();
const topicsByUrl = _.keyBy(subredditArray, (subreddit) => subreddit.url);
dispatch({ type: types.TOPICS_FETCHED, topicsByUrl });
} catch (error) {
console.error(error);
}
};
}


Reddit의 서버 API를 추상화하기 위해서 실제로 서버 호출(network fetch)을 수행하는 새 서비스를 만든다. 이 메소드는 비동기식이므로 응답(response)을 기다릴 수 있다. 일반적으로 나는 async await API를 선호해서 오랫동안 promise API를 직접 사용하지는 않았다.

이 서비스는 배열을 반환하지만, 우리의 상태 구조는 토픽을 map으로 저장한다. action에서 이러한 데이터 변환 작업을 하는 것이 적합하다. 데이터를 실제로 상태에 저장하려면 일반 객체(standard plain object) action 인 TOPICS_FETCHED를 dispatch하여 reducer를 호출해야 한다.

이 단계의 전체 소스는 여기에서 볼 수 있다.

서비스에 대한 몇 마디

서비스는 외부 API를 추상화하는 데 사용된다. 대부분의 경우 Reddit에서 제공하는 것과 같은 서버 API라고 할 수 있다. 이 추상화 계층의 이점은 API의 변경으로부터 최대한 많이 코드를 분리(decouple) 할 수 있다는 것이다. 만약 나중에 Reddit의 API endpoint의 이름을 바꾸거나 필드 이름을 변경한다고 해도, 우리는 앱의 서비스 레이어만 그 영향이 미치게 할 수 있다.

규칙 : 서비스는 완전히 stateless이어야한다.

이 규칙은 사실 까다롭다. 만약에 Reddit API에 로그인이 필요한 경우를 생각해보면, 로그인 세부 정보를 서비스에서 이 로그인 상태(state)를 저장 해두고 싶을 수 있다.

하지만, 이는 모든 앱 상태가 store에 포함되어야하기 때문에 이것은 허용되지 않는다. 서비스에서 상태를 다루는 것은 상태 누출(state leak)이 된다. 이 경우 허용되는 접근 방식은 모든 서비스 기능에 로그인 정보를 인수로 제공하고 우리의 reducer 중 하나에서 로그인 상태를 유지하는 것이다.

서비스 구현은 매우 간단하며 여기에서 확인할 수 있다.

시나리오 완료 - reducer와 view

일반적인 action 객체  TOPICS_FETCHED를 reducer에 도착하고 새로 가져온 topicsByUrl을 파라미터로 포함한다. 이 reducer는 이 데이터를 상태에 저장하는 것을 제외하면 많은 것을 하지는 않는다.


import * as types from './actionTypes';
import Immutable from 'seamless-immutable';
const initialState = Immutable({
topicsByUrl: undefined,
selectedTopicUrls: []
});
export default function reduce(state = initialState, action = {}) {
switch (action.type) {
case types.TOPICS_FETCHED:
return state.merge({
topicsByUrl: action.topicsByUrl
});
default:
return state;
}
}

seamless-immutable을 사용 해서 이 불변의 변경을 명시적이고 간단하게 한 것에 주목하자. 불변 데이터를 다루는 라이브러리는 물론 선택 사항인데, 나는 이 라이브러리에서 object spread를 이용한 방식을 선호 한다.

상태를 업데이트 한 후에는 뷰를 다시 렌더링해야한다. 즉, 이것과 관련한 state의 변경을 listen 해야한다는 의미이다. 이 작업은 mapStateToProps를 통해 수행된다.


import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as topicsActions from '../store/topics/actions';
import * as topicsSelectors from '../store/topics/reducer';
class TopicsScreen extends Component {
// view implementation here
}
// which props do we want to inject, given the global store state?
function mapStateToProps(state) {
return {
rowsById: topicsSelectors.getTopicsByUrl(state),
rowsIdArray: topicsSelectors.getTopicsUrlArray(state)
};
}
export default connect(mapStateToProps)(TopicsScreen);

나는 rowsById map과 rowsIdArray (React Native에서 영감을 얻은)를 필요로 하는 별도의 ListView 컴포넌트를 사용하여 토픽 목록을 렌더링 하도록 했다. TopicsScreen 컴포넌트에서 이 두 가지 props를 위해 mapStateToProps를 사용하고 있다 (이것은 나중에 ListView에 직접 전달된다). 두 props는 상태(state)에서 온다. 여기에서 흥미로운 것은 상태(state)에 직접 접근하지 않는다는 것이다. 

규칙 : 똑똑한(smart) 컴포넌트는 항상 selector를 통해 상태에 액세스해야한다.

selector는 사람들이 간과하는 Redux에서 가장 중요한 구조 중 하나이다. selector는 전역 상태(global state)를 인수로 가져 와서 변경된 상태(transformation)를 리턴하는 순수 함수다. selector는 reducer와 단단히(tightly) 결합되어 있으며 reducer.js 안에 있다. 뷰에서 사용되기 전에 데이터를 계산하는 역할을 한다. 우리는 이 아이디어를 더욱 발전시킬 것이다. 누구든지 (MapStatetoProps와 같이) 상태의 일부에 액세스해야 한다면 selector를 통과해야한다.

왜? 이 아이디어는 앱 상태의 내부 구조를 캡슐화하고 뷰에서 이를 숨기기 위함이다. 나중에 내부 상태 구조를 변경하기로 결정했다고 가정 해보자. 우리는 앱의 모든 뷰를 검토하고 리팩토링하고 싶지는 않을 것이다. selector를 통과하면 리팩토링을 reducer에만 한정 할 수 있다.

이것이 우리 topics/reducer.js의 모습입니다 :


import _ from 'lodash';
export default function reduce(state = initialState, action = {}) {
// reducer implementation here
}
// selectors
export function getTopicsByUrl(state) {
return state.topics.topicsByUrl;
}
export function getTopicsUrlArray(state) {
return _.keys(state.topics.topicsByUrl);
}

ListView를 포함한 앱의 전체 상태는 여기에서 확인 할 수 있다.

"멍청한(dumb)" 컴포넌트에 대한 몇 마디

ListView는 "멍청한(dumb)" 컴포넌트의 좋은 예다. 저장소에 연결되어 있지 않으며 Redux를 전혀 알지 못한다. /src/containers에있는 "똑똑한(smart)" 컴포넌트와 달리 이 컴포넌트는 /src/components에 위치한다.

"멍청한(dumb)" 컴포넌트는 props를 통해 부모로부터 데이터를 받고 로컬 컴포넌트 상태를 가질 수 있다. TextInput 컴포넌트를 처음부터 구현한다고 가정하자. 캐럿 위치(caret position)는 전역 앱 상태에서 관리되면 안되는 로컬 컴포넌트 상태의 좋은 예제라고 할 수 있다.

그렇다면 언제 "똑똑한(smart)" 컴포넌트에서 "멍청한(dumb)" 컴포넌트로 변경해야할까?

규칙 : "똑똑한(smart)" 컴포넌트에서 뷰 로직을 최소화 하여, "멍청한(dumb)" 컴포넌트에서 뷰로직을 다루도록 한다.

ListView의 소스를 살펴보면 행을 반복하는 것과 같은 뷰 로직이 포함되어 있음을 알 수 있다. 우리는 똑똑한(smart) TopicsScreen 컴포넌트에서 이 로직을 사용하지 않을 것이다. 이것은 똑똑한(smart) 컴포넌트를 뷰 로직에 신경쓰지 않게 한다. 또 다른 이점은 ListView 로직을 다시 사용할(reusable) 수 있다는 것이다.

다음 시나리오 - 여러 토픽 선택

첫 번째 시나리오를 완료했다. 다음으로 넘어가자. 사용자가 목록에서 정확히 3 개의 주제를 선택하게 하자.

우리 시나리오는 사용자가 토픽 중 하나를 클릭하는 것으로 시작된다. 이 이벤트는 TopicsScreen에 의해 처리되지만 이 똑똑한 컴포넌트에는 비즈니스 로직을 포함 할 수 없으므로 selectTopic이라는 새로운 action을 dispatch한다. 이 작업은 topics/actions.js에 있는 thunk가 될 것이다. 보다시피, 우리가 내보내는 거의 모든 action (뷰에서 dispatch된)은 thunk이다. 일반적으로 reducer 상태를 업데이트하기 위해 thunk 내에서만 일반 action 객체를 dispatch한다.


export function selectTopic(topicUrl) {
return (dispatch, getState) => {
const selectedTopics = topicsSelectors.getSelectedTopicUrls(getState());
if (_.indexOf(selectedTopics, topicUrl) !== -1) return;
const newSelectedTopics = selectedTopics.length < 3 ?
selectedTopics.concat(topicUrl) :
selectedTopics.slice(1).concat(topicUrl);
dispatch({ type: types.TOPICS_SELECTED, selectedTopicUrls: newSelectedTopics });
};
}

이 thunk가 흥미로운 점은 이것이 상태에 접근하는 것이 필요하다는 것이다. 모든 상태 접근이 여기에서도 selector를 통과해야 한다는 규칙을 유지하고 있음을 유의하자 (일부는 너무 지나치다고 주장 할 수도 있다).

TOPICS_SELECTED action을 처리하고 새로 선택한 토픽을 저장하려면 reducer를 업데이트해야 한다. selectTopic이 thunk 일 필요가 있는지에 대한 흥미로운 질문이 있다. 또는 selectTopic을 일반 action 객체로 만들고 이 비즈니스 로직을 reducer 자체로 옮길 수도 있다. 이것 역시 괜찮은 전략이다. 개인적으로 비즈니스 논리를 thunk로 유지하는 것을 선호한다.

상태가 업데이트되면 선택된 토픽을 뷰에 전파(propagate)해야한다. 즉, 선택한 토픽을 mapStateToProps에 추가해야 한다는 것이다. 뷰에서 모든 rowId가 선택되었는지 여부를 쿼리해야하기 때문에 이 데이터를 map 데이터로 전달하는 것이 더 편리하다. 어쨌든 데이터는 selector를 통과해야하기 때문에 변형(transformation) 작업을 하기에 좋다.

위를 구현하고 행 선택으로 인한 배경색 변경을 새로운 멍청한 컴포넌트 인 ListRow로 리팩토링 하면 다음과 같다.

비즈니스 로직에 대한 몇 마디

우리의 목표 중 하나는 뷰와 비즈니스 로직을 적절하게 분리하는 것이다. 지금까지 우리의 비즈니스 로직은 어디에 구현 되었나?

모든 비즈니스 로직은 Redux에서 /src/store 디렉토리에 구현되었다. 그것의 대부분은 actions.js의 thunk 내부에있다. 일부는 reducer.js의 selector 안에있다. 이것은 실제로 공식적인(official) 규칙이다:

규칙 : 모든 비즈니스 로직을 action 핸들러 (thunk), selector 및 reducer에 배치한다.

다음 화면으로 이동 - 포스트 목록

앱에 한 개 이상의 화면이있는 경우 탐색(navigate) 할 방법이 필요하다. 이것은 보통 react-router와 같은 네비게이션(navigation) 컴포넌트를 통해 이루어진다. 우리의 예제를 단순하게 유지하기 위해 의도적으로 라우터를 사용하는 것을 피하고 싶다.

그 대신에 우리는 사용자가 토픽 선택을 완료했는지 여부를 알려주는 상태 변수 selectionFinalized를 추가하도록 하자. 일단 사용자가 3 개의 토픽을 모두 선택하면 버튼이 표시되어 이것을 클릭하면 선택을 완료하고 다음 화면으로 이동한다. 버튼을 클릭하면 이 상태 변수를 직접 설정하는 action이 dispatch 될 것이다.

이것은 지금까지 우리가 해왔던 것과 상당히 비슷하다. 다른점이 있다면 언제 버튼을 표시 할 지를 알고 있다는 것이다 (3 개의 주제가 선택되면 바로). 이를 구현하기 위해 다른 상태 변수를 추가하는 방식으로 작업 할 수 있지만, 이 변수는 실제로 현재 상태에있는 데이터를 통해서 작업 할 수 있다. 즉, 비즈니스 로직을 selector로 구현해야 함을 의미한다:


export function isTopicSelectionValid(state) {
return state.topics.selectedTopicUrls.length === 3;
}

위의 전체 소스는 여기에서 확인 할 수 있다. 실제 화면 전환을 수행하려면 App을 연결된(connected) 컴포넌트로 변경하고 mapStateToProps에서 selectionFinalized를 리스닝(listen)해야 한다. App의 전체 소스는 여기에서 확인 할 수 있다.

포스트 화면 -  다시 말하지만 상태 먼저

이제 우리는 위에서 다뤘던 경험을 통해 두 번째 화면을 조금 더 빠르게 구현할 수 있을 것이다. 이 새 화면에서 우리는 포스트를 처리한다. 가능한 한 모듈화 된 앱을 만들기 위해 여기에 별도의 reducer와 별도의 앱 상태를 추가 할 것이다.

다시 얘기하지만, 이 화면의 목적은 토픽별로 필터링 할 수 있는 포스트 목록을 표시하는 것이다. 사용자는 포스트 목록을 클릭하고 해당 내용을 볼 수 있다. 다음과 같은 구조 일 것이다:


{
"postsById": {
"57jrtt": {
"title": "My girlfriend left me because she couldn't handle my OCD.",
"topicUrl": "/r/Jokes",
"body": "I told her to close the door five times on her way out.",
},
"57l6oa": {
"title": "Inception style vertical panoramas done with a quadcopter.",
"topicUrl": "/r/pics",
"thumbnail": "http://b.thumbs.redditmedia.com/h74JWprM3wljpdBOOpKDxt5sdZWPRtJBVULIobFfCBU.jpg",
"url": "http://i.imgur.com/d1KUJI8.jpg"
}
},
"currentFilter": "/r/Jokes",
"currentPostId": "57jrtt"
}

그리고 우리의 새로운 포스트 reducer가 여기 있다.

첫 번째 시나리오 - 필터없이 게시물 목록 표시

우리의 상태가 모델링 될 때 지금까지 했던 것처럼, 간단한 사용자 시나리오를 만들어서 그것을 처음부터 끝까지 구현해보자. 필터를 적용하지 않고 전체 게시물 목록을 표시하는 것으로 시작해 보도록 하자.

포스트를 보여주기 위해 새로운 똑똑한(smart) 컨테이너 컴포넌트가 필요하다. PostsScreen이라고 부르고 마운트 될 때 fetchPosts라는 새로운 action을 보내야한다. 이 action은 posts/actions.js의 새로운 도메인에 있는 thunk일 것이다.

이것은 이전에했던 것과 매우 유사하다. 여기를 확인 해보자

thunk 작업이 끝나면 포스트 데이터를 갖는 일반 action인 POSTS_FETCHED가 reducer로 dispatch된다. 우리는 reducer를 수정하여 데이터를 저장해야한다. PostsScreen에 목록을 표시하려면 이 상태 부분을 제공하는 selector에 mapStateToProps를 연결해야한다. 그런 다음 ListView 컴포넌트를 다시 사용하여 목록을 표시 할 수 있다.

새로운 것은 아니지만 소스는 여기에 있다.

다음 시나리오 - 포스트 목록 필터링

이 시나리오는 사용자에게 사용 가능한 필터를 보여주는 것으로 시작한다. 기존의 selector를 사용하여 토픽 reducer에서 이 데이터를 가져올 수 있다. 필터가 변경되면 게시물 reducer에서 직접 필터를 변경하는 action을 dispatch한다.

여기서 흥미로운 부분은 포스트 목록에 필터를 적용하는 것이다. 앱 상태에서는 현재 모든 postsByIdcurrentFilter를 가지고 있다. 필터링 된 결과는 앱 상태에서 나오기 때문에 저장하지 않는 것이 좋다. 데이터를 파생시키는 비즈니스 로직은 mapStateToProps의 뷰에 도착하기 직전에 selector에서 실행된다. 따라서 selector는 다음과 같다:


export function getPosts(state) {
const currentFilter = state.posts.currentFilter;
const postsById = state.posts.postsById;
const postsIdArray = currentFilter === 'all' ?
_.keys(postsById) :
_.filter(_.keys(postsById), (postId) => postsById[postId].topicUrl === currentFilter);
return [postsById, postsIdArray];
}

이 단계의 전체 구현은 여기에서 확인 할 수 있다.

마지막 시나리오 - 포스트 세부 정보 표시

이 시나리오는 실제로 가장 간단한 시나리오다. currentPostId를 포함하는 앱 상태 변수가 있다. 사용자가 action을 dispatch하여 목록의 포스트를 클릭하면 이것(currentPostId)을 업데이트만 하면된다. PostsScreen은 포스트 세부 정보를 표시하기 위해 이 상태 변수가 필요하다. 즉, mapStateToProps에서 selector가 필요하다.

자세한 구현 방법은 여기를 참조하자.



이제 끝났다!

이것으로 예제 애플리케이션의 구현을 마무리하자. 앱의 전체 소스 코드는 GitHub에서 확인 할 수 있다.

우리의 독창적인 워크 플로우(workflow) 규칙 요약

  • 앱 상태는 first class citizen으로서 in-memory 데이터베이스처럼 구조화한다.
  • 똑똑한(smart) 컴포넌트는 dispatch 작업을 제외한 모든 로직을 포함 할 수 없다.
  • 똑똑한(smart) 컴포넌트는 selector를 통해 상태에 접근해야 한다.
  • 똑똑한(smart) 컴포넌트의 뷰 로직을 멍청한(dumb) 컴포넌트로 추출하여 뷰 로직을 최소화 한다.
  • 모드 비지니스 로직은 action 핸들러(thunk), selector 및 reducer에 있어야 한다.
  • 서비스는 완전히 stateless 해야 한다.



Redux는 다양한 스타일로 작업 할 수 있는 방법을 제공한다는 것을 기억하자. 서로 다른 규칙 집합을 사용하는 여러가지 워크플로가 있다. 나의 몇몇 친구들은 thunk 대신에 redux-promise-middleware를 선호하며 모든 비즈니스 로직을 reducer에만 두는 것을 좋아하기도 한다.

자신에게 적합한 다른 방법론을 공유하고 싶다면 위의 프로젝트에 대한 구현 방식을 PR를 보내면, 비교를 위해 branch로 제공 할 것이다.




이 글은 Tal Kol의 Redux Step by Step: A Simple and Robust Workflow for Real Life Apps을 번역한 글 입니다. 전문 번역가가 아니라 잘못된 부분이 있을 수 있습니다. 지적해주시면 수정 하도록 하겠습니다. 원문은 아래에서 확인 할 수 있습니다.



리뷰