React - 6장 : 컴포넌트 반복
React - 7장 : 컴포넌트의 LifeCycle
React - 8장 : React Hooks 총정리
React - 12장 : immer를 사용하여 더 쉽게 불변성 유지하기
React - 13장 : 리액트 라우터로 SPA 개발하기
React - 17장 : 리덕스 실습(현재)
리덕스(Redux) 를 쓰는 이유 ? :
- 리덕스는 상태관리 라이브러리이다.
- 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 별개로 관리가 가능하므로 유지보수가 좋다
- 여러 컴포넌트에서 동일한 상태를 공유해야 할 경우 유용하다.
- 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최적화가 가능하다.
17-1. 작업환경 설정
- 터미널에 npx create-react-app react-redux-tutorial (CRA로 프로젝트 만들기)
- 터미널에 cd react-redux-tutorial
- 터미널에 npm i redux react-redux
17-2. UI 준비하기
- 리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다.
- 프레젠테이셔널 컴포넌트 : 상태관리가 주로 없고, props를 받아와서 UI 표시 정도만 하는 컴포넌트
- 컨테이너 컴포넌트 : 리덕스와 연동된 컴포넌트
- 예제에서는 프레젠테이셔널은 src/components에 저장
- 컨테이너는 src/containers에 저장한다.
17.2.1 카운터 컴포넌트 만들기
- 숫자를 더하고 뺄 수 있는 카운터 컴포넌트를 만들어보자.
src/components/Counter.jsx
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에서 렌더링해보자
App.js
import Counter from './components/Counter'
import './App.css';
function App() {
return (
<div className="App">
<Counter number={0}></Counter>
</div>
);
}
export default App;
- 터미널에서 npm start 해서 확인한다
17.2.2 할 일 목록 만들기
- 해야 할일을 추가하고, 체크하고, 삭제할 수 있는 누구나 다하는 투두컴포넌트를 만들어본다.
/src/components/Todos.jsx
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 type="text" />
<button type="submit">등록</button>
</form>
<div>
<TodoItem></TodoItem>
<TodoItem></TodoItem>
<TodoItem></TodoItem>
<TodoItem></TodoItem>
<TodoItem></TodoItem>
</div>
</div>
)
}
export default Todos;
- 받아오는 props는 나중에 사용하는 걸로 해둔다.
App.js에 렌더링 해보자.
import Counter from './components/Counter'
import Todos from './components/Todos';
import './App.css';
function App() {
return (
<div className="App">
<Counter number={0}></Counter>
<hr />
<Todos></Todos>
</div>
);
}
export default App;
지금까지 결과물
17-3. 리덕스 세팅하기
구조1 )
- actions, constants, reducers 폴더에 기능별로 파일을 하나씩 만드는 방식
- 코드를 종류에 따라 분류, 정리가 가능하지만 세 종류의 파일을 모두 수정하면서 왔다리갔다리 해야한다
- 리덕스의 공식문서에서도 추천하지만, 불편하다.
구조2)
- 액션 타입, 액션 생성, 리듀서 함수를 기능별로 파일 하나에 다 몰아서 작성하는 방식
- Ducks 패턴이라고 부른다.
위의 두가지 구조로 주로 사용되며, 딱히 정해진 건 없고 회사에서 정해준대로 하자.
17-3.1 counter 모듈 작성하기
17.3.1.1 액션 타입 정의
/modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
- 액션 타입은 대문자로 정의, 문자열 내용은 '모듈이름/액션이름' 으로 작성해준다. (겹침 방지)
17.3.1.2 액션 생성 함수 만들기
/modules/counter.js
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
export const increase = ()=> ({ type: INCREASE });
export const decrease = ()=> ({ type: DECREASE });
- export 키워드로 다른곳에서 사용할 수 있도록 한다.
17.3.1.3 초기 상태 및 리듀서 함수 만들기
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 ;
- 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성해준다.
- cf) export 키워드는 여러개 내보낼 수 있다. export default는 하나만 내보낼 수 있다
- cf2) export 키워드는 import { increase, decrease } from './counter.js' 이렇게
- cf3) export default 키워드는 import counter from './counter.js' 이렇게 한다.
17-3.2 todos 모듈 만들기
17.3.2.1 액션 타입 정의하기
modules/todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo 체크 토글
const REMOVE = 'todos/REMOVE'; // todo삭제
17.3.2.1 액션 생성 함수 만들기
- 이번엔 파라미터가 필요하다.
moduels/todos.js
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo 체크 토글
const REMOVE = 'todos/REMOVE'; // todo삭제
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
})
- insert 함수는 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id라는 값에도 의존한다.
- id값이 3인 이유는 다음번에 초기 상태를 작서할 때 todo 객체 두개를 사전에 미리 넣어둘 것이므로 3이다.
17.3.2.3 초기 상태 및 리듀서 함수 만들기
- 불변성 유지를 위해 spread 연산자를 잘 활용한다.
const CHANGE_INPUT = 'todos/CHANGE_INPUT'; // 인풋값 변경
const INSERT = 'todos/INSERT'; // 새로운 todo 등록
const TOGGLE = 'todos/TOGGLE'; // todo 체크 토글
const REMOVE = 'todos/REMOVE'; // todo삭제
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
})
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, doneL: !todo.done} : todo
)
}
case REMOVE :
return {
...state,
todos: state.todos.filter(todo =>
todo.id !== action.id
)
}
default :
return state;
}
}
export default todos;
17-3.3 루트 리듀서 만들기
- 나중에 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;
17-4. 리덕스 적용하기
- 스토어를 만들고 리액트에 적용한다.
/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import {createStore} from 'redux';
import App from './App';
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>
);
17-5. 컨테이너 컴포넌트 만들기
17-5.1 CounterContainer 만들기
/containers/CounterContainer.jsx
import Counter from "../components/Counter";
const CounterContainer = ()=>{
return <Counter/> ;
}
export default CounterContainer;
- 위 컴포넌트를 리덕스와 연동하려면 react-redux에서 제공하는 connect 함수를 사용해야 한다.
- 사용법 : connect(mapStateToProps, mapDispatchToProps)(연동할 컴포넌트)
- mapStateToProps는 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
- mapDispatchToProp는 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
- connect함수를 호출하면 또 다른 함수를 뱉는다. 이 함수에 컴포넌트를 파라미터로 넣으면 된다
결론은 이렇다
const makeContainer = connect(mapStateToProps, mapDispatchToProps)
makeContainer(타겟 컴포넌트)
/containers/CounterContainer.jsx
import Counter from "../components/Counter";
import {connect} from 'react-redux';
import { increase, decrease } from "../modules/counter";
const CounterContainer = ({number, increase, decrease})=>{
return <Counter number={number} onIncrease={increase} onDecrease={decrease}/> ;
}
const mapStateToProps = state => ({
number: state.counter.number,
})
const mapDispatchToProps = dispatch => ({
increase : () => {
dispatch(increase());
},
decrease : () =>{
dispatch(decrease());
}
})
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CounterContainer);
이제 App.js 에서 렌더링해준다
import CounterContainer from './containers/CounterContainer';
import Todos from './components/Todos';
function App() {
return (
<div className="App">
<CounterContainer></CounterContainer>
<hr />
<Todos></Todos>
</div>
);
}
export default App;
17-5.2 TodosContainer 만들기
containers/TodosContainer.jsx
import { connect } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/Todos";
const TodosContainer = ({input, todos, changeInput, insert, toggle, remove}) => {
return (
<Todos
input={input}
todos={todos}
onChangeInput={changeInput}
onInsert={insert}
onToggle={toggle}
onRemove={remove}
/>
)
};
export default connect(
({todos}) => ({
input: todos.input,
todos: todos.todos,
}),
{
changeInput,
insert,
toggle,
remove
},
)(TodosContainer);
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.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 todo={todo} key={todo.id} onToggle={onToggle} onRemove={onRemove}></TodoItem>
))
}
</div>
</div>
)
}
export default Todos;
App.js
import CounterContainer from './containers/CounterContainer';
import TodosContainer from './containers/TodosContainer';
function App() {
return (
<div className="App">
<CounterContainer></CounterContainer>
<hr />
<TodosContainer></TodosContainer>
</div>
);
}
export default App;
- 작업 끝!
- 하다보니 느낀 건 따라치기는 별 도움이 안된다..
- 이번 포스팅처럼 옛날 문법은 대충 알아두는 것으로만 하고, 이제 다음부터는 쉬운 mz세대들의 리덕스를 알아보자.
- 다음 편에 추가로 문법이 훨씬 쉬운 redux/toolkit 과 또한 관련된 미들웨어 세팅을 포스팅해야겠다.
'Frontend > ReactJS(완)' 카테고리의 다른 글
React - 19장 : 코드 스플리팅(완) (0) | 2023.03.02 |
---|---|
React - 18장 : 리덕스 미들웨어를 통한 비동기 작업 관리 (0) | 2023.03.01 |
React - 16장 : 리덕스(Redux) 라이브러리의 이해 (0) | 2023.02.28 |
추가) React에서의 PWA (0) | 2023.02.24 |
추가) React State 관리 성능개선과 Hooks (0) | 2023.02.24 |
댓글