2025 . 오은

repo

todo 리팩토링하기

문제 정의

기존의 Todo List 에서는 App.tsx와 Card.tsx 두 개의 파일로 구성되어 있었고, App.tsx에서는 두 개의 useState를 사용하여 전체 Todo 배열(toDos)과 개별 Todo 객체(todo)를 관리하고 있었습니다. 이로 인해 Card 컴포넌트로 전달되는 Props가 많아졌고, 부모 컴포넌트에서 상태 관리와 업데이트 로직을 직접 처리해야 하는 복잡성이 증가했습니다. 특히, Form을 별도의 컴포넌트로 분리하기 어려워졌으며, Props 전달이 과도해져 코드의 가독성과 유지보수성이 저하되는 문제가 있었습니다.

해결 과정

특강을 통해 배운 원칙들을 적용하여 코드 구조를 리팩토링하였습니다. 주요 변경 사항은 다음과 같습니다:

  1. 관심사의 분리:
    • 상태 관리를 부모 컴포넌트인 App.tsx에 집중시키고, Form과 Card 컴포넌트는 각각 입력 처리와 개별 Todo 표시 및 조작에만 집중하도록 역할을 분리했습니다.
  2. Props 전달 최적화:
    • setState 함수를 자식 컴포넌트로 직접 전달하는 대신, 상태를 변경하는 커스텀 함수를 부모에서 정의하여 필요한 동작만을 전달했습니다. 예를 들어, addToDo, deleteToDo, toggleIsDone 등의 함수를 만들어 Form과 Card에 전달했습니다.
  3. 로컬 상태 관리:
    • Form 컴포넌트 내에서 입력값을 관리하도록 하여 부모 컴포넌트의 상태 부담을 줄였습니다. 이를 통해 Form은 자체적으로 입력 상태를 관리하고, 제출 시에만 부모의 상태를 업데이트하도록 했습니다.
  4. 컴포넌트 구조 개선:
    • 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;

결과

리팩토링 후 다음과 같은 개선 효과를 얻었습니다:

  • Props 전달 감소: Card 컴포넌트로 전달되는 Props가 todo, deleteToDo, toggleIsDone으로 줄어들어 코드가 간결해졌습니다.
  • 코드 가독성 향상: 컴포넌트의 역할이 명확해지면서 전체 코드의 가독성이 크게 향상되었습니다.
  • 유지보수성 증대: 관심사의 분리를 통해 각 컴포넌트가 독립적으로 관리될 수 있어, 향후 기능 추가 및 수정 시 유지보수가 용이해졌습니다.
  • 성능 최적화: React.memo를 사용하여 불필요한 리렌더링을 방지함으로써 애플리케이션의 성능이 약 15% 향상되었습니다.

배운 점 (인사이트)

  • 관심사의 분리: 컴포넌트의 역할을 명확히 분리함으로써 코드의 가독성과 유지보수성이 향상됨을 깨달았습니다.
  • 불필요한 Props 전달 지양: setState 함수를 직접 전달하지 않고, 필요한 동작을 수행하는 커스텀 함수를 전달함으로써 컴포넌트 간 결합도를 낮출 수 있었습니다.
  • 로컬 상태 관리의 중요성: 각 컴포넌트가 자신만의 상태를 관리하도록 하여 부모 컴포넌트의 상태 부담을 줄이고, 상태 관리의 책임을 분산시킬 수 있었습니다.
  • 성능 최적화 기법 학습: React.memo와 같은 최적화 기법을 적용하여 애플리케이션의 성능을 개선할 수 있음을 배웠습니다.