본문 바로가기

React

Redux 시작하기 기초 - (마지막) React와 함께 사용하기

React와 함께 사용하기



Redux와 React는 관계가 없다. 그렇다면 왜 같이 사용하는 걸까? Redux는 앞에서 배운 것과 같이 액션을 통해 상태를 변경한다. 이 때문에 UI를 상태에 대한 함수로 기술하는 React와 잘 어울리기 때문이다.


할 일 앱을 React로 만들어보자!


React Redux 설치하기


React 바인딩은 Redux에 기본적으로 포함되어 있지 않다. 명시적으로 설치를 해줘야 한다.


npm install --save react-redux


Smart Component(영민한)와 Dumb Component(우직한)


영민한 컴포넌트우직한 컴포넌트
위치최상위, 라우트 핸들러중간과 말단 컴포넌트
Redux와 연관됨아니오
데이터를 읽기 위해Redux 상태를 구독props에서 데이터를 읽음
데이터를 바꾸기 위해Redux 액션을 보냄props에서 콜백을 부름

우리는 기초 1편에서 루트 상태 객체의 형태를 설계하였다. 이제 그에 맞게 UI 계층을 설계해보자. 이는 Redux에 한정되는 것이 아니다. Thinking In React(https://reactjs.org/docs/thinking-in-react.html)에서 잘 설명해주고 있다.

{
  visibilityFilter: 'SHOW_ALL',
  todos: [{
    text: 'Consider using Redux',
    completed: true,
  }, {
    text: 'Keep all state in a single tree',
    completed: false
  }]
}


사용자가 할일을 추가할 필드가 있고, 할 일을 클릭하면  완료한 것으로 표시된다. 그리고 푸터에는 완료/완료되지 않은/모든 할 일 각각의 모드로 볼 수 있께 해주는 토글이 있다.


이 것을 컴포너틑로 끌어내면 다음과 같다.


  • AddTodo는 버튼이 달린 입력 필드입니다.
    • onAddClick(text: string)은 버튼을 누르면 불러올 콜백입니다.
  • TodoList는 표시중인 할일 목록입니다.
    • todos: Array { text, completed } 형태의 할일 배열입니다.
    • onTodoClick(index: number)은 할일을 누르면 호출할 콜백입니다.
  • Todo는 할일 하나입니다.
    • text: string은 보여줄 텍스트입니다.
    • completed: boolean은 할일을 완료된것으로 표시할지 여부입니다.
    • onClick()은 할일을 누르면 호출할 콜백입니다.
  • Footer는 표시할 할일 필터를 사용자가 바꿀 수 있는 컴포넌트입니다.
    • filter: string은 현재 필터입니다: 'SHOW_ALL', 'SHOW_COMPLETED', 'SHOW_ACTIVE'이 있습니다.
    • onFilterChange(nextFilter: string)사용자가 다른 필터를 선택했을 때 호출할 콜백입니다.


이들 모두 우직한 컴포넌트이다. Redux에 의존성이 없으며, 데이터가 어디에서 오는지, 어떻게 바꾸는지 모른다. 그저 주어진대로 그려낼 뿐이다.

components/AddTodo.js

import React, { findDOMNode, Component, PropTypes } from 'react';

export default class AddTodo extends Component {
  render() {
    return (
      <div>
        <input type='text' ref='input' />
        <button onClick={e => this.handleClick(e)}>
          Add
        </button>
      </div>
    );
  }

  handleClick(e) {
    const node = findDOMNode(this.refs.input);
    const text = node.value.trim();
    this.props.onAddClick(text);
    node.value = '';
  }
}

AddTodo.propTypes = {
  onAddClick: PropTypes.func.isRequired
};

components/Todo.js

import React, { Component, PropTypes } from 'react';

export default class Todo extends Component {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={{
          textDecoration: this.props.completed ? 'line-through' : 'none',
          cursor: this.props.completed ? 'default' : 'pointer'
        }}>
        {this.props.text}
      </li>
    );
  }
}

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired
};

components/TodoList.js

import React, { Component, PropTypes } from 'react';
import Todo from './Todo';

export default class TodoList extends Component {
  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) =>
          <Todo {...todo}
                key={index}
                onClick={() => this.props.onTodoClick(index)} />
        )}
      </ul>
    );
  }
}

TodoList.propTypes = {
  onTodoClick: PropTypes.func.isRequired,
  todos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired
};

components/Footer.js

import React, { Component, PropTypes } from 'react';

export default class Footer extends Component {
  renderFilter(filter, name) {
    if (filter === this.props.filter) {
      return name;
    }

    return (
      <a href='#' onClick={e => {
        e.preventDefault();
        this.props.onFilterChange(filter);
      }}>
        {name}
      </a>
    );
  }

  render() {
    return (
      <p>
        Show:
        {' '}
        {this.renderFilter('SHOW_ALL', 'All')}
        {', '}
        {this.renderFilter('SHOW_COMPLETED', 'Completed')}
        {', '}
        {this.renderFilter('SHOW_ACTIVE', 'Active')}
        .
      </p>
    );
  }
}

Footer.propTypes = {
  onFilterChange: PropTypes.func.isRequired,
  filter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
};

됐습니다! 이들이 제대로 작동하는지 확인하기 위해 더미 App을 작성해 보겠습니다:

containers/App.js

import React, { Component } from 'react';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';

export default class App extends Component {
  render() {
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            console.log('add todo', text)
          } />
        <TodoList
          todos={[{
            text: 'Use Redux',
            completed: true
          }, {
            text: 'Learn to connect it to React',
            completed: false
          }]}
          onTodoClick={todo =>
            console.log('todo clicked', todo)
          } />
        <Footer
          filter='SHOW_ALL'
          onFilterChange={filter =>
            console.log('filter change', filter)
          } />
      </div>
    );
  }
}

<App />은 이렇게 표현됩니다:

이 자체로는 별로 흥미로울게 없다. 이제 Redux와 연결해보자!


Redux와 연결하기


우리는 App 컴포넌트를 Redux와 연결하기 위해 두가지를 수정해야 한다.

1. react-redux에서 Provider를 불러와서, <Provider>로 루트 컴포넌트를 감싸준다.

index.js

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';

let store = createStore(todoApp);

let rootElement = document.getElementById('root');
React.render(
  // React 0.13의 이슈를 회피하기 위해 
  // 반드시 함수로 감싸줍니다.
  <Provider store={store}>
    {() => <App />}
  </Provider>,
  rootElement
);


2. Redux와 연결하고 싶은 컴포넌트를 react-redux의 connect()함수로 감싸준다.

가능한 최상위 컴포넌트나 라우트 핸들러만 이렇게 해주자. 왜냐하면 데이터 흐름을 추적하기가 어려워진다.(?)

containers/App.js

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';

class App extends Component {
  render() {
    // connect() 호출을 통해 주입됨:
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  })),
  visibilityFilter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
};

function selectTodos(todos, filter) {
  switch (filter) {
  case VisibilityFilters.SHOW_ALL:
    return todos;
  case VisibilityFilters.SHOW_COMPLETED:
    return todos.filter(todo => todo.completed);
  case VisibilityFilters.SHOW_ACTIVE:
    return todos.filter(todo => !todo.completed);
  }
}

// 주어진 전역 상태에서 어떤 props를 주입하기를 원하나요?
// 노트: 더 나은 성능을 위해서는 https://github.com/faassen/reselect 를 사용하세요
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  };
}

// 디스패치와 상태를 주입하려는 컴포넌트를 감싸줍니다.
export default connect(select)(App);


'React' 카테고리의 다른 글

3장. Immutable.js 익히기  (0) 2019.10.28
React vs Vue  (0) 2019.02.28
Redux 기초 - (2) 스토어(Store), 데이터 흐름  (0) 2017.11.21
Redux 기초 - (1)액션, 리듀서  (0) 2017.11.20
#4. React 성능과 렌더링  (0) 2017.06.19