📢 오늘은 React + Recoil + TypeScript 스택으로 ToDoList를 만들어 볼거에요!
만들기 전 제가만든 ToDoList의 파일구조는 이렇습니다!
📦 React + Recoil + Typescript (src)
├─ components
│ ├─ TodoInput
│ │ ├─ todoInput.tsx
│ │ ├─ todoInput.scss
│ │ └─ index.ts
│ ├─ TodoFilter
│ │ ├─ todoFilter.tsx
│ │ ├─ todoFilter.scss
│ │ └─ index.ts
│ ├─ TodoItem
│ │ ├─ todoItem.tsx
│ │ ├─ todoItem.scss
│ │ └─ index.ts
│ └─ TodoList
│ ├─ todoList.tsx
│ ├─ todoList.scss
│ └─ index.ts
├─ recoil
│ └─ todosState.ts
├─ types
│ └─ todoType.ts
├─ templates
│ ├─ todoTemplate.tsx
│ ├─ todoTemplate.scss
│ └─ index.ts
├─ app.tsx
└─ index.tsx
© generated by woochanlee
Recoil을 설치하기 위해선 터미널에서 React폴더로 가서 아래 코드를 치면 됩니다 :)
$ Yarn add recoil 또는 npm install recoil
설치를 했다면 src/index.tsx
에서 아래와 같이 설정해줍시다!
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { RecoilRoot } from "recoil";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
각 폴더의 index.tsx
에는 아래와 같이 적어주시면 됩니다!
export { default } from "[컴포넌트명].tsx";
먼저 ToDoList를 위한 전역상태들을 Recoil
로 만들어주고 그에 맞는 타입
들을 만들어주겠습니다.
types/todoType.ts
export interface TodoType { id: number; done: boolean; contents: string; } export interface TodoPropTypes { data: TodoType; onDelete: (id: number) => void; onDone: (id: number) => void; }
TodoType
- 사용자로 부터 Todo를 받아올 때 쓰입니다.
TodoPropTyes
- TodoItem.tsx에서 각 Todo에 쓰일 Props들의 타입입니다.
recoil/todosState.ts
import { atom, selector } from "recoil"; import { TodoType } from "types/todoType"; export const todosState = atom<TodoType[]>({ key: "todosState", default: [ { id: 1, done: true, contents: "레코일 공부하기!", }, { id: 2, done: false, contents: "투두리스트 만들어보기!", }, { id: 3, done: false, contents: "놀기!", }, ], }); export const todoInputState = atom<string>({ key: "todoInputState", default: "", }); export const filterTodosState = atom<string>({ key: "filterTodosState", default: "All", }); export const filterTodosSelector = selector({ key: "filterTodosSelector", get: ({ get }) => { const todos = get(todosState); const filter = get(filterTodosState); switch (filter) { case "Done": return todos.filter((todo) => { return todo.done !== false; }); case "UnDone": return todos.filter((todo) => { return todo.done !== true; }); default: // All return todos; } }, });
todosState
- 사용자로 부터 Todo를 받아와서 저장하는 atom
입니다.
추후 디자인할 때 편하게 하기위해 저는 default 값에 3개의 Todo를 임의로 넣어주겠습니다.
todoInputState
- 사용자가 Todo를 입력할 때 그 값을 전역으로 관리해주기 위한 atom
입니다.
filterTodosState
- 사용자가 어떤 Todo를 볼것인지 정했을 때 그 값을 저장하는 atom
입니다.
default는 All(모두보기)
으로 주겠습니다.
filterTodosSelector
- 사용자가 정한 filter에 맞는 Todo들을 반환해주는 selector
입니다.
각 필터명에 맞게 Switch-Case
를 써서 Todo를 반환했습니다.
이제 사용자가 어떤 Todo를 볼것인지 확인할 TodoFilter
를 만들겠습니다.
components/TodoFilter/todoFilter.tsx
import { filterTodosState } from "recoil/todosState"; import { useSetRecoilState } from "recoil"; import "./todoFilter.scss"; const TodoFilter = (): JSX.Element => { const setFilter = useSetRecoilState(filterTodosState); const onChangeFilter = (e: any) => { const { value } = e.target; setFilter(value); }; return ( <select className={"todoSelector"} name="filter" onChange={onChangeFilter}> <option value="All">All</option> <option value="Done">Done</option> <option value="UnDone">UnDone</option> </select> ); }; export default TodoFilter;
onChangeFilter
함수로 사용자가 select box
에서 어떤 값을 선택했는지 확인하고 state
에 string
으로 set해줍니다!
디자인은 이렇게 했습니다
components/TodoFilter/todoFilter.scss
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap"); .todoSelector { display: block; width: 80px; border: none; margin-top: -4px; margin-bottom: 8px; border-radius: 3px; font-family: "Noto Sans KR", sans-serif; padding-left: 3px; text-align-last: center; text-align: center; appearance: none; box-shadow: 0.8px 0.8px 3px grey; }
타입
및 atom & Selector
를 다 만들었으니 이제 사용자가 Todo를 입력할 Input
을 만들어 볼게요
components/TodoInput/todoInput.tsx
import { useRecoilState } from "recoil"; import { todosState, todoInputState } from "recoil/todosState"; import { TodoType } from "types/todoType"; import TodoFilter from "components/TodoFilter"; import { IoCheckmarkSharp } from "react-icons/io5"; import "./todoInput.scss"; const TodoInput = (): JSX.Element => { const [input, setInput] = useRecoilState<string>(todoInputState); const [todos, setTodo] = useRecoilState<TodoType[]>(todosState); const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>):void => { const { value } = e.target; setInput(value); }; const onEnter = (e: React.KeyboardEvent<HTMLInputElement>): void => { if (e.key === "Enter") { if (input.length !== 0) { const id = todos.length ? todos[todos.length - 1].id + 1 : 0; const todo = { id, done: false, contents: input, }; setTodo([...todos, todo]); setInput(""); } } }; const addTodo = ():void => { if (input.length !== 0) { const id = todos.length ? todos[todos.length - 1].id + 1 : 0; const todo = { id, done: false, contents: input, }; setTodo([...todos, todo]); setInput(""); } }; return ( <> <div className={"todoInput"}> <TodoFilter /> <input className={"todoInput-Input"} type="text" value={input} onChange={onChangeInput} onKeyPress={onEnter} placeholder={"Write a ToDo"} /> <div className={"todoInput-btn"} onClick={addTodo}> <IoCheckmarkSharp className={"todoInput-icon"} /> </div> </div> </> ); }; export default TodoInput;
onChangeInput
함수로 사용자가 Input
창에 값을 입력하면 자동으로 state
에 값이 넣어지고,
onEnter
함수 및 addTodo
함수로 사용자가 엔터를 누르거나 입력버튼을 눌렀을 때 Todo가 생성되게 했습니다.
디자인은 아래와 같이 했습니다.
components/TodoInput/todoInput.scss
.todoInput { margin: 20px; &-Input { padding: 0px; margin: 0px; width: 345px; height: 53px; font-size: 1.5rem; justify-content: center; text-align: center; align-items: center; border: none; border-radius: 3px; outline: none; padding: 5px; box-shadow: 0.8px 0.8px 3px grey; } &-Input::-webkit-input-placeholder { text-align: center; } &-btn { margin-left: 12px; color: white; justify-content: center; text-align: center; line-height: 54px; font-size: 1.3rem; width: 64px; height: 54px; display: inline-block; cursor: pointer; background-color: #5c7cfa; border-radius: 5px; box-shadow: 1.2px 1.2px 3px gray; } &-icon { margin-top: -5px; vertical-align: middle; width: 28px; height: 28px; } }
Input
도 다 만들었으니 이제 각 Todo를 보여줄 TodoItem
과 Todo들 모두를 보여줄 TodoList
를 만들겠습니다.
components/TodoList/todoList.tsx
import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil"; import { filterTodosSelector } from "recoil/todosState"; import TodoItem from "components/TodoItem"; import { todosState } from "recoil/todosState"; import { TodoType } from "types/todoType"; import "./todoList.scss"; const TodoList = (): JSX.Element => { const filteredTodos = useRecoilValue(filterTodosSelector); const [todos, setTodos] = useRecoilState<TodoType[]>(todosState); const onDelete = (id: number): void => { setTodos( todos.filter((todo) => { return todo.id !== id; }) ); }; const onDone = (id: number): void => { setTodos( todos.map((todo) => { return todo.id === id ? { ...todo, done: !todo.done } : todo; }) ); }; return ( <div className={"todoitemList"}> {filteredTodos.map((todo) => { const data = { id: todo.id, done: todo.done, contents: todo.contents, }; return <TodoItem data={data} onDelete={onDelete} onDone={onDone} />; })} </div> ); }; export default TodoList;
onDelete
와 onDone
함수로 삭제 및 Todo를 클릭하면 done의 값을 반대로 바뀌게 했습니다.
todoList에서 map해서 리턴하는 todo들은 모두 사용자가 select box에서 정한 값에 따라 바뀝니다.
ex) All , Done, UnDone
components/TodoItem/todoItem.scss
import { TodoPropTypes } from "types/todoType"; import "./todoItem.scss"; import { TiDelete } from "react-icons/ti"; const TodoItem = ({ data, onDelete, onDone }: TodoPropTypes): JSX.Element => { const { id, done, contents } = data; return ( <div className={done ? "todoItem-done" : "todoItem"}> <div className={done ? "todoItem-title-done" : "todoItem-title"} onClick={() => onDone(id)} {contents} </div> <div className={"todoItem-delete"} onClick={() => onDelete(id)}> <TiDelete className={"todoItem-delIcon"} /> </div> </div> ); }; export default TodoItem;
TodoList
에서 Props로 onDelete
함수와 onDone
함수, 각 todo의 data
를 받아옵니다.
done
값에 따라 className이 바뀌게 했고 todo를 클릭하면 onDone함수에 해당 Todo의 id를 파라미터로넘겨주고 삭제할시에는 onDelete함수에 id를 파라미터로 넣어 실행합니다.
디자인은 아래와 같이 했습니다.
components/TodoItem/todoItem.scss
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap"); .todoItem { display: flex; justify-content: space-between; text-align: center; align-items: center; font-size: 1.2rem; margin-bottom: 8px; background-color: rgba(116, 143, 252, 0.65); margin-left: 20px; margin-right: 20px; padding: 12px; border-radius: 4px; box-shadow: 1px 1px 3px grey; transition: all 0.5s; &-delIcon { cursor: pointer; width: 34px; height: 34px; transition: 0.7s; } &-delIcon:hover { color: white; transform: rotate(90deg); transform-origin: center center; transition: 0.7s; } &-delete { margin: 0px; padding: 0px; width: auto; height: auto; } &-title { font-family: "Noto Sans KR", sans-serif; font-weight: 500; cursor: pointer; margin-left: 40px; } &-title-done { font-family: "Noto Sans KR", sans-serif; font-weight: 500; cursor: pointer; margin-left: 40px; color: rgba(0, 0, 0, 0.5); text-decoration: line-through; } } .todoItem-done { display: flex; justify-content: space-between; text-align: center; align-items: center; font-size: 1.2rem; margin-bottom: 8px; background-color: rgba(255, 168, 168, 0.65); margin-left: 20px; margin-right: 20px; padding: 12px; border-radius: 4px; box-shadow: 1px 1px 3px grey; transition: 0.5s; }
components/TodoList/todoList.scss
.todoitemList { height: 400px; }
이제 각 컴포넌트들을 모두 가져와서 TodoTemplate
로 만들겠습니다!
templates/todoTemplate.tsx
import TodoInput from "components/TodoInput"; import TodoList from "components/TodoList"; import "./todoTemplate.scss"; const TodoTemplate = (): JSX.Element => { return ( <div className={"todoTemplate"}> <TodoInput /> <TodoList /> </div> ); }; export default TodoTemplate;
templates/todoTemplate.scss
@import url("https://fonts.googleapis.com/css2?family=Nanum+Gothic+Coding&display=swap"); * { font-family: "Nanum Gothic Coding", monospace; user-select: none; } .todoTemplate { position: absolute; left: 50%; bottom: 0px; transform: translate(-50%, -50%); margin: 0px; padding: 0px; background-color: #dbe4ff; border-radius: 8px; box-shadow: 1px 1px 4px grey; overflow-y: scroll; }
이제 마지막으로 App.tsx
에서 가져와서 보여주겠습니다!
index.tsx
import TodoTemplate from "templates"; function App(): JSX.Element { return <TodoTemplate />; } export default App;
요즘들어 취업에 대한 고민이 많아지면서 저의 취약점이 무었인지 곰곰히 생각해봤습니다.
제 최대 취약점은 무에서 유를 창조하는것이라는것을 알게되었고,
이를 해결하기위해선 무언가를 공부하면 꼭 내 힘으로 결과물을 만들어야겠다고 생각해게 되었습니다.
투두리스트 말고도 앞으로 무언가를 더 만들어야겠어요! 😎
덕분에 많은것을 알아갑니다! ㅎㅎ