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 |