// App.tsx
import Todo from "./components/Todo";
export default function App() {
return (
<>
<Todo />
</>
);
}
// src/components/Todo.tsx
import { useState } from "react";
import TodoEditer from "./TodoEditer";
import TodoHeader from "./TodoHeader";
import TodoList from "./TodoList";
export default function Todo() {
const [todos, setTodos] = useState<Todo[]>([]); // 배열로 선언
const addTodo = (text: string) => {
// text만 매개변수로 전달받음
setTodos((todos) => [
...todos,
{
id: Date.now(),
text: text,
completed: false,
},
]);
};
const toggleTodo = (id: number) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
);
};
const deleteTodo = (id: number) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const modifyTodo = (id: number, text: string) => {
setTodos((todos) => todos.map((todo) => todo.id === id ? {...todo, text} : todo))
}
return (
<>
<div className="todo">
<TodoHeader />
{/* <!-- 할 일 등록 --> */}
<TodoEditer addTodo={addTodo} />
{/* <!-- 할 일 목록 --> */}
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
modifyTodo={modifyTodo}
/>
</div>
</>
);
}
// src/components/TodoHeader.tsx
export default function TodoHeader () {
return (
<>
<h1 className="todo__title">Todo List</h1>
<p className="todo__subtitle">Please enter your details to continue.</p>
</>
);
}
// src/components/TodoEditor.tsx
import { useState } from "react";
import Button from "./html/Button";
import Input from "./html/Input";
export default function TodoEditer({
addTodo,
}: {
addTodo: (text: string) => void;
}) {
const [text, setText] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim() === "") { // 빈 문자열을 입력했을 때는 return
return;
}
addTodo(text);
setText("");
};
return (
<>
<form className="todo__form" onSubmit={handleSubmit}>
<div className="todo__editor">
<Input
type="text"
className="todo__input"
placeholder="Enter Todo List"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Button className="todo__button" type="submit">
Add
</Button>
</div>
</form>
</>
);
}
// src/components/TodoListEmpty.tsx
export default function TodoListEmpty() {
return (
<>
<li className="todo__item todo__item--empty">
<p className="todo__text--empty">There are no registered tasks</p>
</li>
</>
);
}
// src/components/TodoList.tsx
import TodoListEmpty from "./TodoListEmpty";
import TodoListItem from "./TodoListItem";
export default function TodoList({
todos,
toggleTodo,
deleteTodo,
modifyTodo
}: {
todos: Todo[];
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
modifyTodo: (id: number, text: string) => void;
}) {
return (
<>
<ul className="todo__list">
{/* <!-- 할 일 목록이 없을 때 --> */}
{todos.length === 0 && <TodoListEmpty />}
{/* <!-- 할 일 목록이 있을 때 --> */}
{todos.map((todo) => (
<TodoListItem key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} modifyTodo={modifyTodo} />
))}
</ul>
</>
);
}
// TodoListItem.tsx
import { useState } from "react";
import Button from "./html/Button";
import Checkbox from "./html/Checkbox";
import Input from "./html/Input";
import SvgClose from "./svg/SvgClose";
import SvgPencil from "./svg/SvgPencil";
export default function TodoListItem({
todo,
toggleTodo,
deleteTodo,
modifyTodo
}: {
todo: Todo;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
modifyTodo: (id: number, text: string) => void;
}) {
const [isModify, setIsModify] = useState(false);
const [modifyText, setModifyText] = useState("");
const modifyHandler = () => {
setIsModify((isModify) => !isModify);
setModifyText((modifyText) => modifyText === "" ? todo.text : modifyText);
if (modifyText.trim() !== "" && todo.text !== modifyText) { // todo의 text와 수정 모드에서 입력한 text가 다를 때 --> 수정
modifyTodo(todo.id, modifyText);
}
};
return (
<>
{/* <!-- 할 일이 완료되면 .todo__item--complete 추가 --> */}
<li className={`todo__item ${todo.completed && "todo__item--complete"}`}>
{!isModify && (
<Checkbox
parentClassName="todo__checkbox-group"
type="checkbox"
className="todo__checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
>
{todo.text}
</Checkbox>
)}
{/* <!-- 할 일을 수정할 때만 노출 (.todo__checkbox-group은 비노출) --> */}
{isModify && (
<Input
type="text"
className="todo__modify-input"
value={modifyText}
onChange={(e) => setModifyText(e.target.value)}
/>
)}
<div className="todo__button-group">
<Button className="todo__action-button" onClick={modifyHandler}>
<SvgPencil />
</Button>
<Button
className="todo__action-button"
onClick={() => deleteTodo(todo.id)}
>
<SvgClose />
</Button>
</div>
</li>
</>
);
}
// src/components/html/Button.tsx
// HTML button이 가진 모든 속성(props)를 그대로 빌려오겠다는 선언
// ref 속성까지 가져오면 오류가 날 수 있기 때문에 WithoutRef
type ButtonProps = React.ComponentPropsWithoutRef<"button">;
export default function Button (props: ButtonProps) {
// children : 태그 사이의 내용
// ...rest : props 중 children을 뺀 나머지 모든 속성을 담은 객체
const {children, ...rest} = props;
return (
<>
<button {...rest}>{children}</button>
</>
);
}
// src/components/html/Checkbox.tsx
type CheckboxProps = Omit<React.ComponentPropsWithoutRef<"input">, "type"> & {
type?: "checkbox";
parentClassName: string;
};
export default function Checkbox(props: CheckboxProps) {
const { parentClassName, children, ...rest } = props;
return (
<>
<div className={ parentClassName }>
<input {...rest} />
<label>{ children }</label>
</div>
</>
);
}
// src/components/html/Input.tsx
type ReactInputType = React.InputHTMLAttributes<HTMLInputElement>["type"];
type InputProps = Omit<React.ComponentPropsWithoutRef<"input">, "type"> & {
type?: Exclude<ReactInputType, "radio" | "checkbox">;
};
export default function Input(props: InputProps) {
const { ...rest } = props;
return (
<>
<input {...rest} />
</>
);
}
Checkbox.tsx
Omit<..., "type">
^ input 태그가 원래 가지고 있는 수많은 속성 중 'type'만 빼겠다는 의미
∵ 누군가 해당 컴포넌트를 가져다쓰면서 <Checkbox type="text" />라고 실수하는 것을 막기 위해
& { type?: "checkbox"; ... }
^ 'type' 자리에 checkbox라는 값만 들어올 수 있도록 엄격하게 제한
ㄴ 특정 타입만을 허용하는 것
Input.tsx
type ReactInputType = React.InputHTMLAttributes<HTMLInputElement>["type"]
^ 브라우저 인풋이 가질 수 있는 모든 type(text, password, email, radio, checkbox 등)을 한데 모은 거대한 목록
Exclude<..., "radio" | "checkbox">
^ 거대한 목록에서 radio와 checkbox만 뺀 새로운 목록을 만듦
∴ Input 컴포넌트는 text, password, number 등은 자유롭게 쓸 수 있지만, type="radio"와 같이 넣으려고 하면 에러 발생
ㄴ 특정 타입만을 허용하는 방식이 아닌, 특정 타입들을 배제하는 방식
'React > 타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지' 카테고리의 다른 글
| 섹션 13. 전역 상태 관리 - Context API (0) | 2026.06.09 |
|---|---|
| 섹션 12. 사이드 이펙트와 컴포넌트 최적화 (0) | 2026.05.22 |
| 섹션 10. 폼 다루기 (0) | 2026.05.07 |
| 섹션 9. (프로젝트) 계산기 만들기 (0) | 2026.05.02 |
| 섹션 8. 컴포넌트 스타일링 (2) (0) | 2026.04.21 |