-
Redux 이용해서 TodoApp 만들기React 2022. 12. 20. 22:32
나중에 보고 또 참고할 수 있도록 단계별로 정리를 하려고 한다.
1. Router 만들기
- yarn add react-router-dom 설치
- src폴더 안에 pages, shared 폴더 생성
- pages 폴더 안에 Main.jsx, Detail.jsx 파일 생성
- shared 폴더 안에 Router.js 파일 생성
코드 작성
import { BrowserRouter, Routes, Route } from 'react-router-dom' import Main from '../pages/Main'; import Detail from '../pages/Detail'; const Router = () => { return ( <BrowserRouter> <Routes> <Route path='/' element={<Main />}></Route> <Route path='/:id' element={<Detail />}></Route> {/*뒤에 어떤 값이 오더라도 id로 가지고 있을 수 있다.*/} </Routes> </BrowserRouter> ); } export default Router;
2. Redux 설치
- yarn add redux react-redux 설치
3. Redux-configStore 만들기
- src폴더 안에 redux 폴더 생성
- redux 폴더 안에 components, config, modules 3개의 폴더 생성
- components : 컴포넌트들을 모아놓을 공간
- config : 여러 store를 모아 외부 컴포넌트와 연결할 것들을 모아놓을 공간. store는 state를 관리하는 일종의 보관처.
- modules : 리듀서를 리턴하는 많은 모듈들을 모아놓을 공간.
모듈들이 합해져서 스토어를 이루고, 스토어를 이루는 모듈들 안에는 여러가지 state를 관리할 수 있는 설정들이 들어가있다.
- config 폴더 안에 configStore.js 파일 생성
기본적인 configStore.js의 흐름
- rootReducer를 만들고
- rootReducer를 이용하여 store를 만들 것이다.(main)
- export 해서 다른 파일에서 import 할 수 있도록 한다.
기본적인 todo를 만들기 위해 필요한 configStore.js의 코드이다. 아래 코드를 해석해보겠다.
import {combineReducers, createStore} from 'redux' const rootReducer = combineReducers({ // todos, <- 다음 단계에서 작성할거임 }) const store = createStore(rootReducer); export default store;
const rootReducer = combineReducers({ todos, })
- 모듈스 밑에 있는 파일들은 모듈스를 export 하는 것들인데, rootReducer 안에 들어가있는 것들은 리듀서 이다. (todos)
- 리듀서들을 combine해서(합쳐서) rootReducer 를 만들고,
const store = createStore(rootReducer);
- rootReducer를 인자로 가지는 createStore 라는 메서드를 이용해서 Store를 만들고,
export default store;
- store를 export 한다.
4. Redux-modules 만들기
- modules 폴더 안에 todos.js 파일 생성
modules에서 만들 것들 정리
- action items
휴먼에러가 없게 type을 상수로 지정해준다. - action creators
액션객체가 다른컴포넌트의 dispatch 안으로 들어가서 액션객체 안에 있는 type에 맞는걸 4번 리듀서에서 찾음. - initial State => reducer 구성할 때 사용
- reducer를 만들 것
들어온 payload에 맞는 case문을 찾으면 return값을 실행한다. 이렇게 해서 state가 변경된다. - reducer를 export 할 것
import {toHaveDescription} from '@testing-library/jest-dom/dist/matchers'; import {v4 as uuidv4} from 'uuid'; // 1. action items const ADD_TODO = 'ADD_TODO'; const REMOVE_TODO = 'REMOVE_TODO'; const SWITCH_TODO = 'SWITCH_TODO'; // 2. action creators // 다른 components 에서 return의 객체를 만들기 위해서 사용된다. // 다른 components 에서 dispatch를 사용해 reducer를 호출한다. // 2. action creators(1) export const addTodo = (payload) => { return { type: ADD_TODO, payload, // payload: payload 와 같은 뜻 }; }; // 2. action creators(2) export const removeTodo = (payload) => { return { type: REMOVE_TODO, payload, }; }; // 2. action creators(3) export const switchTodo = (payload) => { return { type: SWITCH_TODO, payload, }; }; // 3. initial State => reducer 구성할 때 사용 const initialState = [ { id: uuidv4(), title: '제목1', contents: '내용1', isDone: false, }, { id: uuidv4(), title: '제목2', contents: '내용2', isDone: false, }, { id: uuidv4(), title: '제목3', contents: '내용3', isDone: false, }, ]; // 4. reducer를 만들 것 // 2번 action creators에서 만든 타입과 페이로드에 맞는 작업을 리듀서에서 수행한다. // action creators에서 "이런 액션을 해줘" 라고 하면 reducer 안에서 "그래, 그럼 들어온 payload 기준으로 state의 action들을 해줄게." 라고 하는곳 const todos = (state = initialState, action) => { // action에 action creators에서 만든 객체가 들어감. switch (action.type) { case ADD_TODO: return [...state, action.payload]; // ...state => todos라는 배열으로 action.payload는 그 안에 들어가는 todo객체임 case REMOVE_TODO: return state.filter(item => item.id !== action.payload) // action.payload => id로 들어올거임 case SWITCH_TODO: return state.map(item => { if (item.id === action.payload) { return { ...item, isDone: !item.isDone }; } else { return item; } }) default: return state; } }; // 5. reducer를 export 할 것 export default toHaveDescription;
- action : state에 접근해서 어떠한 것을 할 수 있게 하는 action들
- 각 dispatch에서 payload로 넘겨준 값이 다르기 때문에 switch문의 타입별 action.payload는 다를 수 있다.
5. configStore를 reducer와 연결하기
이전에 주석으로 기재했던 todos의 주석을 푼다
import {combineReducers, createStore} from 'redux' const rootReducer = combineReducers({ todos, }) const store = createStore(rootReducer); export default store;
6. index.js 수정
- Provider하고 Store를 import 해온다.
- App 컴포넌트를 Provider로 감싸주고 Store를 연결시킨다.
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; // import 추가 import { Provider } from 'react'; import store from './redux/config/configStore'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <Provider store={store}> // 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();
6. App.js 파일
라우터를 넣는다
7. 컴포넌트 만들기
어떤 컴포넌트가 들어가야 될까 고민하고 작성해본다.
아래와 같이 4개의 컴포넌트로 구성한 Main 페이지를 만들어보려고 한다.
Header,Footer는 꾸며주기만 하면 되는애라 나머지 3개에 대해서 코드를 작성해보겠다.
import React from 'react'; export default function Main() { return ( <> <Header /> <Input /> <TodoList /> <TodoList /> <Footer /> </> ); }
7-1. Input 컴포넌트에 글 추가 기능 만들기
입문 Todo 만들 때 했던거랑 많이 비슷하다.
그것이랑 다른거라면, newTodo를 만들고, setTodo에 newTodo를 추가하는 방식이였는데,
Reducer를 이용하면 dispatch를 이용해서 newTodo를 추가한다.
import React, {useState} from 'react'; import styled from 'styled-components'; import {v4 as uuidv4} from 'uuid'; import {useDispatch} from 'react-redux'; import {addTodo} from '../modules/todos'; export default function Input() { const [title, setTitle] = useState(''); const [contents, setContents] = useState(''); const dispatch = useDispatch(); const handleSubmitButtonClick = (e) => { e.preventDefault(); const newTodo = { id: uuidv4(), title, contents, isDone: false, }; dispatch(addTodo(newTodo)); // dispatch로 addTodo를 불러와서 payload로 newTodo를 전달해주고 // todos.js에서 이걸 받아서 type에 맞는 return 문을 실행해줌 setTitle(''); setContents(''); }; const handleTitleOnChange = (e) => { setTitle(e.target.value); }; const handleContentsOnChange = (e) => { setContents(e.target.value); }; return ( <StyledInputBox> <form onSubmit={handleSubmitButtonClick}> <input value={title} onChange={handleTitleOnChange} /> <input value={contents} onChange={handleContentsOnChange} /> <button>추가하기</button> </form> </StyledInputBox> ); }
7-2. TodoList 컴포넌트에 글 삭제 기능 만들기
입문 Todo랑 다른점은, payload로 id를 넘겨줘야 하기 때문에, item.id를 가져와야되는데
onClick에 함수이름을 넣을 때 원래는
<button onClick={handleDeleteButtonClick}>삭제</button>이렇게 함수 이름만 넣어줬었다.payload로 id를 넘겨주는건 이 onClick 안에서 인자로서 item.id를 함수에 넘겨줘야 하는데,<button onClick={handleDeleteButtonClick(item.id)}>삭제</button>이렇게 작성해버리면 함수가 바로 동작한다는 의미이기 때문에 함수이름을 함수안에서 리턴해주는 방식으로 만들어줘야 한다.import React from 'react'; import {useDispatch,useSelector} from 'react-redux' import styled from 'styled-components'; import {removeTodo} from '../modules/todos'; export default function TodoList({isActive}) { // store에 있는 todos를 가지고 와야 함 const todos = useSelector((state) => state.todos); // state는 modules/todos 에서 만든 모든 state를 말하기때문에 todos만 따로 챙겨옴 const dispatch = useDispatch(); const handleDeleteButtonClick = (id) => { dispatch(removeTodo(id)); } return ( <StyledLisBox> <h4>완료</h4> {todos .filter((item)=>item.isDone === !isActive) .map((item) => { return ( <StyledTodoBox key={item.id}> <h4>{item.title}</h4> <p>{item.contents}</p> <p>{item.isDone.toString()}</p> <button>{isActive ? '완료':'취소'}</button> <button onClick={()=>handleDeleteButtonClick(item.id)}>삭제</button> </StyledTodoBox> ); })} </StyledLisBox> ); }
7-3. TodoList 컴포넌트에 글목록 스위치 기능 만들기
이것도 payload로 id만 넘겨주면 된다.
import React from 'react'; import {useDispatch,useSelector} from 'react-redux' import styled from 'styled-components'; import {removeTodo, switchTodo} from '../modules/todos'; export default function TodoList({isActive}) { // store에 있는 todos를 가지고 와야 함 const todos = useSelector((state) => state.todos); // state는 modules/todos 에서 만든 모든 state를 말하기때문에 todos만 따로 챙겨옴 const dispatch = useDispatch(); const handleDeleteButtonClick = (id) => { dispatch(removeTodo(id)); } // dispatch로 id 넘겨주기 const handleSwitchButtonClick = (id) => { dispatch(switchTodo(id)); } return ( <StyledLisBox> <h4>{isActive ? '해야할 일' : '완료된 일'}</h4> {todos .filter((item)=>item.isDone === !isActive) .map((item) => { return ( <StyledTodoBox key={item.id}> <h4>{item.title}</h4> <p>{item.contents}</p> <p>{item.isDone.toString()}</p> {/* 이것도 함수로.. */} <button onClick={()=>handleSwitchButtonClick(item.id)}> {isActive ? '완료' : '취소'} </button> <button onClick={() => handleDeleteButtonClick(item.id)}> 삭제 </button> </StyledTodoBox> ); })} </StyledLisBox> ); }
8. Header 컴포넌트 레이아웃에 붙혀놓기
Main 컴포넌트에서 Header를 빼고 Router로 옮겨놓는다.
// Main.jsx import React from 'react'; import Header from '../redux/components/Header'; import Footer from '../redux/components/Footer'; import TodoList from '../redux/components/TodoList'; import Input from '../redux/components/Input'; export default function Main() { return ( <> <Input /> <TodoList isActive={true} /> <TodoList isActive={false}/> <Footer /> </> ); }
// Router.js import {BrowserRouter, Routes, Route} from 'react-router-dom'; import Main from '../pages/Main'; import Detail from '../pages/Detail'; import Header from '../redux/components/Header'; const Router = () => { return ( <BrowserRouter> <Header /> {/* 헤더를 여기에 놓으면 페이지가 변경되어도 헤더는 계속 남아있다. */} <Routes> {/* 페이지가 변경되면 바뀌는 부분 */} <Route path='/' element={<Main />}></Route> <Route path='/:id' element={<Detail />}></Route>{' '} {/*뒤에 어떤 값이 오더라도 id로 가지고 있을 수 있다.*/} </Routes> </BrowserRouter> ); }; export default Router;
이렇게 detail 페이지로 넘어와도 header가 남아있는 것을 볼 수 있다.
9. Detail 페이지 만들기
9-1. TodoList에서 상세보기 들어갈 수 있는 버튼 만들고 Navigate 사용하기
import React from 'react'; import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' import styled from 'styled-components'; import {removeTodo, switchTodo} from '../modules/todos'; export default function TodoList({isActive}) { // store에 있는 todos를 가지고 와야 함 const todos = useSelector((state) => state.todos); // state는 modules/todos 에서 만든 모든 state를 말하기때문에 todos만 따로 챙겨옴 const dispatch = useDispatch(); const navigate = useNavigate(); const handleDeleteButtonClick = (id) => { dispatch(removeTodo(id)); } // dispatch로 id 넘겨주기 const handleSwitchButtonClick = (id) => { dispatch(switchTodo(id)); } return ( <StyledLisBox> <h4>{isActive ? '해야할 일' : '완료된 일'}</h4> {todos .filter((item)=>item.isDone === !isActive) .map((item) => { return ( <StyledTodoBox key={item.id}> <h4>{item.title}</h4> <p>{item.contents}</p> <p>{item.isDone.toString()}</p> {/* 이것도 함수로.. */} <button onClick={()=>handleSwitchButtonClick(item.id)}> {isActive ? '완료' : '취소'} </button> <button onClick={() => handleDeleteButtonClick(item.id)}> 삭제 </button> <br /> <br /> {/* navigate에 백틱으로 주소를 item.id로 만들고 그곳으로 이동하게 만들기 */} <button onClick={() => { navigate(`/${item.id}`) }}>상세보기</button> </StyledTodoBox> ); })} </StyledLisBox> ); }
이렇게 글의 상세보기를 누르면 그 글에 해당하는 id주소로 들어가는것이 확인됐다.
이제 Detail 페이지만 장식하면 된다.
9-2. Detail에 주소값과 맞는 id의 정보 가져오기
useParams 메서드 : 따로 정리해서 올릴거다.. 무슨 메서드인지 구글링해도 이해가 되지 않는다. 파라미터를 상속?하는거라는데 맞는건가..
import React from 'react'; import styled from 'styled-components'; import {useParams, useNavigate} from 'react-router-dom'; import {useSelector} from 'react-redux'; export default function Detail() { const navigate = useNavigate(); const paramId = useParams().id; // 뭔지 모르겠음; const todos = useSelector((state) => state.todos); // todos를 가져옴 // item.id와 paramId가 일치한 것만 출력되게하기 // [0]은, todos는 [{ items }] 와 같이 배열 안에 객체가 있기 때문에 [0]을 적어서 객체만 사용할 수 있게 해줌 const filteredTodo = todos.filter((item) => item.id === paramId)[0]; return ( <StyledDetailBox> <h3 style={{marginBottom: '10px'}}> 입력받은 ID와 일치하는 상세보기를 출력 </h3> <p>제목 : {filteredTodo.title}</p> <p>내용 : {filteredTodo.contents}</p> <p>완료여부 : {filteredTodo.isDone.toString()}</p> <button onClick={() => { navigate('/'); }} > 이전 페이지로 </button> </StyledDetailBox> ); }
근데 여기서 문제가 있다.
지금은 새로고침이 될 때마다 id가 변경되기 때문에 detail페이지에서 새로고침하면 오류가 난다.
이건 나중에 json-server 사용하게되면 서버 안에서만 uuid가 생성되게 하면 해결되는 문제인 것 같다.
프로젝트 할 때 유념해서 코드 짜보자.
이렇게 redux로 TodoList를 사용해보았다.
1시간 20분정도 되는 분량을 하나하나 정리하고 적으면서 하니 하루 꼬박 걸렸지만, Redux에 대해서 크게 이해되는 공부였다.
Detail 페이지 완성
'React' 카테고리의 다른 글
Custom Hook (0) 2022.12.21 성능 최적화를 위한 React-hook (0) 2022.12.21 Redux 미들웨어(Thunk) (0) 2022.12.20 styled component porps해서 쓰기 (0) 2022.12.20 Redux(3) - react-router-dom, props children, Dynamic Route (0) 2022.12.20