Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 17
- React + Redux
모듈
로 따로 분리하여 별개의 파일로 관리할 수 있음.이전 포스팅에서 vanila JS 환경에서 리덕스를 사용할 때에는 store의 내장 함수인 store.dispatch
, store.subscribe
를 사용했음.
react-redux
라는 라이브러리의 유틸 함수 및 컴포넌트를 사용함.$ yarn add redux react-redux
리액트 환경에서 리덕스를 사용할 때 가장 많이 사용하는 패턴은
프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것.
프레젠테이셔널 컴포넌트 | 컨테이너 컴포넌트 |
---|---|
state 관리 X. 그저 props를 받아 화면에 UI를 보여주기만 함 | 리덕스와 연동. 리덕스로부터 state를 받아오기도 하고, store에 액션을 디스패치 하기도 함. |
-> 이러한 패턴을 사용하면 코드 재사용성도 높아지고, UI 작성에 더 집중할 수 있다.
UI 관련 프레젠테이셔널 컴포넌트는 src/components
에 저장하고,
Redux에 연동된 컨테이너 컴포넌트는 src/containers
컴포넌트에 작성함.
src/components/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>
)
}
export default Counter;
App.js
import Counter from "./components/Counter";
function App() {
return (
<div>
<Counter number={0} />
</div>
);
}
export default App;
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.preventDefalt();
};
return (
<div>
<form onSubmit={onSubmit}>
<input />
<button type="submit">등록</button>
</form>
<div>
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
<TodoItem />
</div>
</div>
);
};
export default Todos;
import Counter from "./components/Counter";
import Todos from "./components/Todos";
function App() {
return (
<div>
<Counter number={0} />
<hr />
<Todos />
</div>
);
}
export default App;
actions
, constants
, reducers
세 개의 디렉터리를 만들고 그 안에 기능별로 파일을 생성.🐥 Ducks 패턴
- 아래와 같이 액션타입 / 액션 생성함수 / 리듀서 함수를 기능별로 파일 하나로 몰아서 작성함.
- 둘중 아무 패턴이나 사용해도 됨. (상관 X)
// 액션 타입
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
액션 타입은 대문자로 정의하고, 내용은 '모듈이름/액션이름'의 형태로 작성함.
-> 다른 모듈과 액션이름이 중복되지 않도록.
액션 생성함수는 export
해줌.
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
// 🔻 추가된 부분
const initialState = {
number: 0
};
const 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;
export default
키워드로 함수를 export함.✅ export vs. export default
- export는 여러개를 내보낼 수 있지만
- export default는 한개만 내보낼 수 있음.
- import시에는
import counter
로 한번에 불러올 수 있고,
increase, decrease는import {increase, decrease}
로 불러와야함.
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 = id => ({
type: TOGGLE,
id
});
export const remove = id => ({
type: REMOVE,
id
})
id는 전역변수 id로 관리해서 todo의 id로 사용하도록 함.
-> insert 함수가 호출될 때 마다 1씩 증가하도록. (id++)
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 = 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
},
]
};
const 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;
modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({
counter,
todos,
});
export default rootReducer;
redux 라이브러리의
combineReducers
함수를 이용하여 두개의 모듈을 합침.
->createStore
을 통해 스토어를 만들 때 하나의 리듀서를 사용해야 하기 때문.
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import App from './App';
import './index.css'
import rootReducer from './modules';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
❗️ 루트 리듀서의 파일명을
index.js
로 해주면, import시 폴더명까지만 적어도 된다!import rootReducer from './modules';
react-redux
의 Provider 컴포넌트를 임포트함.store
을 props로 전달해주어야 함.import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import './index.css'
import rootReducer from './modules';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
🙋♀️ 참고 - 이제는 createStore() 이 아닌 react-redux의
configureStore({})
을 이용하자.
const store = createStore(
rootReducer, /* preloadedState */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
$ yarn add redux-devtools-extension
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import App from './App';
import './index.css'
import rootReducer from './modules';
const store = createStore(rootReducer, composeWithDevTools);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
🔻 참고로, 이렇게 차트형 or Raw(객체형식)으로도 볼 수 있다.
컨테이너 컴포넌트
라고 한다.src/containers/CounterContainer.js
import Counter from "../components/Counter";
const CounterContainer = () => {
return <Counter />;
};
export default CounterContainer;
위 CounterContainer 컴포넌트를 리덕스와 연동하기 위해서는 react-redux 라이브러리의 connect
함수를 이용해야함.
ex>
connect(mapStateToProps, mapDispatchToPRops)(CounterContainer);
CounterContainer.js (수정)
import Counter from "../components/Counter";
import { connect } from "react-redux";
const CounterContainer = ({ number, increase, decrease }) => {
return (
<Counter number={number} onIncrease={increase} onDecrease={decrease} />
);
};
const mapStateToProps = state => ({
number: state.counter.number,
});
const mapDispatchToProps = dispatch => ({
increase: () => {
console.log('increase');
},
decrease: () => {
console.log('decrease');
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
mapStateToProps와 mapDispatchToProps에서 리턴하는 객체 내부의 값은 컴포넌트의 props로 전달됨.
store.dispatch()
함수를 파라미터로 받아옴.App.js (수정)
import CounterContainer from "./containers/CounterContainer";
import Todos from "./components/Todos";
function App() {
return (
<div>
<CounterContainer />
<hr />
<Todos />
</div>
);
}
export default App;
-> Counter 대신 CounterContainer 컴포넌트 렌더링.
CounterContainer.js
import Counter from "../components/Counter";
import { connect } from "react-redux";
import { increase, decrease } from "../modules/counter";
...
const mapDispatchToProps = dispatch => ({
increase: () => {
dispatch(increase());
},
decrease: () => {
dispatch(decrease());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterContainer);
이제 +1, -1 버튼을 클릭하면 카운터가 변경된다.
Redux DevTool을 보면 아래와 같이 액션명이 나온다.
connect
함수 사용시, 일반적으로 위와 같이 mapStateToProps, mapDispatchToProps를 미리 정의해놓고 사용한다.connect
안에 익명함수 형식으로 선언해도 괜찮다.
export default connect(state => ({
number: state.counter.number,
}), dispatch => ({
increase: () => dispatch(increase()),
decrease: () => dispatch(decrease()),
})
)(CounterContainer);
컴포넌트에서 액션을 dispatch로 감싸는 작업이 번거로움.
액션 생성 함수가 많아질수록 더 번거로워짐.
redux 라이브러리에서 제공하는 유틸함수인 bindActionCreator
을 사용해주면 됨.
CreateContainer.js
...
export default connect(state => ({
number: state.counter.number,
}),
dispatch =>
bindActionCreators(
{
increase,
decrease,
},
dispatch,
)
)(CounterContainer);
dispatch => bindActionCreators({increase,decrease}, dispatch})()
해줌.
-> 따로 Action을 불러오고 dispatch로 감싸줄 필요가 없어짐.
mapDispatchToProps()
의 인자에 액션 생성함수로 이루어진 객체를 넣어줌....
export default connect(state => ({
number: state.counter.number,
}),
{
increase, decrease
}
)(CounterContainer);
import { connect } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/Todos";
const TodoContainer = ({ input, todos, changeInput, insert, toggle, remove }) => {
return (
<Todos
input={input}
todos={todos}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove}
/>
);
};
export default connect(
// state.todos 가져옴
({ todos }) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove
},
)(TodoContainer);
아까 connect의 두번째 인자인 mapDispatchToProps
함수의 인자로 state가 갔었는데,
이 함수는 state를(=store가 가지고 있는 값) 인자로 가짐.
여기서도 마찬가지로 state를 가지지만, state.todos를 {todos}로 구조분해 할당해줌.
import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";
function App() {
return (
<div>
<CounterContainer />
<hr />
<TodosContainer />
</div>
);
}
export default App;
components/Todos.js
const TodoItem = ({ todo, onToggle, onRemove }) => {
return (
<div>
<input
type="checkbox"
onClick={() => onToggle(todo.id)}
checked={todo.done}
readOnly={true}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onRemove(todo.id)}>삭제</button>
</div>
);
};
const Todos = ({
input,
todos,
onChangeInput,
onInsert,
onToggle,
onRemove
}) => {
const onSubmit = e => {
e.preventDefalt();
onInsert(input);
onChangeInput('');
};
const onChange = e => onChangeInput(e.target.value);
return (
<div>
<form onSubmit={onSubmit}>
<input value={input} onChange={onChange} />
<button type="submit">등록</button>
</form>
<div>
{todos.map(todo => (
<TodoItem
todo={todo}
key={todo.id}
onToggle={onToggle}
onRemove={onRemove}
/>
))}
</div>
</div>
);
};
export default Todos;