KDT/TIL
10/7 TIL : Redux, 간단한 Todo-list 만들기
ebulsok
2022. 10. 14. 15:41
🔎 Redux
- 상태 관리 라이브러리
- 컴포넌트의 상태를 하나하나 props로 전달하는 것을 방지하고자 생김
- 컴포넌트의 상태를 store.js 에서 관리
1. dispatch 함수를 실행하면
2. action 발생
3. action을 reducer가 받아서
4. state를 변경
5. state가 변경되면 컴포넌트가 리렌더링
- [터미널] npm i redux
- [터미널] npm i react-redux
- redux 적용을 위해서 <Provider> 컴포넌트를 import하고 <App>을 감싸줘야 함
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider>
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
🚩 store 만들기
- redux에서 createStore를 임포트 한 뒤 store를 만들고, <Provider>의 store 속성에 부여하기
- state용 변수 만들기
- reducer(간단하게 state를 전달만 하는) 만들기
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
const weight = 100;
function reducer(state = weight) {
return state;
}
let store = createStore(reducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
🚩 store에 저장된 값 받아오기
- react-redux 모듈의 useSelector 사용
// src/components/Test.js
import { useDispatch, useSelector } from "react-redux"
export default function Test() {
const weight = useSelector((state) => state);
return (
<>
<h1>당신의 몸무게는 {weight}</h1>
</>
)
}
🚩 action 설정
- action은 reducer에게 어떤 처리를 해야하는지 알려주는 역할
- 객체 내부에 type 이라는 키를 문자열 형태로 가지고 있음
// src/index.js
function reducer(state = weight, action) {
if(action.type === "inc") ++state;
else if(action.type === 'dec') --state;
return state;
}
🚩 dispatch로 action 보내기
- useDispatch()를 하나의 변수에 담고 해당 변수를 통해 컴포넌트의 action 값을 reduer로 전달
// src/component/Test.js
import { useDispatch, useSelector } from "react-redux"
export default function Test() {
const weight = useSelector((state) => state);
const dispatch = useDispatch();
return (
<>
<h1>당신의 몸무게는 {weight}</h1>
<button onClick={() => { dispatch({ type: "inc" })}}>살 찌우기</button>
<button onClick={() => { dispatch({ type: "dec" })}}>살 빼기</button>
</>
)
}
🔎 React TodoList 만들기
- /src/store 폴더 생성, index.js 파일 추가(store 전체를 총괄하는 모듈)
- store 모듈 분할: store/modules 폴더 생성, 투두리스트를 관리하는 모듈인 todo.js 파일 추가
- 초기 state 값 선언: id, text, done
- 설정한 state 값을 바로 return 시키는 reducer 작성
// src/store/modules/todo.js
// 초기 값 설정
const initState = {
list: [
{
id: 0,
text: "리액트 공부하기",
done: false,
},
{
id: 1,
text: "물 마시기",
done: false,
},
{
id: 2,
text: "취준 하기",
done: false,
}
]
}
// reducer
export default function todo(state = initState, action) {
return state;
}
🚩 store 통합 관리
- 초기 값을 선언한 todo.js를 import 해서 todo.js의 reducer를 불러오고, redux의 combineReducer를 이용하여 todo.js의 reducer를 하나로 합쳐서 다시 내보내기
// src/store/index.js
// 통합 관리 파일
import { combineReducers } from "redux";
import todo from "./modules/todo";
export default combineReducers({
todo,
})
🚩 redux 기초 세팅
- combineReducer를 통해 하나로 합쳐서 내보낸 reducer를 rootReducer 라는 값으로 받기
// src/index.js
import rootReducer from './store';
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
🚩 <TodoList>, <DoneList>, <ListContainer> 컴포넌트 제작
// src/components/TodoList.js
import { useRef } from 'react'
import { useSelector } from 'react-redux'
export default function TodoList() {
const list = useSelector((state) => state.todo.list);
const inputRef = useRef();
return (
<section>
<h1>할 일 목록</h1>
<div>
<input type="text" ref={inputRef} />
<button>추가</button>
</div>
<ul>
{list.map((el) => {
return (
<li key={el.id}>
{el.text}
</li>
)
})}
</ul>
</section>
)
}
// src/components/DoneList.js
import { useSelector } from 'react-redux'
export default function DoneList() {
const list = useSelector((state) => state.todo.list);
return (
<section>
<h1>완료된 목록</h1>
<ul>
{list.map((el) => {
return (
<li key={el.id}>
{el.text}
</li>
)
})}
</ul>
</section>
)
}
// src/components/ListContainer.js
import DoneList from "./DoneList";
import TodoList from "./TodoList";
export default function ListComponent() {
return (
<>
<TodoList />
<DoneList />
</>
)
}
🚩 action 타입 정의, action 생성 함수, reducer 구조 구현
- action 생성 함수는 type 정보와 전달해야 할 정보를 payload 객체에 담아서 dispatch를 통해 전달
- 결과적으로 reducer가 action 함수에 들어있는 type을 확인해서 어떤 행동을 할지 정하고, payload에 있는 데이터를 받아서 처리
// src/store/modules/todo.js
// 액션 타입 정의
const CREATE = "todo/CREATE";
const DONE = "todo/DONE";
// 액션 함수
export function create(payload) {
return {
type: CREATE,
payload,
}
}
export function done(id) {
return {
type: DONE,
id,
}
}
// reducer
export default function todo(state = initState, action) {
switch(action.type) {
case CREATE:
return console.log("CREATE 호출");
case DONE:
return console.log("DONE 호출");
default:
return state;
}
}
🚩 dispatch로 action 함수 전달
- dispatch 활용을 위해 useDispatch를 변수에 넣어주기
- todo.js에서 create, done 함수 불러오기
- dispatch의 인자로 create, done 함수를 전달하여 호출 상태 확인
// src/components/TodoList.js
import { useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { create, done } from '../store/modules/todo';
export default function TodoList() {
const list = useSelector((state) => state.todo.list);
const inputRef = useRef();
const dispatch = useDispatch();
return (
<section>
<h1>할 일 목록</h1>
<div>
<input type="text" ref={inputRef} />
<button onClick={() => { dispatch(create('') }>추가</button>
</div>
<ul>
{list.map((el) => {
return (
<li key={el.id}>
{el.text}
</li>
)
})}
</ul>
</section>
)
}
🚩 reducer CREATE/DONE 구현
- 혹시 모를 다른 초기 값이 있을 지 모르므로 state를 전개 연산자로 먼저 리턴
- CREATE: List의 경우 새롭게 입력 받은 값을 list의 배열에 넣어주기
- DONE: List의 경우 컴포넌트에서 전달 받은 id 값과 동일한 객체를 찾은 다음 해당 객체의 done 항목을 true로 변경
// src/store/modules/todo.js
export default function todo(state = initState, action) {
switch(action.type) {
case CREATE:
return {
...state,
list: state.list.concat({
id: action.payload.id,
text: action.payload.text,
done: false,
})
}
case DONE:
return {
...state,
list: state.list.map((el) => {
if(el.id === action.id) return {
...el,
done: true,
}; else return el;
})
}
default:
return state;
}
}
🚩 dispatch로 CREATE/DONE 호출
// src/components/TodoList.js
return (
<section>
<h1>할 일 목록</h1>
<div>
<input type="text" ref={inputRef} />
<button onClick={() => {
dispatch(create({ id: list.length, text: inputRef.current.value }));
inputRef.current.value = "";
}}>추가</button>
</div>
<ul>
{list.map((el) => {
return (
<li key={el.id}>
{el.text}
<button onClick={() => { dispatch(done(el.id)) }}>완료</button>
</li>
)
})}
</ul>
</section>
)
🚩 각각의 컴포넌트에 filter 처리
// src/components/TodoList.js
export default function TodoList() {
const list = useSelector((state) => state.todo.list).filter(
(el) => el.done === false
);
// ...
}
// src/components/DoneList.js
export default function DoneList() {
const list = useSelector((state) => state.todo.list).filter(
(el) => el.done === true
);
// ...
}
🚩 List 요소의 key 값이 고유하도록 해당 순번도 store에서 전역으로 관리하여 처리
// src/store/modules/todo.js
let counts = initState.list.length;
initState['nextID'] = counts;
export default function todo(state = initState, action) {
switch(action.type) {
case CREATE:
return {
...state,
list: state.list.concat({
id: action.payload.id,
text: action.payload.text,
done: false,
}),
nextID: action.payload.id + 1,
}
case DONE:
// ...
}
// src/components/TodoList.js
const nextID = useSelector((state) => state.todo.nextID);
return (
<section>
<h1>할 일 목록</h1>
<div>
<input type="text" ref={inputRef} />
<button onClick={() => {
dispatch(create({ id: nextID, text: inputRef.current.value }));
inputRef.current.value = "";
}}>추가</button>
</div>
// ...
)