[Project] Task Kanban: React + TypeScript로 Todo 앱 만들기
타입스크립트를 익히기 위해 일주일 동안 조금씩 리액트와 타입스크립트로 간단한 task kanban을 만들어 보았다.
Repo
Code Sandbox
Stack
- React
- TypeScript
- SCSS
- react-beautiful-dnd
Features
- Task 추가, 수정(click to edit), 삭제
- Task에 우선순위 부여
- 커스텀 체크박스
- Task 완료시 자동으로 Completed로 이동
- 드래그앤드롭 칸반
- localStorage에 task 저장
- 반응형 UI
Demonstration
Task 추가하기 체크박스 눌러서 Task 완료하기 Task 드래그앤드롭 Task 내용 수정하기What I Learned
RSCSS
Reasonable System for CSS Stylesheet Structure
처음에는 BEM을 따라 CSS 클래스 이름을 지으려고 검색하다가 RSCSS라는 컨벤션도 있다는 것을 알게 되었다. BEM보다는 덜 알려진 듯 했다. (velog RSCSS 검색 결과 0건) BEM은 클래스 이름에 block, element, modifier가 다 들어가서 아주 길어지기도 한다. RSCSS는 element가 어느 component 내부에 있는지 클래스 이름에 담지 않고 자식 선택자를 사용해 표현해서 개인적으로 보기에 더 편하다고 생각돼 이번 프로젝트에 사용해보았다.
- BEM :
.block__element--modifier
- RSCSS :
.some-component >.element &.-variant
Component
- RSCSS는 각각의 UI 요소를 별개의 component로 본다. 각 component의 이름은 최소 두 단어로 이루어져야 하며 단어는
-
로 연결되어야 한다. (예:search-form
,article-card
)
Element
- Component 내부 요소로 element의 이름은 되도록이면 한 단어로, 두 단어 이상이라면
-
나_
없이 이어 표기한다. (예:title
,smalltext
) - 가능하면 자식 선택자를 사용하고, 태그 선택자는 피한다.
Variant
- Component는 variant를 가질 수 있다. Element 또한 variant를 가질 수 있다.
- Variant의 이름은
-
로 시작한다
예시
// RSCSS ----------
.article-card {
> .title { ... }
> .author { ... }
> .content { ... }
> .datecreated { ... }
> .button {
&.-transparent { ... }
&.-solid { ... }
}
}
React에서 TypeScript 사용하기
TypeScript를 사용하는 create-react-app 설치
npx create-react-app app-name --template typescript
Props 타입 지정하기
// src/models/todo
export interface Todo {
id: string;
todo: string;
isDone: boolean;
}
// Parent Component: src/components/TodoList.tsx
import React, {useState} from 'react';
import { Todo } from '../models/todo';
import TodoItem from './TodoItem';
// FC : FunctionComponent
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
return (
<ul>
{todos.map(todo) => <TodoItem todo={todo} key={todo.id} />}
</ul>
)
}
export default TodoList;
// Child Component: src/components/TodoItem.tsx
import { Todo } from '../models/todo';
interface Props {
todo: Todo;
}
const TodoItem: React.FC<Props> = ({ todo }) => {
// const TodoItem: React.FC<{todo: Todo}> = (props) => {
return (
<li>
{todo.todo}
</li>
)
}
export default TodoItem;
react-beautiful-dnd
1. 설치
npm i react-beautiful-dnd
- 타입스크립트를 사용한다면
@types/react-beautiful-dnd
도 설치해주어야 한다.
npm i @types/react-beautiful-dnd
2. DragDropContext로 드래그앤드롭을 사용할 영역 감싸주기
- 드래그앤드롭을 사용하려면 드래그앤드롭이 일날 React 트리를
<DragDropContext>
로 래핑해야한다. - 전체 어플리케이션을
<DragDropContext>
로 래핑하는 것이 좋다.
// src/App.tsx
import React, { useState } = 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import TodoList from './components/TodoList';
import { Todo } from '../models/todo';
/*
interface Todo{
id: string;
todo: string;
isDone: boolean;
}
*/
const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [completed, setCompleted] = useState<Todo[]>([]);
return (
<DragDropContext>
<div className='app'>
<TodoList todos={todos} completed={completed}/>
</div>
</DragDropContext>
);
}
export default App;
3. Droppable로 아이템을 드롭할 영역 감싸주기
- 드롭이 일어날 영역을
<Droppable>
로 래핑한다. droppableId
가 반드시 있으여 하며 이 string id로 각<Droppable>
를 구분한다.<Droppable>
의children
은(provided, snapshot) => ReactNode
형태의 함수다.<Droppable>
이 정상적으로 동작하기 위해서는 droppable로 간주될ReactElement
최상위 DOM 노드에provided.innerRef
를 바인드 해야한다.provided.droppableProps
객체는<Droppable>
에 필요한 프로퍼티들이 담겨 있다.provided.innerRef
를 바인딩한 요소에 적용해야 한다.provided.placeholder
는 드래그가 일어나는 도중<Droppable>
내에 빈 공간을 만들기 위해 사용된다.provided.innerRef
를 바인딩한 요소 내부에 사용해야 한다.
// src/components/TodoList.tsx
import { Droppable } from 'react-beautiful-dnd';
import TodoItem from './TodoItem';
import { Todo } from '../models/todo';
interface Props {
todos: Todo[];
completed: Todo[];
}
const TodoList: React.FC = ({todos, completed}) => {
return (
<div className='kanban'>
<div className='column'>
<h2>Inbox</h2>
<Droppable droppableId='inbox-column'>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
<ul className='todo-list'>
{todos.map(todo, index) => (
<TodoItem
index={index}
todo={todo}
key={todo.id}
/>
)}
{provided.placeholder}
</ul>
</div>
)}
</Droppable>
</div>
<div className='column'>
<h2>Completed</h2>
<Droppable droppableId='completed-column'>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
<ul className='todo-list'>
{completed.map(todo, index) => (
<TodoItem
index={index}
todo={todo}
key={todo.id}
/>
)}
{provided.placeholder}
</ul>
</div>
)}
</Droppable>
</div>
</div>
)
}
export default TodoList;
4. Draggable로 드래그할 요소 감싸주기
- 드래그할 요소를
<Draggable>
로 래핑한다. draggableId
가 반드시 있으여 하며 이 string id로 각<Draggable>
를 구분한다.index
가 반드시 있어야 하며 이는<Droppable>
내에서<Draggable>
의 인덱스와 일치한다.<Draggable>
의children
은(provided, snapshot) => ReactNode
형태의 함수다.<Draggable>
이 정상적으로 동작하기 위해서는 draggable로 간주될ReactElement
에provided.innerRef
를 바인드 해야한다.provided.draggableProps
객체는<Draggable>
의 드래그 동작에 필요한 프로퍼티들이 담겨 있다.provided.innerRef
를 바인딩한 요소에 적용해야 한다.provided.dragHandleProps
는<Draggable>
전체를 드래그하기 위한 핸들이다. 드래그 핸들이 될 노드에 적용한다.
// src/components/TodoItem
import { Draggable } from 'react-beautiful-dnd';
import { Todo } from '../models/todo';
interface Props {
index: number;
todo: Todo;
}
const TodoItem: React.FC = ({index, todo}) => {
return (
<Draggable draggableId={todo.id} index={index}>
{(provided) => (
<li
className='todo-item'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{todo.todo}
</li>
)}
</Draggable>
)
}
export default TodoItem;
5. onDragEnd 함수
<DragDropContext>
에onDragEnd
가 없으면 에러가 발생한다.onDragEnd
는 첫번째 인자로 드롭 결과가 담긴DropResult
를 받는다.onDragEnd
함수에서 드래그앤드롭 이후 draggables 리스트의 순서를 재정렬해주어야 한다.
// src/App.tsx
...
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
...
const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [completed, setCompleted] = useState<Todo[]>([]);
// 드래그가 끝났을 때의 동작을 지정해주는 함수
const onDragEnd = (result: DropResult) => {
const { source, destination } = result;
// 드롭이 droppable 밖에서 일어났을 경우 바로 return
if (!destination) return;
// 드래그가 발생한 위치와 드롭이 발생한 위치가 같을 경우 바로 return
if (source.droppableId === destination.droppableId && source.index === destination.index) return;
let add: Todo;
let newTodo = todos;
let newCompleted = completed;
// 드래그가 발생한 아이템을 add에 담고 원래 자리에서 제거
if (source.droppableId === 'inbox-column') {
add = newTodo[source.index];
newTodo.splice(source.index, 1);
} else {
add = newCompleted[source.index];
newCompleted.splice(source.index, 1);
}
// 드롭이 발생한 곳에 add를 넣어줌
if (destination.droppableId === 'inbox-column') {
newTodo.splice(destination.index, 0, {...add, isDone: false});
} else {
completed.splice(destination.index, 0, {...add, isDone: true});
}
// todos와 completed 업데이트
setTodos(newTodos);
setCompleted(newCompleted);
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<div className='app'>
<TodoList todos={todos} completed={completed}/>
</div>
</DragDropContext>
);
}
export default App;
useLocalStorage 커스텀훅
import { useState } from "react";
// useLocalStorage는 string 타입의 key와 T 타입의 initialValue를 매개변수로 가진다.
// T는 제네릭으로 useLocalStorage가 호출될 대 initialValue로 어떤 타입을 받느냐에 따라 변한다. 예를 들어 useLocalStorage('count', 0)일 때 T는 number 타입이 되고, useLocalStorate('username', 'guest')일 때 T는 string 타입이 된다.
// useLocalStorage는 [T, (s: T) => void] 타입의 값을 리턴한다.
export const useLocalStorage = <T>(key: string, initialValue: T): [T, (s: T) => void] => {
// localStorage에 주어진 key로 저장된 값이 있다면 그 값이, 없다면 initialValue가 반환된다.
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (err) {
return initialValue;
}
});
const setValue = (value: T) => {
try {
// setValue가 인자로 함수를 받으면 그 함수에 storedValue를 넣어 실행한 결과 값이, 함수가 아닌 값을 받으면 그 값이 localStorage에 저장된다.
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (err) {
console.log(err);
}
}
// useState와 유사하게 저장된 값과 그 값을 바꾸기 위한 함수가 담긴 배열이 반환된다.
return [storedValue, setValue];
}
Author And Source
이 문제에 관하여([Project] Task Kanban: React + TypeScript로 Todo 앱 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@alexjlee/Project-Task-Kanban-React-TypeScript로-Todo-앱-만들기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)