● UI 준비하기
- 프레젠테이셔널 컴포넌트 : props를 받아와서 화면에 UI를 보여주기만 하는 컴포넌트를 말한다.
- 컨테이너 컴포넌트 : 리덕스와 연동되어있는 컴포넌트로, 리덕스로부터 상태를 받아 오기도하고 리덕스 스토어에 액션을 디스패치하기도 하는 컴포넌트를 말한다.
먼저 컴포넌트를 만들기 전에 redux와 react-redux를 설치합니다.
npm i redux
npm i react-redux
다음 카운터 컴포넌트와, 할일 컴포넌트를 만들어보겠습니다.
src 폴더 > components 생성 후 > Counter.js
Counter.js
const Counter = ({ number, onIncrease, onDecrease}) => {
return (
<div>
<h1>{number}</h1>
<div>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
</div>
)
}
먼저 카운터 함수는 props로 UI로 보여질 숫자와 이벤트 핸들러에 넣어줄 onIncrease, onDecrease함수를 받아올 것이기 때문에 넣어주었습니다.
src 폴더 > components 생성 후 > Todos.js
Todos.js
const TodoItem = ({ todo, onToggle, onRemove})=>{
return (
<div>
<input type='checkbox' />
<span>예제 텍스트</span>
<button>삭제</button>
</div>
)
}
const Todos = ({input, todos, onChangeInput, onRemove, onInsert, onToggle})=>{
const onSubmit = e =>{
e.preventDefault();
}
return (
<div>
<form onSubmit={onSubmit}>
<input type='text' />
<button type='submit'>등록</button>
</form>
<TodoItem/>
<TodoItem/>
<TodoItem/>
<TodoItem/>
<TodoItem/>
</div>
)
}
export default Todos;
먼저 TodoItem 컴포넌트는 체크박스 - 목록 - 삭제 버튼을 만들어주고,
Todos 컴포넌트에 각각의 Input, todos목록, 그리고 이벤트 핸들러들을 props로 받아오게 설정했습니다.
App.js
import Counter from './components/Counter';
import Todos from './components/Todos';
const App = ()=>{
return (
<div>
<Counter number={0}/>
<hr/>
<Todos />
</div>
)
}
export default App
그리고 앱컴포넌트에서 모두 불러 렌더링해주었습니다.
그럼 일단 아래와 같은 모습으로 렌더링됩니다.
이제 액션, 리듀서를 만들어볼까요?
● 리덕스 관련 코드 작성하기
리덕스 관련해서는 actions, constants, reducers를 각각 구분해서 폴더를 만들어 정리해주기도 하지만 대부분 modules라는 폴더 안에 리덕스 관련 코드를 작성합니다. 우리는 modules 폴더를 만들어 진행합니다.
경로 : src > modules > counter.js
counter.js
const INCREASE = 'counter/increase';
const DECREASE = 'counter/decrease';
export const increase = () => ({type : INCREASE});
export const decrease = () => ({type : DECREASE});
먼저 counter.js에서 액션명을 const 상수로 지정하고 distpatch할 때 쓰일 액션 생성함수를 만들어줍니다.
export const increase, decrease는 모두 액션 생성 함수입니다.
다음에 리듀서 함수를 만들어 줍니다.
counter.js
const INCREASE = 'counter/increase';
const DECREASE = 'counter/decrease';
export const increase = ()=> ({ type : INCREASE });
export const decrease = ()=> ({ type : DECREASE });
const initialState = {
counter : 0
};
function counter(state = initialState, action){
switch(action.type){
case INCREASE :
return { counter : state.counter + 1};
case DECREASE :
return { counter : state.counter - 1};
default :
return state;
}
}
export default counter;
다음으로는 todos.js 파일을 만들어 todos 액션 생성 함수, 리듀서를 만들어 보겠습니다.
경로: src > modules > todos.js
todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT';
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';
let id = 3;
export const changeInput = (input)=>({ type : CHANGE_INPUT, input});
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 });
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.todo)};
default :
return state;
}
}
export default todos;
● 리듀서 합쳐주기
여러개의 리듀서를 생성해주었으니, 합쳐주어야겠죵?
경로: src > modules > index.js
합쳐줄 때는 redux에서 제공해주는 combineReducers를 사용합니다.
index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos,
})
export default rootReducer;
이렇게 액션 생성 함수, 기본 값, 리듀서 파일들을 나눠서 관리하고 rootReducer에서 모아서 합쳐준다면 관리하기가 훨씬 편할 것입니다.
우리 아직 store안만들지 않았나요?
그다음에 우리는 store를 만들어야겠죠?
● 스토어 만들기
경로: src > index.js
store를 합쳐줄 때는 redux에서 제공하는 createStore을 불러줍니다. 그리고 Provider를 App컴포넌트로 감싸주면 되는데요,
이때 Provider는 react-redux에서 불러와줍니다.
context API와 비슷하지 않나요? 그때는 Provider로 감싸주고 value라는 props로 넘겨주었는데, 여기서는 store이라는 props로 넘겨주면 된답니다.
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
const store = createStore(rootReducer)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
그리고 createStore가 밑줄이 쳐지지만, 이는 지극히 정상적입니다! redux에서는 createStore보다 configureStore를 권장하고 있기 때문에 그런 것이라 일단 넘어갑시다.
자 우리는 이제 액션 생성 함수, 리듀서, store을 모두 생성했습니다.
근데 그럼 store와 view는 어떻게 연결시켜주어야할까요? 그리고 dispatch는요?
기존 redux에서는 subscribe를 통해서 상태를 가져올 수 있었죠? 그럼 react-redux는 어떻게 값을 가져올까용?
● 컨테이너 컴포넌트
책에서는 컨테이너 컴포넌트를 만들고 있지만, 사실은 그냥 컴포넌트 내에서 connect라는 함수로 상태를 가져올 수도 있습니다.
하지만 책의 내용에 맞게 컨테이너 컴포넌트를 만들어 보겠습니다.
src경로에 containers 디렉토리를 만들고 CoutnerContainer.js 파일을 생성해주었습니다.
경로 : src > containers > CounterContainer.js
먼저, react-redux에서 connect함수를 불러와 줍니다.
CounterContainer.js
import Counter from '../components/Counter';
import { connect } from 'react-redux';
const CounterContainer = ()=>{
return (
<Counter />
)
}
const mapStateToProps = ()=>{};
const mapDispatchToProps = ()=>{};
export default connect(mapDispatchToProps, mapDispatchToProps)(CounterContainer)
위와 같은 형식으로 이루어지게 되는데, connect는 첫번째, 두번째인자에 모두 콜백함수를 받습니다.
주로 이 콜백함수들의 상수로 정해주고, 변수명을 지정해주는데, mapStateToProps, mapDispatchToProps로 지정해줍니다.
이 변수명은 주로 국룰로 쓰이고 있습니다.
그 뒤, connect함수는 실행되고 (커링함수처럼) 함수를 내뱉는데, 이때 인자로 바로 연결해줄 컴포넌트를 넣어주면 완성입니다.
이제는 그럼 mapStatToProps 와 mapDispatchToProps를 설정해주어야겠죠?
얘네는 모두 객체를 내뱉어야합니다!
CounterContainer.js
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import { decrease, increase } from '../modules/counter';
const CounterContainer = ()=>{
return (
<Counter />
)
}
const mapStateToProps = (state)=> ({
number : state.counter.counter
});
const mapDispatchToProps = (dispatch)=> ({
increase : ()=>{
dispatch(increase());
},
decrease : ()=>{
dispatch(decrease());
}
});
export default connect(mapDispatchToProps, mapDispatchToProps)(CounterContainer)
number라는 이름에, state.counter.counter로 설정해주었습니다.
mapStateToProps는 state를 인자로 받아옵니다.
state는 일단 시작명이고, 두번째에 적힌 counter는 index.js 인 우리 루트 리듀서에 적은 명, 그다음 counter는 coutner.js에서 적은 이니셜스테이츠 명입니다.
mapDispatchProps는 dispatch함수를 인자로 받아옵니다. 그다음 객체를 반환해야하기때문에 객체를 열어주고,
그 안에 함수인 프로퍼티들을 적어주어야합니다. 화살표함수를 열어서 dispatch를 열어줍니다.
그다음 dispatch안에는 우리가 export 해주었던 액션객체함수들을 불러와 dispatch함수 내에서 실행해주어야합니다.
왜냐하면 dispatch에 우리가 액션객체를 넣어주었던것.. 기억하죠?
따라서 increase : ()=>{ dispatch(increase())} 와 같은 식이 되는 것입니다.
const CounterContainer = ({number, increase, decrease})=>{
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease}/>
)
}
그 다음, 이들을 props로 받아와 사용할 수 있습니다.
그런데 우리••• 저렇게 매번 dispatch를 열고 함수를 실행하는 식을 적어주어야할까요? 넘나 기찮죠?
따라서 redux에서 제공해주는 bindActionCreators라는 함수가 있습니다.
CounterContainer.js
import { bindActionCreators } from 'redux';
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import { decrease, increase } from '../modules/counter';
...
const mapStateToProps = (state)=> ({
number : state.counter.counter
});
const mapDispatchToProps = (dispatch)=> bindActionCreators({
increase,
decrease
}, dispatch);
export default connect(mapDispatchToProps, mapDispatchToProps)(CounterContainer)
그럼 위와 같이 수정할 수 있습니다. bindActionCreators가 저절로 dispatch와 연동시켜주는 것이죠.
근데 이것도 솔직히 점 귀찮지 안나염?
CounterContainer.js
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import { decrease, increase } from '../modules/counter';
const CounterContainer = ({number, increase, decrease})=>{
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease}/>
)
}
const mapStateToProps = (state)=> ({
number : state.counter.counter
});
export default connect(mapDispatchToProps, { increase, decrease })(CounterContainer)
사실 connect함수의 두번째 인자는 객체로도 넣어줄 수 있어서 위와 같이 수정될 수 있습니다.. 넘나 편한것~
그리고 아래로도 수정될 수도 있습니다.
CounterContainer.js
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import { decrease, increase } from '../modules/counter';
const CounterContainer = ({number, increase, decrease})=>{
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease}/>
)
}
export default connect((state)=>({
number : state.counter.counter
}), { increase, decrease })(CounterContainer)
● TodosContainer 만들기
경로 : src > containers > TodosContainer.js
TodosContainer.js
import { connect } from 'react-redux';
import { changeInput, insert, remove, toggle } from '../modules/todos';
import Todos from '../components/Todos';
const TodosContainer = ({ input, todos, changeInput, insert, remove, toggle})=>{
return <Todos
input={input}
todos={toods}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove}/>
}
export default connect(({todos})=>({
input : todos.input,
todos : todos.todos
}),{ changeInput, insert, remove, toggle})(TodosContainer);
그다음 App 컴포넌트에서 컨테이너 컴포넌트들을 렌더링 해줍니다.
App.js
import CounterContainer from './containers/CounterContainer'
import TodosContainer from './containers/TodosContainer'
const App = ()=>{
return (
<div>
<CounterContainer />
<hr/>
<TodosContainer />
</div>
)
}
export default App
다음 Todos 컴포넌트로 돌아와, 상태로 받아온 dispatch와 state들을 사용해 수정해줍니다.
Todos.js
const TodoItem = ({ todo, onToggle, onRemove})=>{
return (
<div>
<input type='checkbox' onClick={()=>{ onToggle(todo.id)}} checked={todo.done} readOnly={true}/>
<span>{todo.text}</span>
<button onClick={()=>{onRemove(todo.id)}}>삭제</button>
</div>
)
}
const Todos = ({input, todos, onChangeInput, onRemove, onInsert, onToggle})=>{
const onSubmit = e =>{
e.preventDefault();
onInsert(input);
onChangeInput('');
};
const onChange = e => onChangeInput(e.target.value);
return (
<div>
<form onSubmit={onSubmit}>
<input type='text' value={input} onChange={onChange}/>
<button type='submit'>등록</button>
</form>
<div>
{
todos.map(todo => <TodoItem key={todo.id} todo={todo} onToggle={onToggle} onRemove={onRemove}/>)
}
</div>
</div>
)
}
export default Todos;