Jieunny์ ๋ธ๋ก๊ทธ
S3) Unit 8. [์ค์ต] Coz'Mini Hackaton (Todo-List) ๋ณธ๋ฌธ
๐ฃ ์คํ์ผ ์ปดํฌ๋ํธ & ๋ฆฌ๋์ค & ๋ฆฌ์กํธ ๋ผ์ฐํฐ๋ฅผ ํ์ฉํ ํฌ๋ ๋ฆฌ์คํธ ๊ตฌํํ๊ธฐ
๐ญ. ํ์ผ ๊ตฌ์กฐ
src
ใด components (์ปดํฌ๋ํธ ํ์ผ)
ใด TodoList.js : TodoItem์ ๋ชจ์ ๋ ผ TotoList
ใด TodoItem.js : ํ๋์ TodoItem
ใด TodoInsert.js : Todo ์ ๋ ฅํ๋ ์นธ
ใด MemoList.js : MemoItem์ ๋ชจ์ ๋ ผ MemoList
ใด MemoItem.js : ํ๋์ MemoItem
ใด MemoInsert.js : Memo ์ ๋ ฅํ๋ ์นธ
ใด pages ( ๋ผ์ฐํฐ๋ฅผ ํตํด ์ด๋ํ ํ์ด์ง)
ใด TodoTemplate.js : ์ฒ์ ํ์ด์ง ์ด์์ ๋ ๋จ๋ ํ๋ฉด(Menu ํ ๊ธ, ํ์คํฐ ๋ณด๊ธฐ ์ฐฝ๋ ์ฌ๊ธฐ์ ๋์ด๋ค)
ใด MemoTemplate.js : Header์์ ๋ฉ๋ชจ ์์ด์ฝ ๋๋ฅด๋ฉด ์ด๋ํ๋ ํ๋ฉด
ใด Ctreator.js : ์ฒ์ ํ์ด์ง์์ ๋ฉ๋ด ์์ด์ฝ -> ์ ์์ ๋๋ฅด๋ฉด ์ด๋ํ๋ ํ๋ฉด
ใด reducer ( ์ก์ , ์ก์ ์์ฑ ํจ์)
ใด Todo.js : TodoList์ ํ์ํ ์ก์ , ์ก์ ์์ฑํจ์ ์ ์
ใด Memo.js : MemoList์ ํ์ํ ์ก์ , ์ก์ ์์ฑํจ์ ์ ์
๐ฎ. ํผ๊ทธ๋ง ์์ฐํ๋ฉด
โฐ ํผ๊ทธ๋ง๋ก ์ ๋ ฅ๊ฐ ๋ฐ๊พธ๋ ๊ฑด ์ด๋ป๊ฒ ํ๋ ๊ฑธ๊น..
๐ฏ. ์ฝ๋ ๊ตฌํ (์ค์ํ๊ฑฐ๋ ๊ตฌํํ ๋ ์ด๋ ค์ ๋ ์ฝ๋๋ง)
− Todo๋ Memo๊ฐ ๊ตฌํ ๋ฐฉ๋ฒ์ ๊ฐ์ผ๋ฏ๋ก Todo๋ง ์ค๋ช
๐ App.js
import TodoTemplate from './pages/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import styled, { createGlobalStyle } from 'styled-components';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import MemoInsert from './components/MemoInsert';
import MemoTemplate from './pages/MemoTemplate';
import Creator from './pages/Creator';
import { AnimatePresence } from "framer-motion";
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
font-family: 'Jua', sans-serif;
}
`;
const AppContainer = styled.div`
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
`
function App() {
return (
<Router>
<AppContainer>
<GlobalStyle />
<AnimatePresence>
<Routes>
<Route path="/" element={
<TodoTemplate>
<TodoInsert />
<TodoList />
<MemoInsert />
</TodoTemplate>
} />
<Route path="/memolist" element={<MemoTemplate />} />
<Route path="/creator" element={<Creator />} />
</Routes>
</AnimatePresence>
</AppContainer>
</Router>
);
}
export default App;
โฐ GlobalStyle์ ์ฌ์ฉํด์ box-sizing์์ฑ์ ๋ชจ๋ ์์์ ์ ์ฉํด์ฃผ๊ณ , ๊ธ๊ผด๋ ๋ฐ๊ฟ์ค๋ค.
โฐ ํ์ด์ง ์ ํ์ด ์์ฐ์ค๋ฝ๊ฒ ๋๋ motion์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ํ์ด์ง ์ด๋์ด ์ผ์ด๋๋ <Routes> ๋ถ๋ถ์ 'framer-motion' ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ <AnimatePresence>๋ก ๊ฐ์ธ์ฃผ์ด์ผ ํ๋ค.
โฐ ํ์ด์ง ์ ํ์ด ์ผ์ด๋๋ ํ์ผ(์ปดํฌ๋ํธ)์ <Route path="">๋ฅผ ์ฌ์ฉํด์ ๋ฏธ๋ฆฌ ๊ฒฝ๋ก๋ฅผ ์ง์ ํด์ฃผ์ด์ผ ํ๋ค.
๐ Todo.js
// ์ก์
, ์ก์
์์ฑํจ์์ธ ๋ฆฌ๋์
// ์ก์
-> dispatch() -> ๋ฆฌ๋์ -> store ์์ฑ
// ๋ด๊ฐ ์ก์
์ ์คํํด์ผ ํ ๋ dispatch()๋ก ๋ถ๋ฌ์จ๋ค.
// ์ก์
ํ์
์ ์
const TODO_INSERT = "TODO/INSERT";
const TODO_REMOVE = "TODO/REMOVE";
const TODO_UPDATE = "TODO/UPDATE";
const TODO_TOGGLE = "TODO/TOGGLE";
const TODO_RESET = "TODO/RESET";
// ์ก์
์์ฑ ํจ์ ์ ์(์ปดํฌ๋ํธ์์ ๋ฐ์์จ ์ธ์๋ฅผ ์ ๋ฌ๋ฐ์ ์ฌ์ฉํ๋ค)
export const todoInsert = (id, text) => {
return {
type: TODO_INSERT,
payload: {
id: id,
text: text,
isCompleted: false,
},
};
};
// Remove, Update, Toggle๋ ๊ฐ์ ์๋ฆฌ
// ์ด๊น๊ฐ ์ ์
const initState = {
todos: [
{
id: 1,
text: "๋๋ด์ฃผ๊ฒ ์จ์ฌ๊ธฐ",
isCompleted: true,
},
{
id: 2,
text: "๊ฐ์ง๋๊ฒ ์๊ธฐ",
isCompleted: false,
},
{
id: 3,
text: "์์ด๋๊ฒ ๋ฐฅ๋จน๊ธฐ",
isCompleted: false,
},
],
};
// ๋ฆฌ๋์ ์์ฑ(์ก์
์ ๋ฆฌ๋์์ ์ ๋ฌํ๊ณ , ๋ฆฌ๋์๊ฐ ์ด๋ฅผ ๋ณด๊ณ ์คํ ์ด์ ์ํ๋ฅผ ์
๋ฐ์ดํธ ํ๋ค)
// ์ก์
์ ๋ฆฌ๋์์ ์ ๋ฌํ๊ธฐ ์ํด์๋ dispatch() ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
export default function todoReducer(state = initState, { type, payload }) {
switch (type) {
case TODO_INSERT:
return {
...state,
todos: state.todos.concat({
id: payload.id,
text: payload.text,
isCompleted: false,
}),
};
case TODO_REMOVE:
return {
...state,
todos: state.todos.filter((todo) => todo.id !== payload.id),
};
case TODO_UPDATE:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === payload.id ? { ...todo, text: payload.text } : todo
// todoUpdate๋ ์
๋ฐ์ดํธ ํ text๋ฅผ ๋ฐ์์ค๋ฏ๋ก text๋ง ๋ฐ๊ฟ์ค๋ค.
),
};
case TODO_TOGGLE:
return {
...state,
todos: state.todos.map((todo) =>
todo.id === payload.id
? { ...todo, isCompleted: !todo.isCompleted }
: todo
),
};
default:
return { ...state };
}
}
๐ TodoTemplate.js
import { motion } from "framer-motion";
.hide-menu{
//...
}
.show-menu{
position: absolute;
width: 100%;
height: 80%;
background-color: #F5EBE0;
margin: 0;
border-radius: 15px 15px 0 0;
padding: 1rem;
bottom: 0;
left: 0;
transition: 1s;
box-shadow: rgba(17, 17, 26, 0.1) 0px 1px 0px, rgba(17, 17, 26, 0.1) 0px 8px 24px, rgba(17, 17, 26, 0.1) 0px 16px 48px;
}
// ๋ฉ๋ด ๋ฒํผ ๋๋ฅด๊ธฐ ์ ์ ์จ๊ฒจ๋๊ณ , ๋๋ฅด๋ฉด ์ผ->์ค ๋ก ๋ํ๋๊ฒ ํ๋ค.
function TodoTemplate({children}){
let today = new Date();
let todayStr = today.toISOString().slice(0, 10);
const week = ['์ผ', '์', 'ํ', '์', '๋ชฉ', '๊ธ', 'ํ '];
let todayWeek = week[today.getDay()];
const todos = useSelector((state) => state.todoReducer.todos);
let completedTodo = todos.filter((todo) => todo.isCompleted);
const [menuOn, setMenuOn] = useState(false);
const [imgOn, setImgOn] = useState(false);
const toggleMenu = () => {
setMenuOn(!menuOn);
}
const toggleImg = () => {
setImgOn(!imgOn);
}
return(
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
>
<Template>
<Header>
<StyledLink to="/memolist"><TbNotes className="memo" size="30" color="#1B1A17"/></StyledLink>
// react-router์ Link๋ก ๋ฉ๋ชจ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ฉ๋ชจ๋ค์ด ์ ์ฅ๋ ํ์ด์ง๋ก ์ด๋ํ๋ค.
</Header>
<Title> ์ค๋์ ํ ์ผ </Title>
<CompletedTodo>
...
</CompletedTodo>
{children}
<ul className={menuOn ? 'show-menu': 'hide-menu'}>
<StyledLink to="/creator"><MenuLi><MdPeopleOutline className="icon" size="40" color="#1B1A17" />์ ์์</MenuLi></StyledLink>
// ์ฌ๊ธฐ๋ Link๋ฅผ ์ด์ฉํด์ ๋๋ฅด๋ฉด ์ ์์ ํ์ด์ง๋ก ์ด๋ํ๋ค.
</ul>
{ imgOn ?
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
exit={{opacity: 0}}
>
<ImgContainer onClick={toggleImg}>
<img src={cinammon} className="cinnamon" alt="ํฌ๊ธฐ๊ฐ ๋ณํ๋ ํ์คํฐ"></img>
</ImgContainer>
</motion.div>
:
null
}
</Template>
</motion.div>
)
}
โฐ Menu ํ ๊ธ์ styled-component์ props๋ฅผ ์ด์ฉํด์ ๊ตฌํํ๊ณ ์ถ์๋๋ฐ, ๊ทธ๋ฌ๋ฉด ๋ํ๋๊ธฐ๋ ํ๋๋ฐ ํผ์ณ์ง๋ ๊ฒ ๊ฐ์ ํจ๊ณผ๋ฅผ ์ค ์๊ฐ ์์ด์ ์ฌ์ฉํ์ง ๋ชปํ๊ณ , ํ์๋๋ก class๋ฅผ ์ฌ์ฉํด์ ๊ตฌํํ๋ค.
โฐ 'framer-motion' ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ motion์ ์ฌ์ฉํด์ ๋ฆฌ์กํธ ๋ผ์ฐํฐ๋ก ํ์ด์ง๊ฐ ์ด๋ํ ๋, ํ๋ฒ์ ๋ฑ ์ผ์ง๋๊ฒ ์๋ opacity๋ฅผ ์กฐ์ ํด์ ์์ฐ์ค๋ฝ๊ฒ ํ์ด์ง ์ ํ์ด ๋๊ฒ ๋ง๋ค์๋ค.
โฐ ๋ฉ๋ด์์ 'ํ์คํฐ ๋ณด๊ธฐ' ํญ์ ๋๋ฅด๋ฉด imgOn์ด true๋ก ๋ฐ๋๋ฉฐ ํ์คํฐ ์ฌ์ง์ด ์ค๋ฒ๋ ์ด๋ก ๋ํ๋๋ค.
๐ TodoInsert.js
function TodoInsert(){
const [todoInput, setTodoInput] = useState('');
let nextId = useRef(4); // ๋ ๋๋ง์ด ๊ณ์ ๋๋ฉด ์๋๊ธฐ ๋๋ฌธ์ useRef์ฌ์ฉ
// id ๊ฐ์ด ๊ณ์ ++ ๋์ผ ํ๋๋ฐ useRef๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด ๊ณ์ ๋ ๋๋ง ๋์ ์ด๊ธฐํ๋๋ค.
const dispatch = useDispatch();
// ๋์คํจ์น๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ฃผ๋ Hooks
//...
const addTodo = () => {
if(todoInput.length === 0){
alert("ํ ์ผ์ ์
๋ ฅํด์ฃผ์ธ์!");
return;
}
dispatch(todoInsert(nextId.current, todoInput));
// ์ก์
์ dispatch๋ฅผ ํตํด ๋ฆฌ๋์๋ก ์ ๋ฌํ๋ค.
// ์ถ๊ฐ ๋ ํ ์ผ์ id์ ๋ด๊ฐ ์
๋ ฅํ ๊ฐ(ํ ์ผ ํ
์คํธ)์ ์ ๋ฌํ๋ค.
nextId.current += 1; // ๋ค์์ ๋ค์ด์ฌ ํ ์ผ์ ์ํด์ id+1 ํด์ค๋ค.
onRemove();
}
return(
...
)
}
โฐ dispatch๋ฅผ ์ฌ์ฉํด์ Todo.js์ ์ ์ํด ๋์๋ ์ก์ ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค.
๐ TodoItem.js
const TextBox = styled.input`
flex: 1;
display: inline;
border: none;
outline: none;
background-color: ${(props) => (!props.readOnly ? "rgb(0, 0, 0, 0.1)" : "transparent")};
width: 500px;
cursor: ${(props) => (!props.readOnly ? "defalut" : "pointer")};
text-decoration: ${(props) => (props.checked ? "line-through" : "none")};
color: ${(props) => (props.checked ? "gray" : "black")};
font-size: 1rem;
`;
// props๋ฅผ ์ด์ฉํด์ css๋ฅผ ๋ค๋ฅด๊ฒ ์ค์ ํ ์ ์๋ค.
function TodoItem({todo}) {
const {id, text, isCompleted} = todo;
const [readOnly, setReadOnly] = useState(true);
const [updateText, setUpdateText] = useState(text);
const dispatch = useDispatch();
const onChangeText = (e) => {
const value = e.target.value;
setUpdateText(value);
}
const updateTodo = () => {
if(!isCompleted) {
setReadOnly(!readOnly);
}
};
return(
<TodoItemBox>
<CheckBox onClick={() => dispatch(todoToggle(id))}>
{isCompleted ? (
<FiCheckSquare size="25px" />
) : (
<FiSquare size="25px" color="black" />
)}
{/* ์ฒดํฌ๋ฐ์ค ๋๋ฅด๋ฉด todoToggle์ ๋ถ๋ฌ์์ isComplete ์์ฑ์ ๋ฐ๊ฟ์ค๋ค */}
</CheckBox>
<TextBox
name="text"
readOnly={readOnly}
defaultValue={text}
checked={isCompleted}
onChange={onChangeText}
onBlur={() => dispatch(todoUpdate(id, updateText))}
/>
{/* ๋์ค์ ์์ ๊ธฐ๋ฅ ์ํด์ readOnly ์ ์ธํด์ค๋ค. */}
{/* onBlur => TextBox์ ํฌ์ปค์ค๊ฐ ์ฌ๋ผ์ง๋ฉด ๋ณ๊ฒฝ๋ ํ
์คํธ ๊ฐ์ dispatch๋ฅผ ์ฌ์ฉํด์ ์คํ ์ด์ ๋ฐ์ */}
...
<Button onClick={() => dispatch(todoRemove(id))}>
{/* ๋ฒํผ์ ๋๋ฅด๋ฉด todoRemove์ ๋ถ๋ฌ์์ ๊ทธ id๋ฅผ ๊ฐ์ง todo๋ฅผ ์ญ์ ํ๋ค. */}
<BsFillTrashFill size="25px" color="#e56b6f" />
</Button>
</TodoItemBox>
)
}
๐ MemoItem.js (textarea ๋์ด ์ค์ -> ๋ฐ์์จ text ๊ธธ์ด์ ๋ฐ๋ผ textarea์ ๋์ด๋ฅผ ๋ค๋ฅด๊ฒ ์ค์ ํด์ค๋ค)
const TextBox = styled.textarea`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
background-color: ${(props) => (!props.readOnly ? "rgb(0, 0, 0, 0.1)" : "transparent")};
cursor: ${(props) => (!props.readOnly ? "defalut" : "pointer")};
width: 100%;
text-align: center;
font-size: 1rem;
height: ${(props) => props.line !== 1 ? `${props.line * 21}px` : "24px"};
resize: none;
font-weight: 200;
`;
function MemoItem({memo}) {
...
return(
<MemoItemBox>
<div className="textContainer">
<TextBox
name="text"
readOnly={readOnly}
defaultValue={text}
checked={isCompleted}
onChange={onChangeText}
onBlur={() => dispatch(memoUpdate(id, updateText))}
line={Math.ceil(text.length / 23)}
></TextBox>
</div>
...
</MemoItemBox>
)
}
โฐ textarea์ line์ด๋ผ๋ props๋ฅผ ์ ๋ฌํด์ฃผ๋๋ฐ, ์ ์ฒด ํ ์คํธ ๊ฐ์๋ฅผ ๋๋ต ํ ์ค์ ์ ๋ ฅํ ์ ์๋ ํ ์คํธ ๊ฐ์๋ก ๋๋ ์ค๋ค(๋ฐ์์จ ํ ์คํธ๊ฐ ๋ช ์ค์ธ์ง ๋๋ต ๊ณ์ฐ ํ๋ ๊ฒ)
โฐ css๋ฅผ ์ง์ ํด์ฃผ๋ ๋ถ๋ถ์์ line์ ๋ฐ์์, ๋๋ต ํ์ค์ ๋์ด์ธ 21์ ๊ณฑํด์ ํ ์คํธ ๋์ด์ ๋ฐ๋ผ textarea ๋์ด๋ ๋ค๋ฅด๊ฒ ์ง์ ํด์ค๋ค.
โฐ ๋ด๊ฐ ๊ตฌํํ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ ํํ ๋ฑ ๋ง์๋จ์ด์ง๊ฒ ๊ณ์ฐ ํ ์๊ฐ ์์ด์ ๋น ๊ณต๊ฐ์ด ์๊ธฐ๊ฑฐ๋, ๊ธ๊ผด์ ๋ฐ๋ผ ์ํ๋ ๋์ด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ ์๋ ์๋ค.
โฐ ๊ทธ๋์ ๋ง์ด ๊ฒ์ํด๋ณด๊ณ ์์๋ดค๋๋ฐ, useRef๋ฅผ ์ฐ๋ ๋ฐฉ๋ฒ๋ ์๋๋ฐ ๋ด ์ฝ๋์ ์ ์ฉํด์ ์ฌ๊ธฐ์ ๊ธฐ ์์ ํด๋ด๋ ์ํ๋ ๋ฐฉํฅ์ผ๋ก ๊ตฌํํ ์ ์์๋ค..ใ ใ
๐ฐ. ์์ฐ ํ๋ฉด
๋ฐฐํฌํ ๋ ค๊ณ ์ด๊ฒ์ ๊ฒ ๋ง์ก๋ค๊ฐ
ํฌ๋๋ฆฌ์คํธ ์น์ด ์์ด๋ ค์...ใ ใ ..
์ฒ์๋ถํฐ ๋ค์ ๋ฆฌ์กํธ ํ์ ์๋ก ๋ง๋ค์ด์ ํ๋ํ๋ ๋ถ์ฌ๋ฃ๊ธฐ ํ๋ฉด์ ์๋ฌ ์ฐพ์๋ดค๋๋ฐ ์์ง๋ ์ด๋๊ฐ ์๋ฐ์ง ๋ชจ๋ฅด๊ณ์ใ ใ ใ ใ ใ
์๋ ํ์ ์์ Provider ์ฐ๋ฉด hooks invalid ์๋ฌ๊ฐ ๋จ๋๋ฐ ์๋ก ๋ง๋ ํ์ ์์๋ ๊ทธ๋๋ก ํด๋ ์๋จ๊ณ ..
์ง์ง ์ง์ฆ๋๋ค ์ ๋ง ใ ใ ใ
์ด์จ๋ ๋ณต๊ตฌ ์..
์ด์ ์๋ฌด๊ฒ๋ ๊ฑด๋๋ฆฌ์ง ์์ํ ์ผ ๋ฐฐํฌ ๋์ค์ ํ ๋..
'CodeStates > Training' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
S4) Unit 1. [์ค์ต] Tree & Graph ๋ฌธ์ (0) | 2023.03.15 |
---|---|
S4) Unit 1. [์ค์ต] Stack & Queue ๋ฌธ์ (0) | 2023.03.14 |
S3) Unit 7. [์ค์ต] OAuth(๊นํ๋ธ) ์ฌ์ฉํด์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (0) | 2023.03.09 |
S3) Unit 7. [์ค์ต] Token & Cookie ์ฌ์ฉํด์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (0) | 2023.03.09 |
S3) Unit 7. [์ค์ต] Cookie๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ (0) | 2023.03.07 |