[패스트캠퍼스 수강 후기] 프론트엔드 인강 100% 환급 챌린지 44회차 미션 - 32강 TodoList만들기 2
이번에는 TodoList에 기능을 붙여보도록 하자. 먼저 App안에 기능을 다 만들어서 컴포넌트를 호출하는 것이 아니라 TodoContect라는 컴포넌트를 만들어서 기능을 관리하게 해줄것이다.
역시나 자세하고 정확한 코드는 벨로퍼트의 위키를 참고하도록 하자. 먼저 상태를 관리할 TodoContext를 만들자.
TodoContext.js
import React, { createContext, useReducer, useContext, useRef } from "react";
const initialTodos = [
{
id: 1,
text: "프로젝트 생성하기",
done: true,
},
{
id: 2,
text: "프로젝트 생성하기",
done: false,
},
{
id: 3,
text: "프로젝트 생성하기",
done: true,
},
{
id: 4,
text: "프로젝트 생성하기",
done: true,
},
];
// CREATE, TOGGLE, REMOVE
function todoReducer(state, action) {
switch (action.type) {
case "CREATE":
return state.concat(action.todo);
case "TOGGLE":
return state.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case "REMOVE":
return state.filter((todo) => todo.id !== action.id);
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
const TodoNextIdContext = createContext();
export function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialTodos);
const nextId = useRef(5);
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
<TodoNextIdContext.Provider value={nextId}>
{children}
</TodoNextIdContext.Provider>
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
export function useTodoState() {
const context = useContext(TodoStateContext);
if (!context) {
throw new Error("Cannot find TodoProvider");
}
return context;
}
export function useTodoDispatch() {
const context = useContext(TodoDispatchContext);
if (!context) {
throw new Error("Cannot find TodoProvider");
}
return context;
}
export function useNextId() {
const context = useContext(TodoNextIdContext);
if (!context) {
throw new Error("Cannot find TodoProvider");
}
return context;
}
각 컴포넌트에 상태관리 및 기능을 붙여보자.
TodoList.js
import React from "react";
import styled from "styled-components";
import TodoItem from "./TodoItem";
import { useTodoState } from "./TodoContext";
const TodoListBlock = styled.div`
flex: 1;
padding: 20px 32px;
padding-bottom: 48px;
overflow-y: auto;
`;
function TodoList() {
const todos = useTodoState();
return (
<TodoListBlock>
{todos.map((todo) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
done={todo.done}
/>
))}
</TodoListBlock>
);
}
export default TodoList;
TodoItem.js
import React from "react";
import styled, { css } from "styled-components";
import { MdDone, MdDelete } from "react-icons/md";
import { useTodoDispatch } from "./TodoContext";
const Remove = styled.div`
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
color: #dee2e6;
font-size: 24px;
cursor: pointer;
&:hover {
color: #ff6b6b;
}
`;
const CheckCircle = styled.div`
width: 32px;
height: 32px;
border-radius: 16px;
border: 1px solid #ced4da;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20px;
cursor: pointer;
/* 나중에 CheckCircle에게 done값을 던져줄건데 그때 true면 이것을 보여줘라 */
${(props) =>
props.done &&
css`
border: 1px solid #38d9a9;
color: #38d9a9;
`}
`;
const Text = styled.div`
flex: 1;
font-size: 21px;
color: #495057;
${(props) =>
props.done &&
css`
color: #ced4da;
`}
`;
const TodoItemBlock = styled.div`
display: flex;
align-items: center;
padding-top: 12px;
padding-bottom: 12px;
&:hover {
${Remove} {
/* TodoItemBlock에 hover시 Remove를 보이도록 */
opacity: 1;
}
}
`;
function TodoItem({ id, done, text }) {
const dispatch = useTodoDispatch();
const onToggle = () =>
dispatch({
type: "TOGGLE",
id,
});
const onRemove = () =>
dispatch({
type: "REMOVE",
id,
});
return (
<TodoItemBlock>
<CheckCircle done={done} onClick={onToggle}>
{done && <MdDone />}
</CheckCircle>
<Text done={done}>{text}</Text>
<Remove onClick={onRemove}>
<MdDelete />
</Remove>
</TodoItemBlock>
);
}
export default React.memo(TodoItem);
TodoHead.js
import React from "react";
import styled from "styled-components";
import { useTodoState } from "./TodoContext";
const TodoHeadBlock = styled.div`
padding-top: 48px;
padding-left: 32px;
padding-right: 32px;
padding-bottom: 24px;
h1 {
margin: 0;
font-size: 36px;
color: #343a40;
}
.day {
margin-top: 4px;
color: #868e96;
font-size: 21px;
}
.tasks-left {
color: #20c997;
font-size: 18px;
margin-top: 40px;
font-weight: bold;
}
`;
function TodoHead() {
const todos = useTodoState();
const undoneTasks = todos.filter((todo) => !todo.done);
const today = new Date();
const date = today.toLocaleDateString("ko-KR", {
year: "numeric",
month: "long",
day: "numeric",
});
const dayName = today.toLocaleDateString("ko-KR", {
weekday: "long",
});
return (
<TodoHeadBlock>
<h1>{date}</h1>
<div className="day">{dayName}</div>
<div className="tasks-left">할일 {undoneTasks.length}개 남음</div>
</TodoHeadBlock>
);
}
export default TodoHead;
TodoCreate.js
import React, { useState } from "react";
import styled, { css } from "styled-components";
import { MdAdd } from "react-icons/md";
import { useNextId, useTodoDispatch } from "./TodoContext";
const CircleButton = styled.button`
background: #38d9a9;
&:hover {
background: #63e6be;
}
&:active {
background: #20c997;
}
z-index: 5;
cursor: pointer;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 50%;
bottom: 0px;
transform: translate(-50%, 50%); /* 자신의 너비의 반만큼 */
font-size: 60px;
color: white;
border-radius: 40px;
border: none;
outline: none;
transition: 0.2s all;
${(props) =>
props.open &&
css`
background: #ff6b6b;
&:hover {
background: #ff8787;
}
&:active {
background: #ff6b6b;
}
transform: translate(-50%, 50%) rotate(45deg);
`}
`;
const InsertFormPositioner = styled.div`
width: 100%;
bottom: 0;
left: 0;
position: absolute;
`;
const InserForm = styled.form`
background: #f8f9fa;
padding: 32px;
padding-bottom: 72px;
border-radius: 0 0 16px 16px;
border-top: 1px solid #e9ecef;
`;
const Input = styled.input`
padding: 12px;
border-radius: 4px;
border: 1px solid #dee2e6;
width: 100%;
outline: none;
font-size: 18px;
box-sizing: border-box;
`;
function TodoCreate() {
const [open, setOpen] = useState(false);
const [value, setValue] = useState("");
const dispatch = useTodoDispatch();
const nextId = useNextId();
const onToggle = () => setOpen(!open);
const onChange = (e) => setValue(e.target.value);
const onSubmit = (e) => {
e.preventDefault();
dispatch({
type: "CREATE",
todo: {
id: nextId.current,
text: value,
done: false,
},
});
setValue("");
setOpen(false);
nextId.current += 1;
};
return (
<>
{open && (
<InsertFormPositioner>
<InserForm onSubmit={onSubmit}>
<Input
placeholder="할일을 입력 후 엔터를 눌러주세요."
autoFocus
onChange={onChange}
value={value}
/>
</InserForm>
</InsertFormPositioner>
)}
<CircleButton onClick={onToggle} open={open}>
<MdAdd />
</CircleButton>
</>
);
}
export default React.memo(TodoCreate);
이제 기능을 모두 붙였다. 잘 되나 확인해보자.
잘된다. 신기할정도로 잘된다. 왜냐면 내가 이해를 못했기 때문이다. 그래도 에러 없이 잘되서 다행이다. 이 TodoList만드는거는 몇번 더 돌리면서 복습해봐야겠다. 이것만 자유자재로 해도 리액트로 웬만한 프론트앤드 개발은 다 할 수 있을것같은 (근거없는) 자신감이 든다…..!
오늘은 여기까지..
시청 영상 32강 04~05까지
프론트엔드 개발 올인원 패키지 with React Online. 👉 https://bit.ly/31Cf1hp
Comments