1주차에 만들었던 프로젝트를 react-router-dom, styled-components, redux를 사용해서
My Todo List 를 다시 만드는게 과제다...
저번 프로젝트에 새로 배운것들을 추가만 할까 생각 했지만 복습도 할겸 다시 만들기로 결정했다...
npx create-react-app todo-list-redux
yarn add styled-components
yarn add redux react-redux
yarn add react-router-dom
우선 프로젝트를 만들고 필요한 라이브러리를 다운받았다.
그리고 폴더와 파일들을 만들었다...
styled-components를 사용할거라 css는 뺐다 개꿀
서버를 돌려봤다 잘 되는군...
이제 리액트 로고가 돌아가는걸 보며 뭐 부터 할지 생각한다....
Router를 사용하니까 페이지 이동부터 만들고
html css꾸미고
컴포넌트화 하면서
기능을 넣으면 되겠다... 후 말로는 쉽군
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
//페이지들..
import TodoList from "../pages/TodoList";
import Detail from "../pages/Detail";
const Router = () => {
return (
<BrowserRouter>
<Routes>
{/*
Routes안에 이렇게 작성합니다.
Route에는 react-router-dom에서 지원하는 props들이 있습니다.
paht는 우리가 흔히 말하는 사용하고싶은 "주소"를 넣어주면 됩니다.
element는 해당 주소로 이동했을 때 보여주고자 하는 컴포넌트를 넣어줍니다.
*/}
<Route path="/" element={<TodoList />} />
<Route path="/detail" element={<Detail />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
우선 라우터 페이지를 작성 했다.
그리고 App에서 router를 불러왔다.
import './App.css';
import Router from './shared/Router';
function App() {
return (
<Router />
);
}
export default App;
페이지 이동이 잘 된다 ㅎㅎ router 끝났쥬?
컴포넌트 화하며 꾸미기 전에 store를 만들고 reducer를 먼저 만들어야겠다...
생각해 보니 이걸 먼저 만들어야 겠다...
configStore.js
import { createStore } from "redux";
import { combineReducers } from "redux";
// 리듀서
import todoList from "../modules/todoList";
const rootReducer = combineReducers({
todoList: todoList,
});
//스토어
const store = createStore(rootReducer);
//스토어 익스포트
export default store;
우선 store를 선언을 했다.
todoList.js
//action 타입
const ADD_TODO = 'ADD_TODO';
//action 함수
export const addTodo = (payload) => {
if (payload.todo !== undefined && payload.todo !== "") {
return {
type: ADD_TODO,
id: payload.length,
title: payload.title,
content: payload.todo,
done: payload.done
};
}
}
// 초기 상태값
const initTodoList = [
{
id: 0,
title: `리액트 공부하기`,
content: `리액트 기초를 공부해봅시다.`,
done: false,
},
{
id: 1,
title: `리액트 공부하기2`,
content: `리액트 기초를 공부해봅시다2.`,
done: true,
}
];
// 리듀서
const todoList = (state = initTodoList, action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: action.id,
title: action.title,
content: action.content,
done: action.done
}
];
default:
return state;
}
};
export default todoList;
우선 추가하는 기능만 만들었다.. 잘 되면 업데이트랑 삭제도..
import React from 'react';
//스토어의 상태값과 action을 취하기 위해
import { useSelector, useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { addTodo } from '../redux/modules/todoList';
const TodoList = () => {
const gTodoList = useSelector(state => state.todoList);
console.log(gTodoList);
return (
<div>
TodoList
</div>
);
};
export default TodoList;
todoList 페이지에 스토어를 연결하고 실행 해봤다
깔끔하게 에러 짜릿하다.
에러 복사후 번역해보니 스토어 범위를 안정해줬다...
뭐야? 왜 친절해?
구글링 전에 번역해보면 바로 처리할수 있는것들이 대부분일때가 많다
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
//스토어 가져오기
import store from "./redux/config/configStore";
//스토어 사용범위를 정할 프로바이더 가져오기
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// 프로바이더 선언및 속성으로 스토어주기
<Provider store={store}>
<App />
</Provider>
);
reportWebVitals();
Provider로 스토어의 범위를 정해줬다.
바로 잘 나온다.
역시 하나씩 확인하면서 해야해...
이제 컴포넌트들을 페이지 뿌려줘야겠다.
가즈아 폭풍 복붙!
import React from 'react';
import Layout from "../components/layout/Layout";
import List from "../components/list/List";
import Form from "../components/form/Form";
import Header from "../components/header/Header";
const TodoList = () => {
return (
<Layout>
<Header />
<Form />
<List />
</Layout>
);
};
export default TodoList;
생각해 보니 state는 List컴포넌트에서 불러와야겠다...
redux를 쓰니 컴포넌트에 속성으로 useSate를 넘길 필요가 없어 편하다.
일단 뭐가 보여야 하니 Layout과 Header에 내용을 채웠다.
import React from "react";
import styled from "styled-components";
const StLayoutDiv = styled.div`
margin: 0 auto;
max-width: 1200px;
min-width: 800px;
`;
function Layout(props) {
return (
<StLayoutDiv>
{props.children}
</StLayoutDiv>
);
}
export default Layout;
import React from "react";
import styled from "styled-components";
const StHeaderDiv = styled.div`
align-items: center;
border: 1px solid #ddd;
display: flex;
height: 50px;
justify-content: space-between;
padding: 0 20px;
`
function Header() {
return (
<StHeaderDiv>
<div>My Todo List</div>
<div>React</div>
</StHeaderDiv>
)
}
export default Header;
실행
이런식으로 html과 css부터 채워야겠다.
나는 멍청해서 보면서 해야한다 ㅠ
일단 기능은없고 css component를 이용해서 만들었다.
소스는 길어서 생략한다.
이제 기능을 넣으면 끝이다..
import React from "react";
import Todo from "../todo/Todo";
import styled from "styled-components";
//state
import { useSelector } from 'react-redux';
const StListContainerDiv = styled.div`
padding: 0 24px;
`
const StListWrapperDiv = styled.div`
display: flex;
flex-wrap: wrap;
gap: 12px;
`
const StH2 = styled.h2`
display: block;
font-size: 1.5em;
margin-block-start: 0.83em;
margin-block-end: 0.83em;
margin-inline-start: 0px;
margin-inline-end: 0px;
font-weight: bold;
`
function List() {
const gTodoList = useSelector(state => state.todoList);
//console.log(gTodoList)
return (
<StListContainerDiv>
<StH2>Working.. 🔥</StH2>
<StListWrapperDiv>
{gTodoList.map((val) => {
if (val.done === false) return (<Todo obj={val} key={val.id} />);
})}
</StListWrapperDiv>
<StH2>Done..! 🎉</StH2>
<StListWrapperDiv>
{gTodoList.map((val) => {
if (val.done) return (<Todo obj={val} key={val.id} />);
})}
</StListWrapperDiv>
</StListContainerDiv>
)
}
export default List;
list에서 store의 state를 받아와 뿌려준다.
import React from "react";
import styled from "styled-components";
const StTodoContainerDiv = styled.div`
border: 4px solid teal;
border-radius: 12px;
padding: 12px 24px 24px;
width: 270px;
`
const StButtonWrap = styled.div`
display: flex;
gap: 10px;
margin-top: 24px;
`
const StTodoButton = styled.button`
border: 1px solid ${(props) => props.buttonColor};
height: 40px;
width: 120px;
background-color: rgb(255, 255, 255);
border-radius: 12px;
cursor: pointer;
`
const StDetail = styled.a`
text-decoration: none;
`
function Todo({ obj }) {
return (
<StTodoContainerDiv>
<div>
<StDetail href="/detail">상세보기</StDetail>
<h2>{obj.title}</h2>
<div>{obj.content}</div>
</div>
<StButtonWrap>
<StTodoButton
buttonColor="red"
onClick={() => {
// deleteTodo(obj.id);
}}
>
삭제하기
</StTodoButton>
<StTodoButton
buttonColor="green"
onClick={() => {
// changeDone(obj.id, obj.done);
}}
>
{obj.done ? "취소" : "완료"}
</StTodoButton>
</StButtonWrap>
</StTodoContainerDiv>
);
}
export default Todo;
todo페이지 기능은 아직이다.
state의 초기값으로 잘 뿌려진다.
이제 추가하기 기능을 만들어 보자
import React, { useState } from "react";
import styled from "styled-components";
//스토어의 상태값과 action을 취하기 위해
import { useSelector, useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { addTodo } from '../../redux/modules/todoList';
const StAddFormDiv = styled.div`
background-color: rgb(238, 238, 238);
border-radius: 12px;
margin: 0px auto;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify;
justify-content: space-between;
padding: 30px;
gap: 20px;
`
const StInputGroupDiv = styled.div`
align-items: center;
display: flex;
gap: 20px;
`
const StFormLabel = styled.label`
font-size: 16px;
font-weight: 700;
`
const StAddInput = styled.input`
border: none;
border-radius: 12px;
height: 40px;
padding: 0 12px;
width: 240px;
`
const StAddButton = styled.button`
background-color: teal;
border: none;
border-radius: 10px;
color: #fff;
font-weight: 700;
height: 40px;
width: 140px;
`
function Form() {
const [inputText, setInputText] = useState({ title: "", content: "" });
//store의 상태값 변수로 선언
const gTodoList = useSelector(state => state.todoList);
//store의 상태를 변경할 dispatch선언
const dispatch = useDispatch();
//인풋 바뀔때 마다 값넣기
const getInput = (e) => {
setInputText({
...inputText,
[e.target.name]: e.target.value,
});
};
//id최대값
const getMaxId = () => {
let stateIdArr = gTodoList.map(element => {
return Number(element.id)
});
return Math.max(...stateIdArr);
}
return (
<StAddFormDiv>
<StInputGroupDiv>
<StFormLabel>제목</StFormLabel>
<StAddInput
type="text"
name="title"
onChange={getInput}
value={inputText.title || ""}
/>
<StFormLabel>내용</StFormLabel>
<StAddInput
type="text"
name="content"
onChange={getInput}
value={inputText.content || ""}
/>
</StInputGroupDiv>
<StAddButton onClick={() => {
if (inputText.title === "" || inputText.content === "" || inputText.title === undefined || inputText.content === undefined) {
alert("빈칸좀..")
} else {
let obj = {
id: Number(getMaxId()) + 1,
title: inputText.title,
content: inputText.content
}
dispatch(addTodo(obj));
setInputText({});
}
}}>
추가하기
</StAddButton>
</StAddFormDiv>
);
}
export default Form;
state의 id의 최대값을 찾아 +1해준 값을 id에 넣어 추가해줬다.
length로 하려했는데 하지말라고해서...
length로 했을때 id가 0,1,2가있다고 치면
1을 지우고 새로 만들면 id가 2인것이 2개가 될것이다.
이제 완료 취소 삭제하기 상세보기만 하면 끝이다.
//action 타입
const ADD_TODO = 'ADD_TODO';
const CHANGE_DONE = 'CHANGE_DONE';
const DELETE_TODO = 'DELETE_TODO';
//action 함수
export const addTodo = (obj) => {
return {
type: ADD_TODO,
id: obj.id,
title: obj.title,
content: obj.content,
done: false
};
}
export const changeDone = (id) => {
return {
type: CHANGE_DONE,
id: id
}
}
export const deleteTodo = (id) => {
return {
type: DELETE_TODO,
id: id
}
}
// 초기 상태값
const initTodoList = [
{
id: 0,
title: `리액트 공부하기`,
content: `리액트 기초를 공부해봅시다.`,
done: false,
},
{
id: 1,
title: `리액트 공부하기2`,
content: `리액트 기초를 공부해봅시다2.`,
done: true,
}
];
// 리듀서
const todoList = (state = initTodoList, action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: action.id,
title: action.title,
content: action.content,
done: action.done
}
];
case CHANGE_DONE:
state.map((val, i, arr) => {
if (val.id === action.id) {
arr[i].done ? arr[i].done = false : arr[i].done = true
}
})
return [...state];
case DELETE_TODO:
state.map((val, i, arr) => {
if (val.id === action.id) {
arr.splice(i, 1);
}
})
return [...state];
default:
return state;
}
};
export default todoList;
이전에 만들었던 기능을 리듀서에 그대로 복붙했다.
import React from "react";
import styled from "styled-components";
//action을 취하기 위해
import { useDispatch } from 'react-redux';
//리듀서의 액션 함수 쓰기위해
import { deleteTodo, changeDone } from '../../redux/modules/todoList';
const StTodoContainerDiv = styled.div`
border: 4px solid teal;
border-radius: 12px;
padding: 12px 24px 24px;
width: 270px;
`
const StButtonWrap = styled.div`
display: flex;
gap: 10px;
margin-top: 24px;
`
const StTodoButton = styled.button`
border: 1px solid ${(props) => props.buttonColor};
height: 40px;
width: 120px;
background-color: rgb(255, 255, 255);
border-radius: 12px;
cursor: pointer;
`
const StDetail = styled.a`
text-decoration: none;
`
function Todo({ obj }) {
const dispatch = useDispatch();
return (
<StTodoContainerDiv>
<div>
<StDetail href={'/detail/' + obj.id}>상세보기</StDetail>
<h2>{obj.title}</h2>
<div>{obj.content}</div>
</div>
<StButtonWrap>
<StTodoButton
buttonColor="red"
onClick={() => {
dispatch(deleteTodo(obj.id));
}}
>
삭제하기
</StTodoButton>
<StTodoButton
buttonColor="green"
onClick={() => {
dispatch(changeDone(obj.id));
}}
>
{obj.done ? "취소" : "완료"}
</StTodoButton>
</StButtonWrap>
</StTodoContainerDiv>
);
}
export default Todo;
todo컴포넌트에서 기능을 구현했다.
redux개꿀...
case GET_DETAIL:
const selTodo = state.filter((val) => {
return (val.id === action.id);
})
return selTodo;
상세페이지 리듀서 추가
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';
import { useParams, Link } from 'react-router-dom'
import { getDetail } from '../redux/modules/todoList';
const StDetailWrapDiv = styled.div`
border: 2px solid rgb(238, 238, 238);
width: 100%;
height: 100vh;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
`
const StDetailBorderDiv = styled.div`
width: 600px;
height: 400px;
border: 1px solid rgb(238, 238, 238);
display: flex;
flex-direction: column;
-webkit-box-pack: justify;
justify-content: space-between;
`
const StDetailTitleDiv = styled.div`
display: flex;
height: 80px;
-webkit-box-pack: justify;
justify-content: space-between;
padding: 0px 24px;
-webkit-box-align: center;
align-items: center;
`
const StBackButton = styled.button`
border: 1px solid rgb(221, 221, 221);
height: 40px;
width: 120px;
background-color: rgb(255, 255, 255);
border-radius: 12px;
cursor: pointer;
`
const StDetailH1 = styled.h1`
padding: 0px 24px;
display: block;
font-size: 2em;
margin-block-start: 0.67em;
margin-block-end: 0.67em;
margin-inline-start: 0px;
margin-inline-end: 0px;
font-weight: bold;
`
const StDetailMain = styled.main`
padding: 0px 24px;
display: block;
`
const Detail = () => {
const [selTodo] = useSelector(state => state.todoList);
const { id } = useParams();
const dispatch = useDispatch();
useEffect(() => {
dispatch(getDetail(Number(id)))
}, [dispatch, id])
return (
<StDetailWrapDiv>
<StDetailBorderDiv>
<div>
<StDetailTitleDiv>
<div>ID : {selTodo.id}</div>
<Link to="/">
<StBackButton>이전으로</StBackButton>
</Link>
</StDetailTitleDiv>
<StDetailH1>{selTodo.title}</StDetailH1>
<StDetailMain>{selTodo.content}</StDetailMain>
</div>
</StDetailBorderDiv>
</StDetailWrapDiv>
);
};
export default Detail;
상세페이지
상세 페이지 갔다가 돌아오면 한개만 남음...
그래서 별짓을 다 해봄...
결론은 설계 미스...
state를 todoList랑 todo를 갖는 객체로 만들어야 겠다는 생각밖에 안떠오름... 3시간은 뻘짓한듯..
//action 타입
const ADD_TODO = 'ADD_TODO';
const CHANGE_DONE = 'CHANGE_DONE';
const DELETE_TODO = 'DELETE_TODO';
const GET_DETAIL = 'GET_DETAIL';
//action 함수
export const addTodo = (obj) => {
return {
type: ADD_TODO,
id: obj.id,
title: obj.title,
content: obj.content,
done: false
};
}
export const changeDone = (id) => {
return {
type: CHANGE_DONE,
id: id
}
}
export const deleteTodo = (id) => {
return {
type: DELETE_TODO,
id: id
}
}
export const getDetail = (id) => {
return {
type: GET_DETAIL,
id: id
}
}
// 초기 상태값
const initTodoList = {
todoList: [
{
id: 0,
title: `리액트 공부하기`,
content: `리액트 기초를 공부해봅시다.`,
done: false,
},
{
id: 1,
title: `리액트 공부하기2`,
content: `리액트 기초를 공부해봅시다2.`,
done: true,
}
],
todo: {
id: 1,
title: `리액트 공부하기2`,
content: `리액트 기초를 공부해봅시다2.`,
done: true,
}
};
// 리듀서
const todoList = (state = initTodoList, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todoList: [
...state.todoList,
{
id: state.todoList.length === 0 ? 0 : action.id,
title: action.title,
content: action.content,
done: action.done
}
]
};
case CHANGE_DONE:
state.todoList.map((val, i, arr) => {
if (val.id === action.id) {
arr[i].done ? arr[i].done = false : arr[i].done = true
}
})
return { ...state, todoList: [...state.todoList] };
case DELETE_TODO:
state.todoList.map((val, i, arr) => {
if (val.id === action.id) {
arr.splice(i, 1);
}
})
return { ...state, todoList: [...state.todoList] };
case GET_DETAIL:
const [selTodo] = state.todoList.filter((val) => {
return (val.id === action.id);
})
return { ...state, todo: selTodo };
default:
return state;
}
};
export default todoList;
상태값의 타입을 새로 설정했다.
리스트의 배열객체랑 객체...
잘 된다...
useNavigate도 써보고 별짓 다 했었는데...
설계미스... 눈물난다
그래도 해결되서 행복하다..
끝
'과제' 카테고리의 다른 글
항해99 React 1주차 과제 컴포넌트화 (0) | 2022.10.03 |
---|---|
항해99 React 1주차 과제 (1) | 2022.10.02 |
언어 과제 (1) | 2022.09.23 |