Tdd기반 React,redux,immutable 시작하기 (2)

http://www.theodo.fr/blog/2016/03/getting-started-with-react-redux-and-immutable-a-test-driven-tutorial-part-2/ 를 번역한 글입니다.

이 포스트는 React, Redux, Immutable 튜토리얼의 2번째이자 마지막 파트입니다.
만약 여러분이 첫번째 튜토리얼을 놓쳤다면, 첫번째 파트를 여기서 보실 수 있습니다.

첫번째 포스트에서는 모듈 컴포넌트를 개발하고 유닛 테스팅을 하였고, 앱 UI의 뼈대를 만들었습니다.

우리는 React props를 이용해서 각 컴포넌트에 앱의 상태를 전달하고, 사용자-액션이 콜백으로 선언 되어짐으로써 UI와 앱 로직을 분리한다는 것을 알 수 있었습니다.

현재 온보딩 중이거나, 두번째 파트 부터 시작하려고 하면 여기서부터 시작하는 저장소 에 관련 커밋을 여기서 볼 수 있습니다.

저장소를 복제하고 이제 따라오세요.!

Redux 워크플로우 소개

이 시점에서 우리 UI는 상호작용하지 않고 있습니다.: 아이템이 완료로 설정되면, 그 아이템은 줄이 그어질 것이라고 테스트를 했지만, 실제로 아직 사용자가 아이템을 완료할 방법이 없습니다.
Redux 에코시스템에서 UI 업데이트와 사용자 옵션은 항상 같은 워크플로우를 따릅니다.

  • 상태트리는 props로 UI와 액션 콜백을 정의합니다.
  • 클릭과 같은 사용자 액션은 그것들을 정규화 하는 액션 생성자에게 전달됩니다.
  • redux 액션 결과는 실제 앱 로직을 구현한 리듀서로 전달 됩니다.
  • 리듀서는 상태트리를 업데이트 하고 디스패치하여 그 상태를 스토어에 저장합니다.
  • UI는 스토어에 새로운 상태트리에 따라 업데이트 됩니다.

리덕스 워크플로우

초기 상태 설정

참고 : 여기 관련 커밋이 있습니다.

첫번째 액션으로 적절하게 Redux 스토어에 초기 상태를 설정 할 수 있습니다.

Redux 액션은 정보의 payload 입니다. 이를테면, 액션이 무엇을 하는지와 앱이 필요하는 기타 정보를 간결하게 설명하는 type 속성을 가진 JSON객체로 표현합니다.
아래 같은 경우엔, 타입은 SET_STATE 설정 할 수 있다라는 것이며, 우리가 원하는 상태가 포함된 상태 객체를 추가 할 수 있다라는 것입니다.

{
  type: 'SET_STATE',
  state: {
    todos: [
      {id: 1, text: 'React', status: 'active', editing: false},
      {id: 2, text: 'Redux', status: 'active', editing: false},
      {id: 3, text: 'Immutable', status: 'active', editing: false},
    ],
    filter: 'all'
  }
}

이 액션은 리듀서로 디스패치 될 것이며, 리듀서의 역할을 식별하고 액션과 연관된 실제 로직을 구현 합니다.
다음의 경우, 로직은 스토어에 새로운 상태를 저장하여 앱을 통해 전달 되게 합니다.
리듀서 유닛 테스트를 진행보겠습니다.

test/reducer_spec.js
import {List, Map, fromJS} from 'immutable';
import {expect} from 'chai';

import reducer from '../src/reducer';

describe('reducer', ()=>{
  it('handles SET_STATE', ()=>{
    const initialState = Map();
    const action = {
      type: 'SET_STATE',
      state: Map({
        todos : List.of(
          Map({id:1, text:'React', status:'active'}),
          Map({id:2, text:'Redux', status:'active'}),
          Map({id:3, text:'Immutable', status:'completed'})
        )
      })
    };

    const nextState = reducer(initialState, action);

    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    }));
  });
});

편의상, 불변 데이터 구조를 사용하는 대신에 일반 javascript로 상태 객체를 작성하고, 리듀서가 변환 처리를 하도록 하고 싶습니다. 마지막으로 리듀서는 undefined 인 초기 상태를 우아하게 처리해야 합니다.

test/reducer_spec.js
// ...
describe('reducer', () => {
  // ...
  it('handles SET_STATE with plain JS payload', ()=>{
    const initialState = Map();
    const action = {
      type : 'SET_STATE',
      state : {
        todos: [
          {id: 1, text: 'React', status: 'active'},
          {id: 2, text: 'Redux', status: 'active'},
          {id: 3, text: 'Immutable', status: 'completed'}
        ]
      }
    };

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    }));
  });

  it('handles SET_STATE without initial state', ()=>{
    const action = {
      type : 'SET_STATE',
      state : {
        todos: [
          {id: 1, text: 'React', status: 'active'},
          {id: 2, text: 'Redux', status: 'active'},
          {id: 3, text: 'Immutable', status: 'completed'}
        ]
      }
    };

    const nextState = reducer(undefined, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    }));
  });
});

리듀서는 액션에서 들어온 타입 유형을 매칭하고, 만약 타입이 SET_STATE이면, 현재 상태와(우리의 경우 초기상태) 병합될 것 입니다.

src/reducer.js
import {Map} from 'immutable';

function setState(state, newState){
  return state.merge(newState);
}

export default function(state = Map(), action){
    switch(action.type){
      case 'SET_STATE':
        return setState(state, action.state);
    }
    return state;
}

앱이 실행될 때, 초기 상태가 우리 액션으로 설정하기 위해 우리는 앱과 리듀서를 연결 해야 합니다.
사실 우린 실제 Redux 라이브러리를 처음 사용하기 때문에, Redux를 설치 해야합니다.

npm install --save redux@3.3.1 react-redux@4.4.1
src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import {List, Map} from 'immutable';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import reducer from './reducer';
import {TodoAppContainer} from './components/TodoApp';

// 새로운 Redux 스토어 초기화
const store = createStore(reducer);
// 원하는 상태로 유지하기 위해 SET_STATE 디스패치
store.dispatch({
  type: 'SET_STATE',
  state: {
    todos: [
      {id: 1, text: 'React', status: 'active', editing: false},
      {id: 2, text: 'Redux', status: 'active', editing: false},
      {id: 3, text: 'Immutable', status: 'active', editing: false},
    ],
    filter: 'all'
  }
});

require('../node_modules/todomvc-app-css/index.css');

ReactDOM.render(
  // 컴포넌트로 스토어를 전달하기 위한 Provider 컴포넌트를 랩핑합니다.
  <Provider store={store}>
    <TodoAppContainer />
  </Provider>,
  document.getElementById('app')
);

앞선 코드스니펫을 잘 살펴보면, TodoApp 컴포넌트가 TodoAppContainer 컴포넌트로 대체 된 것을 알 수 있습니다. Redux에는 PresentationalContainer 라는 두가지 타입의 컴포넌트가 있습니다. Dan Abramov의 highly informative article 기사를 보면 두 타입의 차이점을 알 수 있습니다.

빠르게 Redux 문서를 인용하여 요약 해드리겠습니다.

Presentational 은 어떻게 보이는지(스타일, 템플릿), 그리고 Container 는 어떻게 작동하는지(데이터 패칭, 상태 업데이트) 에 관한 것입니다.

자 이제 우리는 스토어를 설정 하였고, 스토어를 TodoAppContainer 컴포넌트에 전달 하였습니다. 하지만 자식 컴포넌트가 스토어를 이해 하기 위해선, TodoApp 컴포넌트의 React props 와 상태 속성을 매핑 해야 합니다. 이 작업이 TodoAppContainer 에게 주는 행위 입니다.

src/components/TodoApp.jsx
// ...
import {connect} from 'react-redux';

export class TodoApp extends React.Component{
  // ...
}

function mapStateToProps(state){
  return {
    todos : state.get('todos'),
    filter : state.get('filter')
  };
}

export const TodoAppContainer = connect(mapStateToProps)(TodoApp);

브라우저에서 앱을 리로드 하게되면, Redux 툴을 사용하지 않는 이상 이전과 같이 초기화 된 상태를 보게 될 것 입니다.

Redux dev tools

참고 : 여기에 관련 커밋이 있습니다.

Redux 스토어와 리듀서를 설정했으므로, 이제 우리는 간소화된 개발을 위해 Redux dev tools 을 설정 하겠습니다.
첫번째로, Redux 개발도구 크롬확장프로그램 으로 가서 설치 하세요.
개발 도구는 스토어 생성시에 활성화 됩니다.

src/index.jsx
// ...
import {compose, createStore} from 'redux';

const createStoreDevTools = compose(
  window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
const store = createStoreDevTools(reducer);
// ...

dev tools

브라우저에서 앱을 리로드하고 개발도구에서 Redux 아이콘을 클릭하면 보여집니다.

Diff Monitor, Log Monitor, Slider Monitor 의 3가지 다른 모니터링을 바로 사용 할 수 있습니다.

액션 생성자로 액션 설정

아이템 상태 토글

참고 : 여기에 관련 커밋이 있습니다.

다음 단계로 todos 아이템의 활성화와 완료 상태 사이를 사용자가 토글 할 수 있게 해줄 것입니다.

첫번째로, 리듀서는 활성화와 완료 상태 사이를 변경하는 새로운 액션인 TOGGLE_COMPLETE 를 처리 해야합니다.

test/reducer_spec.js
import {List, Map, fromJS} from 'immutable';
import {expect} from 'chai';

import reducer from '../src/reducer';

describe('reducer', () => {
  // ...

  it('handles TOGGLE_COMPLETE by changing the status from active to completed', ()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    });

    const action = {
      type: 'TOGGLE_COMPLETE',
      itemId: 1
    }

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'completed'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    }));
  });

  it('handles TOGGLE_COMPLETE by changing the status from completed to active', ()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'completed'}
      ]
    });
    const action = {
      type: 'TOGGLE_COMPLETE',
      itemId : 3
    }

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
        {id: 3, text: 'Immutable', status: 'active'}
      ]
    }));
  });
});

테스트를 통과하기 위해 리듀서를 업데이트 하겠습니다.

src/reducer.js
// ...
function toggleComplete(state, itemId){
  // 아이템 ID로 할당된 index 찾기
  const itemIndex = state.get('todos').findIndex(
    (item) => item.get('id') === itemId;
  );

  // index 로 todo를 업데이트
  const updatedItem = state.get('todos')
    .get(itemIndex)
    .update('status', status => status === 'active' ? 'completed' : 'active');

  // 수정된 todo아이템으로 상태를 업데이트
  return state.update('todos', todos => todos.set(itemIndex, updatedItem));
}

export default function(state=Map(), action){
  switch (action.type) {
    case 'SET_STATE':
      return setState(state, action.state);
    case 'TOGGLE_COMPLETE':
      return toggleComplete(state, action.itemId);
  }
  return state;
}

우리는 SET_STATE 액션과 같은 선상에서, TodoAppContainer 컴포넌트도 이 액션을 인식 시키도록 만들어야 합니다. 따라서 toggleComplete 콜백은 TodoItem 컴포넌트로(실제 호출하는 컴포넌트) 전달 됩니다.

Redux에는 이를 진행 하기 위한 표준 방법으로 액션 생성자(Action Creators)가 있습니다.
액션 생성자는 적절하게 포맷팅된 액션을 반환하는 간단한 함수 입니다. - 이 함수는 React props와 매핑되어집니다.

이제 첫번째 액션 생성자를 만들어 보겠습니다.

src/action_creators.js
export function toggleComplete(itemId){
  return {
    type: 'TOGGLE_COMPLETE',
    itemId
  }
}

이제 스토어 데이터를 가져오는데 사용했던 TodoAppContainer 컴포넌트 안에 connect함수를 호출 합니다. 그리고 props 콜백을 액션 생성자의 같은 이름과 매핑 하도록 지시합니다.

src/components/TodoApp.jsx
// ...
import * as actionCreators from '../action_creators';
export class TodoApp extends React.Component{
  // ..
  render() {
    return <div>
      // ...
        // spread연산자로 넘기기
        <TodoList {...this.props} />
      // ...
      </div>
  }
};

export const TodoAppContainer = connect(mapStateToProps, actionCreators)(TodoApp);

이제 웹서버와 브라우저를 재시작 해보세요. 이제 아이템을 클릭하면 상태가 적절하게 토글됩니다. 그리고 Redux 개발툴을 보면, 액션이 트리거 되는것과 후속 업데이트 되는 상태를 볼 수 있습니다.

##현재 필터 변경

여기서 다음 관련 커밋을 볼 수 있습니다.

모든 세팅이 완료 되었으니, 다른 액션을 작성하는 것은 쉽습니다. 이제 우리는 CHANGE_FILTER 액션을 작성할 것 입니다. 아마 추측할 수 있을텐데, 현재 상태 필터를 변경하면 표시되는 아이템이 필터링 되는 것입니다.

액션 생성자를 작성해보도록 하겠습니다.

src/action_creators.js
// ...
export function changeFilter(filter){
  return {
    type: 'CHANGE_FILTER',
    filter
  }
}

이제 리듀서용 유닛테스트를 작성합니다.

test/reducer_spec.js
// ...
describe('reducer', () => {
  // ...
  it('les CHANGE_FILTER by changing the filte', () =>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
      ],
      filter: 'all'
    });
    const action = {
      type: 'CHANGE_FILTER',
      filter: 'active'
    }
    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
      ],
      filter: 'active'
    }));
  });
});

이제 리듀서 함수를 작성하겠습니다.

src/reducer.js
// ...
function changeFilter(state, filter){
  return state.set('filter', filter);
}
export default function(state=Map(), action){
  switch(action.type){
    case 'SET_STATE':
      return setState(state, action.state);
    case 'TOGGLE_COMPLETE':
      return toggleComplete(state, action.itemId);
    case 'CHANGE_FILTER':
      return changeFilter(state, action.filter);
  }
  return state;
}

마지막으로, 우리는 changeFilter 콜백을 TodoTools 컴포넌트로 전달해야 합니다.

TodoApp.jsx
// ...
export class TodoApp extends React.Component{
  // ..
  render() {
    return <div>
      <section className="todoapp">
        // ...
        <TodoTools changeFilter={this.props.changeFilter}
                    filter={this.props.filter}
                    nbActiveItems={this.getNbActiveItems()} />
      </section>
      <Footer />
    </div>
  }
};

이게 끝입니다. 필터가 완벽하게 작동할 것입니다.

아이템 수정

여기 관련 커밋이 있습니다.

사용자가 아이템을 수정할 때, 실제로는 3가지 중 2가지 액션이 트리거된 작업이 있습니다.

  • 사용자가 엔터를 치면 수정모드로 돌입: EDIT_ITEM;
  • 사용자가 저장하지 않은채 수정모드를 취소할 때: CANCEL_EDITING;
  • 사용자가 저장하고 유효성 검사: DONE_EDITING;

이제 위 3개의 액션을 위한 액션 생성자를 작성 하겠습니다.

src/action_creators.js
// ...
export function editItem(itemId){
  return {
    type: 'EDIT_ITEM',
    itemId
  }
}

export function cancelEditing(itemId){
  return {
    type: 'CANCEL_EDITING',
    itemId
  }
}

export function doneEditing(itemId, newText){
  return {
    type: 'DONE_EDITING',
    itemId,
    newText
  }
}

이제 각 액션을 위한 유닛테스트를 작성하겠습니다.

test/reducer_spec.js
// ...
describe('reducer', ()=>{
  // ...
  it('handles EDIT_ITEM by setting editing to true',()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active', editing: false},
      ]
    });
    const action = {
      type: 'EDIT_ITEM',
      itemId: 1
    }
    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos:[
         {id: 1, text: 'React', status: 'active', editing: true},
      ]
    }));
  });

  it('handles CANCEL_EDITING by setting editing to false', ()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active', editing: true},
      ]
    });
    const action = {
      type: 'CANCEL_EDITING',
      itemId: 1
    }
    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active', editing: false},
      ]
    }));
  });

  it('handles DONE_EDITING by setting by updating the text', ()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active', editing: true},
      ]
    });
    const action = {
      type: 'DONE_EDITING',
      itemId: 1,
      newText: 'Redux'
    }
    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'Redux', status: 'active', editing: false},
      ]
    }));
  });
});

이제 우리는 3가지 액션을 실제로 다룰 수 있는 리듀서 함수를 개발 할 수 있습니다.

src/reducer.js
function findItemIndex(state, itemId){
  return state.get('todos').findIndex(
    (item) => item.get('id') === itemId
  );
}

// findItemIndex 사용하기 위해 toggleComplete 함수를 리팩토링 합니다.
function toggleComplete(state, itemId){
  const itemIndex = findItemIndex(state, itemId);
  const updatedItem = state.get('todos')
    .get(itemIndex)
    .update('status', status => status === 'active' ? 'completed' : 'active');

  return state.update('todos', todos => todos.set(itemIndex, updatedItem));
}

function editItem(state, itemId){
  const itemIndex = findItemIndex(state, itemId);
  const updatedItem = state.get('todos')
    .get(itemIndex)
    .set('editing', true);

  return state.update('todos', todos => todos.set(itemIndex, updatedItem));
}

function cancelEditing(state, itemId){
  const itemIndex = findItemIndex(state, itemId);
  const updatedItem = state.get('todos')
    .get(itemIndex)
    .set('editing', false);

  return state.update('todos', todos => todos.set(itemIndex, updatedItem));
}

function doneEditing(state, itemId, newText){
  const itemIndex = findItemIndex(state, itemId);
  const updatedItem = state.get('todos')
    .get(itemIndex)
    .set('editing', false)
    .set('text', newText)

  return state.update('todos', todos => todos.set(itemIndex, updatedItem));
}

export default function(state=Map(), action){
  switch(action.type){
    // ...
    case 'EDIT_ITEM':
      return editItem(state, action.itemId);
    case 'CANCEL_EDITING':
      return cancelEditing(state, action.itemId);
    case 'DONE_EDITING':
      return doneEditing(state, action.itemId, action.newText);
  }
  return state;
}

이제 브라우저에서 잘 작동 할 것입니다.

완료 삭제와 아이템 추가 삭제

여기 관련 커밋이 있습니다.

나머지 3개 액션은 다음과 같습니다.

  • CLEAR_COMPLETED, TodoTools 컴포넌트에서 트리거되며, 리스트에 완료된 아이템을 없애줍니다.
  • ADD_ITEM, TodoHeader 컴포넌트에서 트리거되며, 사용자가 엔터친 텍스트로 된 아이템이 추가됩니다.
  • DELETE_ITEM, TodoItem에서 호출되며, 아이템을 삭제합니다.

우리는 이제 액션 생성자 추가, 리듀서 유닛데스트와 코드로직, props로 콜백을 전달하는 이벤트같은 워크플로우를 사용하겠습니다.

src/action_creators.js
// ...
export function clearCompleted(){
  return {
    type : 'CLEAR_COMPLETED'
  }
}

 export function addItem(text){
  return {
    type: 'ADD_ITEM',
    text
  }
 }

 export function deleteItem(itemId){
  return {
    type: 'DELETE_ITEM',
    itemId
  }
 }
test/reducer_spec.js
// ...
describe('reducer', () => {
  // ... 
  it('handles CLEAR_COMPLETED by removing all the completed items', ()=>{
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'completed'},
      ]
    });

    const action = {
      type: 'CLEAR_COMPLETED'
    }

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
      ]
    }));
  });

  it('handles ADD_ITEM by adding the item', () => {
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'}
      ]
    });

    const action = {
      type: 'ADD_ITEM',
      text: 'Redux'
    }

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'active'},
      ]
    }));
  });

  it('handles DELETE_ITEM by removing the item', () => {
    const initialState = fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
        {id: 2, text: 'Redux', status: 'completed'},
      ]
    });

    const action = {
      type: 'DELETE_ITEM',
      itemId: 2
    }

    const nextState = reducer(initialState, action);
    expect(nextState).to.equal(fromJS({
      todos: [
        {id: 1, text: 'React', status: 'active'},
      ]
    }));
  });
});
src/reducer.js
function clearCompleted(state){
  return state.update('todos',
    (todos) => todos.filterNot(
      (item) => item.get('status') === 'completed'
    )
  );
}

function addItem(state, text){
  const itemId = state.get('todos').reduce((maxId, item) => Math.max(maxId, item.get('id')), 0) + 1;
  const newItem = Map({id: itemId, text: text, status: 'active'});
  return state.update('todos', (todos) => todos.push(newItem));
}

function deleteItem(state, itemId){
  return state.update('todos',
    (todos) => todos.filterNot(
      (item) => item.get('id') === itemId
    )
  );
}

export default function(state = Map(), action){
  switch(action.type){
    // ...
    case 'CLEAR_COMPLETED':
      return clearCompleted(satate);
    case 'ADD_ITEM':
      return addItem(state, action.text);
    case 'DELETE_ITEM':
      return deleteItem(state, action.itemId);
  }
  return state;
}
src/components/TodoApp.jsx
// ...
export class TodoApp extends React.Component{
  // ...
  render() {
    return <div>
      <section className="todoapp">
        // addItem 콜백을 전달
        <TodoHeader addItem={this.props.addItem}/>
        <TodoList {...this.props} />
        // clearCompleted 콜백 전달
        <TodoTools changeFilter={this.props.changeFilter}
                    filter={this.props.filter}
                    nbActiveItems={this.getNbActiveItems()}
                    clearCompleted={this.props.clearCompleted} />
      </section>
      <Footer />
    </div>
  }
};

와우 TodoMVC 앱이 이제 완성되었습니다.

마무리

React, Redux, Immutable 스택 TDD 튜토리얼은 마쳤습니다.

더 많은 것들을 알고 싶어하는 여러분들을 위해 다음과 같은 것들이 있습니다.

  • React Redux router SPA를 완벽하게 구축할 수 있습니다.
  • 백엔드에서 Redux를 사용하여 Isomorphic Redux2개 튜토리얼 에서 광범위하게 다뤄지고있습니다.
  • Gambit은 API와 간단하게 연결 하게 해주는 작은 Redux 래퍼입니다.
  • Dan Abramov(redux 창시자)이 만든 무료 비디오영상은 깊이있는 Redux에 대한 내용을 다루고 있습니다.
  • Redux 웹사이트에서 더 많은 것을 이용할 수 있습니다.
Written on January 1, 2018