리액트 프로젝트에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다.
프레젠테이셔널 컴포넌트란 주로 상태 관리가 이루어지지 않고, props를 받아와서 화면에 UI를 보여주기만 하는 컴포넌트를 말한다. 컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아오기도 하고 리덕스 스토어에 액션을 디스패치하기도 한다.
리덕스를 사용할 때 꼭 이 패턴을 적용해야 하는 것은 아니다. 다만 패턴을 사용하면 코드의 재사용성도 높아지고, 관심사가 분리되어 UI를 작성할 때 더 집중할 수 있다.
// components/Counter.js
import React from 'react';
const Counter = ({number, onIncrease, onDecrease}) => {
return (
<div>
<hi>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
// App.js
import React from 'react';
import Counter from './components/Counter';
const App = () => {
return (
<div>
<Counter number={0} />
</div>
);
};
export default App;
// components/Todo.js
import React from 'react';
const TodoItem = ({todo, onToggle, onRemove}) => {
return(
<div>
<input type="checkbox" />
<span>예제 텍스트</span>
<button>삭제</button>
</div>
);
};
const Todos = ({
input, // 인풋에 입력되는 텍스트
todos, // 할 일 목록이 들어있는 객체
onChangeInput,
onInsert,
onToggle,
onRemove,
}) => {
const onSubmit = e => {
e.preventDefault();
};
return (
<div>
<form onSubmit={onSubmit}>
<input />
<button type="submit">등록</button>
</form>
<div>
<TodoItem/>
<TodoItem/>
</div>
</div>
);
};
export default Todos;
Todos 컴포넌트와 TodoItem 컴포넌트를 한 파일에 정의했는데, 취향에 따라 분리해도 상관없다.
//App.js
import React from 'react';
import Counter from './components/Counter';
import Todos from './components/Todos';
const App = () => {
return (
<div>
<Counter number={0} />
<hr />
<Todos />
</div>
);
};
export default App;
이 프로젝틍서는 리덕스를 사용할 것이다. 리덕스를 사용할 떄는 액션 타입, 액션 생성 함수, 리듀서 함수 코드를 작성해야 한다.
// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
가장 먼저 해야할 작업은 액션 타입을 정의하는 것이다. 액션 타입은 대문자로 정의하고, 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성한다. 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해준다.
// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = () => ({type: INCREASE});
export const decrease = () => ({type: DECREASE});
// modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = () => ({type: INCREASE});
export const decrease = () => ({type: DECREASE});
const initialState= {
number: 0
};
function counter(state=initialState, action){
switch(action.type){
case INCREASE:
return {
number: state.number + 1
};
case DECREASE:
return {
number: state.number -1
};
default:
return state;
}
}
export default counter;
이 모듈으 초기 상태는 number 값을 설정해줬고, 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성했다.
// modules/todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
// modules/todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
export const changeInput = input => ({
type: CHANGE_INPUT,
input
});
let id = 3; //insert가 호출될 때마다 1씩 더해진다.
export const insert = text => ({
type: INSERT,
todo: {
id: id++,
text,
done: false
}
});
export const toggle = id => ({
type:TOGGLE,
id
});
export const remove = id => ({
type: REMOVE,
id
});
// modules/todos.js
(...)
const initialState = {
input: '',
todos: [
{
id: 1,
text: '리덕스 기초 배우기',
done: true
},
{
id: 2,
text: '리액트와 리덕스 사용하기',
done: false
}
}
};
function todos(state=initialState, action){
switch(action.type){
case CHANGE_INSERT:
return {
...state,
input: action.input
};
case INSERT:
return {
...state,
todos;
state.todos.concat(action.todo)
};
case TOGGLE:
return {
...state,
todos: state.todos.map(todo=> todo.id === action.id? {...todo, done: !todo.done} : todo)
};
case REMOVE:
return {
...state,
todos: state.todos.filter(todo=>todo.id !== action.id)
};
default:
return state;
}
}
export default todos;
counter, todos 리듀서 함수를 만들었다. 나중에 createStore 함수를 사용하여 ㅅ토어를 만들 때는 리듀서를 하나만 사용해야 한다. 기존에 만들었던 리듀서 함수를 하나로 합쳐야 한다. 이는 리덕스에서 제공하는 combineReducers라는 유틸함수를 사용하면 쉽게 처리가능 하다.
// modules/index.js
import {combineReducers} from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
파일 이름을 이렇게 index.js로 설정해주면 나중에 불러올 때 디렉토리 이름까지만 입력해도 된다. index라는 파일 이름 없이 아래처럼 말이다.
import rootReducer from './modules';
리액트 어플리케이션에 리덕스를 적용할 차례다. 이 작업은 index.js에서 이뤄진다.
가장 먼저 스토어를 생성한다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
const store=createStore(rootReducer);
ReactDOM.render(<App/>, document.getElementById('root'));
리액트 컴포넌트에서 스토어를 사용할 수 있도록 App 컴포넌트를 react-redux에서 제공하는 Provider 컴포넌트로 감싸준다. 이 컴포넌트를 사용할 때는 store를 props로 전달해줘야 한다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root'),
);
Redux DevTools라는 리덕스 개발자 도구(크롬 확장 프로그램)를 설치한다.이후 `$ yarn add redux-devtools-extension'을 해준다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import './index.css';
import App from './App';
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root'),
);
이것을 적용하면 리덕스 스토어 내부 상태를 볼 수 있다.