Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions jivvonC/week6/6주차 할일 관리 앱.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
### 컴포넌트 분리하기

다양한 버튼에 모두 사용 가능한 버튼 컴포넌트 만들기

```tsx
//ref속성을 사용하지 않는 조건에서의 버튼 태그에서 사용할 수 있는 타입을 일괄적으로 지정할 수 있는 react 타입
type ButtonProps = React.ComponentPropsWithoutRef<"button">;
export default function Button(props: ButtonProps) {
const { children, ...rest } = props;
return (
<>
<button {...rest}>{children}</button>
</>
);
}
```

Input 컴포넌트 만들기

```tsx
//Input.tsx
//input이라는 태그에서 사용할 수 있는 모든 속성이 허용됨, 체크박스나 라디오 버튼도 가능
type ReactInputType = React.InputHTMLAttributes<HTMLInputElement>["type"];
//input으로 가능한 모든 종류에서 "type"속성만 omit으로 제거하겠다 + 체크박스 & 라디오는 올 수 없는 컴포넌트가 됨
type InputProps = Omit<React.ComponentPropsWithoutRef<"input">, "type"> & {
type?: Exclude<ReactInputType, "radio" | "checkbox">;
};

export default function Input(props: InputProps) {
const { ...rest } = props;
return (
<>
<input {...rest} />
</>
);
}
```

체크박스 컴포넌트 만들기

커스터마이징을 했기 때문에 그냥 input 컴포넌트로 다 쓰는게 아니라 따로 컴포넌트 만들기

```tsx
//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>
</>
);
}
```

TodoList 컴포넌트

```tsx
//리스트에서도 최대한 개별 아이템 컴포넌트로 분리하기
import TodoListEmpty from "./TodoListEmpty";
import TodoListItem from "./TodoListItem";

export default function TodoList() {
return (
<>
<ul className="todo__list">
{/* <!-- 할 일 목록이 없을 때 --> */}
<TodoListEmpty />
{/* <!-- 할 일 목록이 있을 때 --> */}
<TodoListItem />
</ul>
</>
);
}
```

### 할 일 등록 기능 구현하기

![image.png](attachment:e6f096f2-a804-406f-9c51-9bad3248c890:image.png)

할 일 input 입력 받기 > 제어 컴포넌트 방식, 값을 입력하면 리 렌더링이 됨

그런데 Todo Editor의 상태값을 Todo에서 제어하면 밑의 하위 컴포넌트까지 모두 다 리렌더링이 되니까
Todo Editor 컴포넌트에서만 상태를 정의하고 관리해야 다른 컴포넌트들에게 영향이 가지 않음

그래서 TodoEditor에서도 상태값 만들어서 개별 컴포넌트에 대한 리스너, 상태 업데이트도 해주고

Todo에서도 상태값 만들어서 전체 item 리스트에 대한 리스너 달아주기

엔터를 눌러도 제출, 버튼 눌러도 제출이 되도록 설정 > onSubmit 이용

```tsx
//TodoEditor.tsx
import { useState } from "react";
import Button from "./html/Button";
import Input from "./html/Input";

export default function TodoEditor({
addTodo,
}: {
addTodo: (text: string) => void;
}) {
const [text, setText] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addTodo(text);
console.log(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>
</>
);
}
```

```tsx
//Todo.tsx
import { useState } from "react";
import TodoEditor from "./TodoEditor";
import TodoHeader from "./TodoHeader";
import TodoList from "./TodoList";

export default function Todo() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
setTodos((todos) => [
...todos,
{
id: Date.now(),
text,
completed: false,
},
]);
};
return (
<>
//테스트 출력용
{/* <pre>{JSON.stringify(todos, null, 2)}</pre> */}
<div className="todo">
<TodoHeader />
{/* <!-- 할 일 등록 --> */}
<TodoEditor addTodo={addTodo} />
{/* <!-- 할 일 목록 --> */}
<TodoList />
</div>
</>
);
}
```

### 할 일 목록 렌더링 기능 구현하기

Todo interface로 만든걸 TodoList > TodoItem까지 가져오기

```tsx
//TodoListItem.tsx
import Button from "./html/Button";
import Checkbox from "./html/Checkbox";
import SvgClose from "./svg/SvgClose";
import SvgPencil from "./svg/SvgPencil";

export default function TodoListItem({ todo }: { todo: Todo }) {
return (
<>
{/* <!-- 할 일이 완료되면 .todo__item--complete 추가 --> */}
//선택적 스타일 적용(체크됨/안됨)
<li className={`todo__item ${todo.completed && "todo__item--complete"}`}>
<Checkbox
parentClassName="todo__checkbox-group"
type="checkbox"
className="todo__checkbox"
checked={todo.completed}
>
{todo.text}
</Checkbox>
{/* <!-- 할 일을 수정할 때만 노출 (.todo__checkbox-group은 비노출) --> */}
{/* <!-- <Input type="text" className="todo__modify-Input" /> --> */}
<div className="todo__button-group">
<Button className="todo__action-button">
<SvgPencil />
</Button>
<Button className="todo__action-button">
<SvgClose />
</Button>
</div>
</li>
</>
);
}
```

### 할 일 완료 및 삭제 기능 구현하기

props drilling으로 todo에서 만들어서 toggleTodo, deleteTodo 내려보내주기

```tsx
import { useState } from "react";
import TodoEditor from "./TodoEditor";
import TodoHeader from "./TodoHeader";
import TodoList from "./TodoList";

export default function Todo() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
setTodos((todos) => [
...todos,
{
id: Date.now(),
text,
completed: false,
},
]);
};

//id가 일치하는걸 찾으면 completed를 반대로 바꿔줌
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));
};
return (
<>
{/* //테스트 출력용 */}
{/* <pre>{JSON.stringify(todos, null, 2)}</pre> */}
<div className="todo">
<TodoHeader />
{/* <!-- 할 일 등록 --> */}
<TodoEditor addTodo={addTodo} />
{/* <!-- 할 일 목록 --> */}
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
</div>
</>
);
}
```

### 할일 수정 기능 구현하기

1. 수정 버튼 클릭 여부에 따라 컴포넌트 바뀌기(입력칸 + 저장으로)
> 새 상태 만들어서 수정 버튼에 클릭 리스너 달아주기
2. 입력된 값 수정 입력칸에 들어가 있어야 함
> 새 상태 만들어서 변할 때마다 todo.text로 value 채워주기
3. 새로 수정했을때 수정 내역 반영하기
> Todo.tsx에서 modifyTodo 함수 만들고 props drilling으로 내려보내주기

```tsx
//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) {
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>
</>
);
}
```

![스크린샷 2025-11-18 오후 7.07.13.png](attachment:392b2990-406d-4bcc-8525-df3f2d11cce7:스크린샷_2025-11-18_오후_7.07.13.png)