Skip to content

Commit 44b4df9

Browse files
authored
Merge pull request #24 from IT-Cotato/jivvonC/week6
[6주차] 전지원
2 parents e1c9e5a + 87e2e19 commit 44b4df9

File tree

1 file changed

+349
-0
lines changed

1 file changed

+349
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
### 컴포넌트 분리하기
2+
3+
다양한 버튼에 모두 사용 가능한 버튼 컴포넌트 만들기
4+
5+
```tsx
6+
//ref속성을 사용하지 않는 조건에서의 버튼 태그에서 사용할 수 있는 타입을 일괄적으로 지정할 수 있는 react 타입
7+
type ButtonProps = React.ComponentPropsWithoutRef<"button">;
8+
export default function Button(props: ButtonProps) {
9+
const { children, ...rest } = props;
10+
return (
11+
<>
12+
<button {...rest}>{children}</button>
13+
</>
14+
);
15+
}
16+
```
17+
18+
Input 컴포넌트 만들기
19+
20+
```tsx
21+
//Input.tsx
22+
//input이라는 태그에서 사용할 수 있는 모든 속성이 허용됨, 체크박스나 라디오 버튼도 가능
23+
type ReactInputType = React.InputHTMLAttributes<HTMLInputElement>["type"];
24+
//input으로 가능한 모든 종류에서 "type"속성만 omit으로 제거하겠다 + 체크박스 & 라디오는 올 수 없는 컴포넌트가 됨
25+
type InputProps = Omit<React.ComponentPropsWithoutRef<"input">, "type"> & {
26+
type?: Exclude<ReactInputType, "radio" | "checkbox">;
27+
};
28+
29+
export default function Input(props: InputProps) {
30+
const { ...rest } = props;
31+
return (
32+
<>
33+
<input {...rest} />
34+
</>
35+
);
36+
}
37+
```
38+
39+
체크박스 컴포넌트 만들기
40+
41+
커스터마이징을 했기 때문에 그냥 input 컴포넌트로 다 쓰는게 아니라 따로 컴포넌트 만들기
42+
43+
```tsx
44+
//Checkbox.tsx
45+
46+
//체크박스만 가능한 타입
47+
type CheckboxProps = Omit<React.ComponentPropsWithoutRef<"input">, "type"> & {
48+
type?: "checkbox";
49+
parentClassName: string;
50+
};
51+
export default function Checkbox(props: CheckboxProps) {
52+
const { parentClassName, children, ...rest } = props;
53+
return (
54+
<>
55+
<div className={parentClassName}>
56+
<input {...rest} />
57+
<label>{children}</label>
58+
</div>
59+
</>
60+
);
61+
}
62+
```
63+
64+
TodoList 컴포넌트
65+
66+
```tsx
67+
//리스트에서도 최대한 개별 아이템 컴포넌트로 분리하기
68+
import TodoListEmpty from "./TodoListEmpty";
69+
import TodoListItem from "./TodoListItem";
70+
71+
export default function TodoList() {
72+
return (
73+
<>
74+
<ul className="todo__list">
75+
{/* <!-- 할 일 목록이 없을 때 --> */}
76+
<TodoListEmpty />
77+
{/* <!-- 할 일 목록이 있을 때 --> */}
78+
<TodoListItem />
79+
</ul>
80+
</>
81+
);
82+
}
83+
```
84+
85+
### 할 일 등록 기능 구현하기
86+
87+
![image.png](attachment:e6f096f2-a804-406f-9c51-9bad3248c890:image.png)
88+
89+
할 일 input 입력 받기 > 제어 컴포넌트 방식, 값을 입력하면 리 렌더링이 됨
90+
91+
그런데 Todo Editor의 상태값을 Todo에서 제어하면 밑의 하위 컴포넌트까지 모두 다 리렌더링이 되니까
92+
Todo Editor 컴포넌트에서만 상태를 정의하고 관리해야 다른 컴포넌트들에게 영향이 가지 않음
93+
94+
그래서 TodoEditor에서도 상태값 만들어서 개별 컴포넌트에 대한 리스너, 상태 업데이트도 해주고
95+
96+
Todo에서도 상태값 만들어서 전체 item 리스트에 대한 리스너 달아주기
97+
98+
엔터를 눌러도 제출, 버튼 눌러도 제출이 되도록 설정 > onSubmit 이용
99+
100+
```tsx
101+
//TodoEditor.tsx
102+
import { useState } from "react";
103+
import Button from "./html/Button";
104+
import Input from "./html/Input";
105+
106+
export default function TodoEditor({
107+
addTodo,
108+
}: {
109+
addTodo: (text: string) => void;
110+
}) {
111+
const [text, setText] = useState("");
112+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
113+
e.preventDefault();
114+
addTodo(text);
115+
console.log(text);
116+
setText("");
117+
};
118+
return (
119+
<>
120+
<form className="todo__form" onSubmit={handleSubmit}>
121+
<div className="todo__editor">
122+
<Input
123+
type="text"
124+
className="todo__input"
125+
placeholder="Enter Todo List"
126+
value={text}
127+
onChange={(e) => setText(e.target.value)}
128+
/>
129+
<Button className="todo__button" type="submit">
130+
Add
131+
</Button>
132+
</div>
133+
</form>
134+
</>
135+
);
136+
}
137+
```
138+
139+
```tsx
140+
//Todo.tsx
141+
import { useState } from "react";
142+
import TodoEditor from "./TodoEditor";
143+
import TodoHeader from "./TodoHeader";
144+
import TodoList from "./TodoList";
145+
146+
export default function Todo() {
147+
const [todos, setTodos] = useState<Todo[]>([]);
148+
const addTodo = (text: string) => {
149+
setTodos((todos) => [
150+
...todos,
151+
{
152+
id: Date.now(),
153+
text,
154+
completed: false,
155+
},
156+
]);
157+
};
158+
return (
159+
<>
160+
//테스트 출력용
161+
{/* <pre>{JSON.stringify(todos, null, 2)}</pre> */}
162+
<div className="todo">
163+
<TodoHeader />
164+
{/* <!-- 할 일 등록 --> */}
165+
<TodoEditor addTodo={addTodo} />
166+
{/* <!-- 할 일 목록 --> */}
167+
<TodoList />
168+
</div>
169+
</>
170+
);
171+
}
172+
```
173+
174+
### 할 일 목록 렌더링 기능 구현하기
175+
176+
Todo interface로 만든걸 TodoList > TodoItem까지 가져오기
177+
178+
```tsx
179+
//TodoListItem.tsx
180+
import Button from "./html/Button";
181+
import Checkbox from "./html/Checkbox";
182+
import SvgClose from "./svg/SvgClose";
183+
import SvgPencil from "./svg/SvgPencil";
184+
185+
export default function TodoListItem({ todo }: { todo: Todo }) {
186+
return (
187+
<>
188+
{/* <!-- 할 일이 완료되면 .todo__item--complete 추가 --> */}
189+
//선택적 스타일 적용(체크됨/안됨)
190+
<li className={`todo__item ${todo.completed && "todo__item--complete"}`}>
191+
<Checkbox
192+
parentClassName="todo__checkbox-group"
193+
type="checkbox"
194+
className="todo__checkbox"
195+
checked={todo.completed}
196+
>
197+
{todo.text}
198+
</Checkbox>
199+
{/* <!-- 할 일을 수정할 때만 노출 (.todo__checkbox-group은 비노출) --> */}
200+
{/* <!-- <Input type="text" className="todo__modify-Input" /> --> */}
201+
<div className="todo__button-group">
202+
<Button className="todo__action-button">
203+
<SvgPencil />
204+
</Button>
205+
<Button className="todo__action-button">
206+
<SvgClose />
207+
</Button>
208+
</div>
209+
</li>
210+
</>
211+
);
212+
}
213+
```
214+
215+
### 할 일 완료 및 삭제 기능 구현하기
216+
217+
props drilling으로 todo에서 만들어서 toggleTodo, deleteTodo 내려보내주기
218+
219+
```tsx
220+
import { useState } from "react";
221+
import TodoEditor from "./TodoEditor";
222+
import TodoHeader from "./TodoHeader";
223+
import TodoList from "./TodoList";
224+
225+
export default function Todo() {
226+
const [todos, setTodos] = useState<Todo[]>([]);
227+
const addTodo = (text: string) => {
228+
setTodos((todos) => [
229+
...todos,
230+
{
231+
id: Date.now(),
232+
text,
233+
completed: false,
234+
},
235+
]);
236+
};
237+
238+
//id가 일치하는걸 찾으면 completed를 반대로 바꿔줌
239+
const toggleTodo = (id: number) => {
240+
setTodos((todos) =>
241+
todos.map((todo) =>
242+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
243+
)
244+
);
245+
};
246+
247+
const deleteTodo = (id: number) => {
248+
setTodos((todos) => todos.filter((todo) => todo.id !== id));
249+
};
250+
return (
251+
<>
252+
{/* //테스트 출력용 */}
253+
{/* <pre>{JSON.stringify(todos, null, 2)}</pre> */}
254+
<div className="todo">
255+
<TodoHeader />
256+
{/* <!-- 할 일 등록 --> */}
257+
<TodoEditor addTodo={addTodo} />
258+
{/* <!-- 할 일 목록 --> */}
259+
<TodoList
260+
todos={todos}
261+
toggleTodo={toggleTodo}
262+
deleteTodo={deleteTodo}
263+
/>
264+
</div>
265+
</>
266+
);
267+
}
268+
```
269+
270+
### 할일 수정 기능 구현하기
271+
272+
1. 수정 버튼 클릭 여부에 따라 컴포넌트 바뀌기(입력칸 + 저장으로)
273+
> 새 상태 만들어서 수정 버튼에 클릭 리스너 달아주기
274+
2. 입력된 값 수정 입력칸에 들어가 있어야 함
275+
> 새 상태 만들어서 변할 때마다 todo.text로 value 채워주기
276+
3. 새로 수정했을때 수정 내역 반영하기
277+
> Todo.tsx에서 modifyTodo 함수 만들고 props drilling으로 내려보내주기
278+
279+
```tsx
280+
//TodoListItem.tsx
281+
import { useState } from "react";
282+
import Button from "./html/Button";
283+
import Checkbox from "./html/Checkbox";
284+
import Input from "./html/Input";
285+
import SvgClose from "./svg/SvgClose";
286+
import SvgPencil from "./svg/SvgPencil";
287+
288+
export default function TodoListItem({
289+
todo,
290+
toggleTodo,
291+
deleteTodo,
292+
modifyTodo,
293+
}: {
294+
todo: Todo;
295+
toggleTodo: (id: number) => void;
296+
deleteTodo: (id: number) => void;
297+
modifyTodo: (id: number, text: string) => void;
298+
}) {
299+
const [isModify, setIsModify] = useState(false);
300+
const [modifyText, setModifyText] = useState("");
301+
const modifyHandler = () => {
302+
setIsModify((isModify) => !isModify);
303+
setModifyText((modifyText) => (modifyText === "" ? todo.text : modifyText));
304+
if (modifyText.trim() !== "" && todo.text !== modifyText) {
305+
modifyTodo(todo.id, modifyText);
306+
}
307+
};
308+
return (
309+
<>
310+
{/* <!-- 할 일이 완료되면 .todo__item--complete 추가 --> */}
311+
<li className={`todo__item ${todo.completed && "todo__item--complete"}`}>
312+
{!isModify && (
313+
<Checkbox
314+
parentClassName="todo__checkbox-group"
315+
type="checkbox"
316+
className="todo__checkbox"
317+
checked={todo.completed}
318+
onChange={() => toggleTodo(todo.id)}
319+
>
320+
{todo.text}
321+
</Checkbox>
322+
)}
323+
{/* <!-- 할 일을 수정할 때만 노출 (.todo__checkbox-group은 비노출) --> */}
324+
{isModify && (
325+
<Input
326+
type="text"
327+
className="todo__modify-Input"
328+
value={modifyText}
329+
onChange={(e) => setModifyText(e.target.value)}
330+
/>
331+
)}
332+
<div className="todo__button-group">
333+
<Button className="todo__action-button" onClick={modifyHandler}>
334+
<SvgPencil />
335+
</Button>
336+
<Button
337+
className="todo__action-button"
338+
onClick={() => deleteTodo(todo.id)}
339+
>
340+
<SvgClose />
341+
</Button>
342+
</div>
343+
</li>
344+
</>
345+
);
346+
}
347+
```
348+
349+
![스크린샷 2025-11-18 오후 7.07.13.png](attachment:392b2990-406d-4bcc-8525-df3f2d11cce7:스크린샷_2025-11-18_오후_7.07.13.png)

0 commit comments

Comments
 (0)