17-1. 프레젠테이셔널 컴포넌트
프레젠테이셔널 컨포넌트가 될 UI 컴포넌트 두 개를 준비한다
- +,-버튼으로 동작하는 Counter 컴포넌트
- 등록, 삭제, 체크가 가능한 Todos 컴포넌트
Presentatinal 코드들
import React from 'react'; import TodoItem from "./TodoItem"; const Todos = ({ input, todos, onChangeInput, onInsert, onToggle, onRemove, }) => { function onSubmit(e) { e.preventDefault(); } return ( <div> <form action="submit" onSubmit={onSubmit}> <input type="text"/> <button type={"submit"}>등록</button> </form> <div> <TodoItem/> <TodoItem/> <TodoItem/> <TodoItem/> <TodoItem/> </div> </div> ); }; export default Todos;
import React from "react"; const TodoItem = ({todo, onToggle, onRemove}) => { return ( <div> <input type="checkbox"/> <span>예제 텍스트</span> <button>삭제</button> </div> ); }; export default TodoItem;
import React from 'react'; const Counter = ({number, onIncrease, onDecrease}) => { return ( <div> <h1>{number}</h1> <div> <button onClick={onIncrease}>+1</button> <button onClick={onDecrease}>-1</button> </div> </div> ); }; export default Counter;
17-2. 리덕스 코드 작성하기
17-2-1. counter Module
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;
17-2-2. todos Module
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;
export const insert = (text)=> ({
type:INSERT,
todo: {
id: id++,
text,
done: false,
}
});
export const toggle = ()=> ({
type:TOGGLE,
id
});
export const remove = ()=> ({
type:REMOVE,
id
});
const initialState = {
input : '',
todos : [
{
id:1,
text: '리덕스 기초 배우기',
done : true,
},
{
id:2,
text: '리액트와 리덕스 사용하기',
done : false
}
]
};
function todos(state = initialState, action) {
switch (action.type) {
case CHANGE_INPUT :
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;
17-2-3. 루트 리듀서 만들기
store를 만들 때 파라미터로 들어가는 리듀서는 하나인데 현재 내가 만든 리듀서 함수는 두 개 이므로 combineReducers를 이용하여 리듀서를 하나로 합쳐준다
import counter from "./counter";
import todos from "./todos";
import {combineReducers} from "redux";
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
17-3. 리덕스 적용하기
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {createStore} from "redux";
import rootReducer from "./modules";
import {Provider} from "react-redux";
const store = createStore(
rootReducer,
// redux Dev_Tools를 이용하기 위한 코드
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
이전에 하나로 합친 리듀서를 스토어에 파라미터로 넘겨줘서 프로젝트에서 사용할 store를 만든 뒤
Provider를 이용해서 App을 감싸주면 이제 프로젝트에서 리덕스를 사용할 준비가 끝났다
17-4. 컨테이너 컴포넌트
이제 Counter 컴포넌트에 props를 줄 CounterContainer 컴포넌트를 작성해야한다
위에서 작성한 store에 저장되어있는 데이터들을 CounterContainer에서 조회하려면 어떻게 해야할까?
17-4-1. connect
react-redux에서 제공하는 connect 함수를 이용하는 방법이 있다
connect(mapStateToProps, mapDispatchToProps)(Component);
connect 함수는 위와 같이 두 개의 함수를 파라미터로 받고 연동할 컴포넌트를 파라미터로 받는 함수를 반환한다
위 코드는 아래와 같다
// store에서 state와 dispatch를 조회
const fn = connect(mapStateToProps, mapDispatchToProps);
// 위에서 조회한 store의 state와 dispatch들을 원하는 component에 연동
fn(Component);
위와 같은 코드를 작성해주고 나면 이제 Component에서 store의 state와 dispatch 조회가 가능하다
mapStateToProps
mapStateToProps는 store의 state를 파라미터로 받은 뒤 객체를 반환하는데
이 때 반환한 객체는 연동할 컴포넌트의 props로 전달된다
const mapStateToProps = (state)=> ({
number : state.counter.number
});
mapDispatchToProps
mapDispatchToProps도 비슷하게 store의 내장 함수인 dispatch를 파라미터로 받은 뒤 객체를 반환한다
마찬가지로 객체는 연동할 컴포넌트의 props로 전달된다
const mapDispatchToProps = (dispatch)=> ({
// 여기서 사용하는 increase()와 decrease()는
// import {decrease, increase} from "../modules/counter";
// modules에서 작성한 액션 객체 생성 함수이다
increase : ()=> dispatch(increase()),
decrease : ()=> dispatch(decrease()),
});
전달된 increase와 decrease는 컴포넌트 내부에서 호출될 시
- 액션 객체 생성 함수 실행
- 1에서 생성한 객체를 dipatch의 파라미터로 전달
- dispatch가 호출되어 리듀서 함수 실행
- 액션에 맞는 동작 실행
connect
위에서 connect 함수에 필요한 파라미터들을 다 작성했으니 다음과 같이 연동하면 된다
export default connect(mapStateToProps,mapDispatchToProps)(CounterContainer);
연동과 동시에 CounterContainer를 내보냄으로써 state와 dispatch가 CounterContainer의 props로 전달되었다
완성된 CounterContainer와 Counter 컴포넌트는 다음과 같다
import React from 'react';
import Counter from "../components/Counter";
import {connect} from "react-redux";
import {decrease, increase} from "../modules/counter";
const CounterContainer = ({number, decrease, increase}) => {
return (
<div>
<Counter number={number} onDecrease={decrease} onIncrease={increase}/>
</div>
);
};
const mapStateToProps = state => ({
number : state.counter.number,
})
const mapDispatchToProps = (dispatch)=> ({
increase : ()=> dispatch(increase()),
decrease : ()=> dispatch(decrease()),
})
export default connect(mapStateToProps,mapDispatchToProps)(CounterContainer);
import React from 'react';
const Counter = ({number, onIncrease, onDecrease}) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
);
};
export default Counter;
Counter는 데이터가 어떻게 처리되는지 전혀 알지 못하고 뷰만 관리하고 있고
⇒ Presentational
ContainerCounter는 뷰가 어떻게 보이는지 전혀 알지 못하며 데이터 처리와 전달을 담당하고 있으며
⇒ Container
데이터를 처리하는 로직은 redux 관련 코드들이 처리하고 있다
⇒ modules
17-5. Hooks 사용하기
이때까진 connect함수를 이용하여 store에 있는 데이터들을 조회했지만 react-redux에서 제공하는 Hook들을 사용할 수도 있다
17-5-1. useSelector
useSelector는 connect함수의 첫 번째 파라미터였던 mapStateToProps와 동일한 역할을 하고 형태 또한 같다
const CounterContainer = (decrease, increase) => {
const number = useSelector(state => state.counter.number);
return (
<div>
<Counter number={number} onDecrease={decrease} onIncrease={increase}/>
</div>
);
};
useSelector로 number를 조회했기 때문에 props로 받아오던 number는 이제 필요가 없어졌다
17-5-2. useDispatch
mapStateToProps를 대체할 Hook이 나왔으니 이번엔 mapDispatchToProps의 차례다
예상했듯이 useDispatch를 사용하면 connect를 사용하지않고 컴포넌트 내부에서 dispatch를 발생시킬 수 있다
const CounterContainer = () => {
const number = useSelector(state => state.counter.number);
const dispatch = useDispatch();
return (
<div>
<Counter number={number} onDecrease={()=>dispatch(decrease())} onIncrease={()=>dispatch(increase())}/>
</div>
);
};
'React > 리액트를 다루는 기술' 카테고리의 다른 글
21. 백엔드 프로그래밍 : Node.js-Koa (0) | 2021.02.21 |
---|---|
18. 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2021.02.19 |
16. 리덕스 라이브러리 이해하기 (0) | 2021.02.11 |
15. Context API (0) | 2021.02.10 |
14. 외부 API를 연동하여 뉴스 뷰어 만들기 (0) | 2021.02.08 |