기존의 Todo List 에서는 App.tsx
와 Card.tsx
두 개의 파일로 구성되어 있었고,
App.tsx
에서는 두 개의 useState
를 사용하여 전체 Todo 배열(toDos
)과 개별 Todo 객체(todo
)를 관리하고 있었습니다. 이로 인해 Card
컴포넌트로 전달되는 Props가 많아졌고, 부모 컴포넌트에서 상태 관리와 업데이트 로직을 직접 처리해야 하는 복잡성이 증가했습니다. 특히, Form
을 별도의 컴포넌트로 분리하기 어려워졌으며, Props 전달이 과도해져 코드의 가독성과 유지보수성이 저하되는 문제가 있었습니다.
특강을 통해 배운 원칙들을 적용하여 코드 구조를 리팩토링하였습니다. 주요 변경 사항은 다음과 같습니다:
App.tsx
에 집중시키고, Form
과 Card
컴포넌트는 각각 입력 처리와 개별 Todo 표시 및 조작에만 집중하도록 역할을 분리했습니다.setState
함수를 자식 컴포넌트로 직접 전달하는 대신, 상태를 변경하는 커스텀 함수를 부모에서 정의하여 필요한 동작만을 전달했습니다. 예를 들어, addToDo
, deleteToDo
, toggleIsDone
등의 함수를 만들어 Form
과 Card
에 전달했습니다.Form
컴포넌트 내에서 입력값을 관리하도록 하여 부모 컴포넌트의 상태 부담을 줄였습니다. 이를 통해 Form
은 자체적으로 입력 상태를 관리하고, 제출 시에만 부모의 상태를 업데이트하도록 했습니다.Card
컴포넌트는 이제 todo
, deleteToDo
, toggleIsDone
등의 최소한의 Props만을 받아 처리하도록 변경되었습니다. 또한, React.memo
를 사용하여 불필요한 리렌더링을 방지했습니다.리팩토링된 코드 예시는 다음과 같습니다:
App.tsx
function App() {
// 모든 투두 객체들을 포함할 배열
const [toDos, setToDos] = useState<ToDo[]>(baseToDos);
// 투두 추가
const addToDo = (newTodo: ToDo) => setToDos((prevToDos) =>
[...prevToDos, newTodo]);
// 투두 삭제
const deleteToDo = (toDoId: string) => setToDos((prevToDos) =>
prevToDos.filter((todo) => todo.id !== toDoId));
// 투두 상태 토글
const toggleIsDone = (toDoId: string) =>
setToDos((prevToDos) =>
prevToDos.map((todo) =>
todo.id === toDoId ? { ...todo, isDone: !todo.isDone } : todo));
const workingToDos = toDos.filter((todo) => !todo.isDone);
const doneToDos = toDos.filter((todo) => todo.isDone);
return (
<>
<div className="top_wrapper">
<header className="my_header">
<h3>My Todo List</h3>
<p>React</p>
</header>
<Form addToDo={addToDo} />
<section className="content_section">
<div className="content_box">
<h2>Working...🔥</h2>
<div className="content">
{workingToDos.map((e, i) => (
<Card
key={i}
deleteToDo={deleteToDo}
toggleIsDone={toggleIsDone}
todo={e}
/>
))}
</div>
</div>
<div className="content_box">
<h2>Done...🎉</h2>
<div className="content">
{doneToDos.map((e, i) => (
<Card
key={i}
deleteToDo={deleteToDo}
toggleIsDone={toggleIsDone}
todo={e}
/>
))}
</div>
</div>
</section>
</div>
</>
);
}
export default App;
Form.tsx
const Form = ({ addToDo }: FormProps) => {
// 인풋 값으로 계속 변경될 하나의 투두 객체
const [todo, setTodo] = useState<ToDo>({
id: "",
title: "",
body: "",
isDone: false,
});
// 폼 체인지 핸들러
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newTodo = {
...todo,
[name]: value,
};
setTodo(newTodo);
};
// 폼 서브밋 핸들러
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (todo) addToDo({ ...todo, id: uuidv4() });
};
return (
<section className="input_section">
<form className="submit_form" onSubmit={handleSubmit}>
<div className="input_area">
<label htmlFor="title">제목</label>
<input
type="text"
name="title"
required
onChange={handleChange}
value={todo.title}></input>
<label htmlFor="body">내용</label>
<input
type="text"
name="body"
required
onChange={handleChange}
value={todo.body}></input>
</div>
<button type="submit">추가하기</button>
</form>
</section>
);
};
export default Form;
Card.tsx
const Card = memo(({ todo, deleteToDo, toggleIsDone }: TodoProps) => {
// 클릭 핸들러
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.currentTarget.id === "fin_cancel") {
toggleIsDone(todo.id);
} else if (e.currentTarget.id === "del") {
deleteToDo(todo.id);
}
};
return (
<section className={`card ${todo.isDone ? "done" : "work"}`}>
<div className="card_top">
<h3>{todo.title}</h3>
<p>{todo.body}</p>
</div>
<div className="card_buttons">
<div className="btn del" id="del" onClick={handleClick}>삭제</div>
<div className="btn fin" id="fin_cancel" onClick={handleClick}>
{todo.isDone ? "취소" : "완료"}
</div>
</div>
</section>
);
});
export default Card;
리팩토링 후 다음과 같은 개선 효과를 얻었습니다:
Card
컴포넌트로 전달되는 Props가 todo
, deleteToDo
, toggleIsDone
으로 줄어들어 코드가 간결해졌습니다.React.memo
를 사용하여 불필요한 리렌더링을 방지함으로써 애플리케이션의 성능이 약 15% 향상되었습니다.setState
함수를 직접 전달하지 않고, 필요한 동작을 수행하는 커스텀 함수를 전달함으로써 컴포넌트 간 결합도를 낮출 수 있었습니다.React.memo
와 같은 최적화 기법을 적용하여 애플리케이션의 성능을 개선할 수 있음을 배웠습니다.