|
| 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 | + |
| 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 | + |
0 commit comments