폴더 구조
React와 Redux를 사용하는 프로젝트의 폴더는 크게 action폴더와 component폴더, reducer폴더, store 폴더로 구성된다.
action 폴더
action 폴더는 애플리케이션에서 사용하는 명령어(action type)와 API 통신과 같은 작업을 하는 액션 매서드(action creator)를 모아둔 폴더다. 서비스에 따라 모든 명령어와 액션 메서드를 한 곳에 모아 두거나 도메인별로 구분해 나눠 놓기도 한다.
// action type(명령어)
export const COMPLETE_TODO = 'COMPLETE_TODO'
// action creators(액션 메서드)
export function complete({complete, id}) {
return { type: COMPLETE_TODO, complete, id};
}
액션 메서드에서는 리듀서로 데이터 생성을 요청한다. 비즈니스 로직을 주로 액션 메서드에서 개발하기 때문에 액션 메서드는 컴포넌트의 재활용을 높이고 코드를 관리하는 데 중요한 역할을 한다.
component 폴더
컴포넌트는 보통 도메인별로 구분돼 있다.
컴포넌트는 컨테이너 컴포넌트와 프리젠테이션 컴포넌트를 구분해서 개발한다.
컨테이너 컴포넌트는 여러 개의 프레젠테이션 컴포넌트로 구성돼 있으며, 데이터나 공통으로 관리해야 하는 객체, 컴포넌트 간의 인터랙션 등을 관리하는 컴포넌트다. 프리젠테이션 컴포넌트는 일반적으로 알고 있는 UI 컴포넌트라 생각함녀 된다. 즉, UI 컴포넌트인 프리젠테이션 컴포넌트를 컨테이너 컴포넌트에서 관리한다고 생각하면 간단하다.
프리젝테이션 컴포넌트의 prop에 state를 설정하고, 액션을 보내는 함수를 설정했다.
컨테이너 컴포넌트 TODOList.js
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
// 컨테이너 컴포넌트에서 프레젠테이션 컴포넌트로 전달하는 state
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
// 컨테이너 컴포넌트에서 프레젠테이션 컴포넌트로 액션을 보내는 함수
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
dispatch(complete(data)) // 액션 메서드
}
}
}
// 연결
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
프리젠트 컴포넌트 TODO.js
class TODO extends Component {
render() {
const {id, todo, complete, onClick} = this.props; // 컨테이너 컴포넌트에서 받은 prop
return (
<li id={id}
onClick={() => onClick({
id : id,
complete : !complete
})}
className={!!complete ? 'completed' : ''}
>{todo}</li>
);
}
}
예를 살펴보면 프레젠테이션 컴포넌트에는 비즈니스 로직이 없다. 비즈니스 로직은 컨테이너 컴포넌트에서 개발한다. 그래야 프레젠테이션 컴포넌트인 TODO 컴포넌트의 재활용성이 높아진다.
reducer 폴더
리듀서는 액션 메서드에서 변경한 상태를 받아 기존의 상태를 새로운 상태로 변경하는 일을 한다. reducer폴더는 action 폴더와 같이 하나로 만들기도 하지만 도메인별로 구분해 만들기도 한다. 액션 파일과 리듀서 파일을 합쳐서 사용하는 ducks 기법도 있다.
reducer/todo.js
import todoAction from '../action/index';
const {ADD_TODO} = todoAction.todo;
const todo = (state, action) => {
switch (action.type) {
case ADD_TODO:
return {
text: action.text,
completed: false
};
default:
return state;
}
}
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state, todo(undefined, action)
];
default:
return state;
}
}
다음 코드는 리듀서를 합쳐서 사용하는 index.js 파일의 예이다.
export default combineReducers({
todos
});
store 폴더
store 폴더에는 index.js 파일 하나만 있으며, 주로 미들웨어를 설정하는 일을 한다. 예를 들어 비동기 통신을 사용하기 위해 redux-thunk 라이브러리를 설정하거나, state의 변경 내역을 확인하기 위해 react-router-redux 라이브러리를 추가하거나, 디버깅을 위해 react-devtool을 설정하는 일을 주로 한다.
import { createStore, compose, applyMiddleware } from "redux";
import thunk from "redux-thunk";
export default function configureStore(reducer, initialState = {}) {
const storeEnhancers = compose(
applyMiddleware(thunk)
);
return createStore(reducer, initialState, storeEnhancers);
}
작동 과정
Redux를 사용할 때 브라우저의 이벤트를 받아 뷰를 바꾸는 과정은 다음과 같다.
1. 브라우저에서 이벤트가 발생한다.
2. 컴포넌트에서 이벤트가 발생한다. (todolistDispatchToProps의 onClick)
src/components/todoList/todoList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { complete, complete2 } from '../../action/todo';
import Todo from './TODO';
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
// dispatch(complete2(data))
dispatch(complete(data))
}
}
}
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
3. 액션 메서드가 호출된다. (todolistDispatchToProps의 dispatch(complete(data)))
src/components/todoList/todoList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { complete, complete2 } from '../../action/todo';
import Todo from './TODO';
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
// dispatch(complete2(data))
dispatch(complete(data))
}
}
}
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
4. 스토어의 dispatch() 메서드가 호출된다.(todolistDispatchToProps의 dispatch(complete(data)))
src/components/todoList/todoList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { complete, complete2 } from '../../action/todo';
import Todo from './TODO';
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
// dispatch(complete2(data))
dispatch(complete(data))
}
}
}
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
5. 스토어에서 리듀서를 호출한다. 샘플 코드에서는 todos 리듀서를 호출한다. (const todos)
src/reducer/todo.js
import todoAction from '../action/index';
const {ADD_TODO, COMPLETE_TODO} = todoAction.todo;
const todo = (state, action) => {
switch (action.type) {
case ADD_TODO:
return {
id :Math.floor(Math.random() * 100) + 1,
todo: action.text,
completed: false
};
case COMPLETE_TODO:
if (state.id !== action.id) {
return state;
}
return {
...state,
complete: !state.complete
};
default:
return state;
}
}
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state, todo(undefined, action)
];
case COMPLETE_TODO:
return state.map(t => todo(t, action));
default:
return state;
}
}
export default todos;
6. subscribe() 메서드로 등록한 리스너를 호출한다. 샘플코드에서는 render()메서드를 호출해 뷰를 갱신한다. (store.subscribe(render))
src/index.js
import React from 'react';
import configureStore from './store/index';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import reducer from './reducer/index';
import App from './component/App'
import './index.css';
const store = configureStore(reducer,{
"todos" : [
{"id":1, "todo":"빨래하기", "complete":false},
{"id":2, "todo":"청소하기", "complete":false},
{"id":3, "todo":"공부하기", "complete":false}
]
});
const render = () => {
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
)
};
store.subscribe(render);
render();
애플리케이션 개발할 때는 다음과 같은 절차로 개발하면 애플리케이션을 좀 더 쉽게 개발할 수 있다.
1. React 컴포넌트 만들기 : 하위 React 컴포넌트로 props와 dispatch() 메서드를 전달한다.
src/component/todolist/TODOList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { complete, complete2 } from '../../action/todo';
import Todo from './TODO';
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
// dispatch(complete2(data))
dispatch(complete(data))
}
}
}
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
2. 액션 명령어와 액션 메서드 만들기: state 변경과 비동기 처리를 구현한다.
src/action/todo.js
// action type
const ADD_TODO = 'ADD_TODO'
const COMPLETE_TODO = 'COMPLETE_TODO'
// action creators
function addTodo(text) {
return { type: ADD_TODO, text};
}
function addTodo2(text) {
return (dispatch) => {
return fetch("api/add.json").then(
res => res.json().then(data => dispatch(addTodo(data.status)))
);
};
}
function complete({complete, id}) {
return { type: COMPLETE_TODO, complete, id};
}
function complete2(data2) {
return (dispatch) => {
return fetch("api/add.json").then(
res => res.json().then(data => dispatch(complete(data2)))
);
};
}
export {
ADD_TODO,
COMPLETE_TODO,
addTodo,
addTodo2,
complete,
complete2
}
3. 리듀서 생성: 스토어의 구조를 정한다.
src/reducer/todos.js
import todoAction from '../action/index';
const {ADD_TODO, COMPLETE_TODO} = todoAction.todo;
const todo = (state, action) => {
switch (action.type) {
case ADD_TODO:
return {
id :Math.floor(Math.random() * 100) + 1,
todo: action.text,
completed: false
};
case COMPLETE_TODO:
if (state.id !== action.id) {
return state;
}
return {
...state,
complete: !state.complete
};
default:
return state;
}
}
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state, todo(undefined, action)
];
case COMPLETE_TODO:
return state.map(t => todo(t, action));
default:
return state;
}
}
export default todos;
4. dispatch()메서드에 액션 결과 전달: 액션 결과를 전달한다. (dispatch(complete(data))
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { complete, complete2 } from '../../action/todo';
import Todo from './TODO';
class TODOList extends Component {
render() {
const {todos, onClick} = this.props;
return (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
onClick={onClick}
{...todo}
/>
)}
</ul>
);
}
}
const todolistStateToProps = (state) => {
return {
todos: state.todos
}
}
const todolistDispatchToProps = (dispatch) => {
return {
onClick(data){
// dispatch(complete2(data))
dispatch(complete(data))
}
}
}
export default connect(todolistStateToProps,todolistDispatchToProps)(TODOList);
'JavaScript > React.js' 카테고리의 다른 글
리액트 16.3 Context API 파헤치기 (0) | 2019.04.22 |
---|---|
Reselect를 이용하여 React와 Redux 최적화하기 (0) | 2019.04.17 |
HOC (Higher Order Component) (0) | 2019.04.15 |
VELOPERT 리액트 강의 (0) | 2019.04.03 |
webpack 설정 (0) | 2019.04.01 |