어제 리액트 특강을 들으면서, 작동여부는 둘째 치더라도 잘 정렬된 props와 컴포넌트 구조를 가져야 하고 이로부터
가독성이 담보된다는 사실을 다시 한번 느꼈습니다. 물론 작동도 잘 해야겠죠? 아래 내용을 정리해 보려고 합니다.
기존 코드
일단 제가 작성했던 todo list는 App.tsx 와 Card.tsx 두 개로 되어 있었고,컴포넌트화는 Card 만 한 상태였습니다.
부모 컴포넌트가 되는 App.tsx 에서 2개의 useState 를 만들었습니다.
toDos 배열(전체 todo들)과 개별 toDo 객체 이렇게 2개 였고, 이렇게 생각한 이유는
Form 을 컴포넌트로 분리하기가 어렵다고 생각했습니다.
Form 컴포넌트에서 다루는 데이터는 어차피 부모로부터 props 로 받아야 할 것이고,
그러면 state 와 setState 함수 모두 내려받아야 해서 현재 단계에서 불필요할 것으로 생각했습니다.
그래서, App.tsx 에서는 form을 처리합니다.
onChange 핸들러인 handleChange와 onSubmit 핸들러인 handleSubmit 이 있습니다.
function App() {
// 모든 투두 객체들을 포함할 배열
const [toDos, setToDos] = useState<ToDo[]>(baseToDos);
// 인풋 값으로 계속 변경될 하나의 투두 객체
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) setToDos([...toDos, { ...todo, id: uuidv4() }]);
};
return (
<>
<div className="top_wrapper">
<header className="my_header">
<h3>My Todo List</h3>
<p>React</p>
</header>
<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>
<section className="content_section">
<div className="content_box">
<h2>Working...🔥</h2>
<div className="content">
{toDos
.filter((e) => !e.isDone)
.map((e, i) => (
<Card
key={i}
todo={e}
toDos={toDos}
inputted={todo}
setToDos={setToDos}
/>
))}
</div>
</div>
<div className="content_box">
<h2>Done...🎉</h2>
<div className="content">
{toDos
.filter((e) => e.isDone)
.map((e, i) => (
<Card
key={i}
todo={e}
toDos={toDos}
inputted={todo}
setToDos={setToDos}
/>
))}
</div>
</div>
</section>
</div>
</>
);
}
export default App;
이렇게 하다보니, Card 로 보내야하는 props 도 자연히 많았습니다. todo, toDos, inputted, setToDos 이렇게 4가지나 되었고, 그 중에서 setState 함수인 setToDos 또한 직접 내려주는 방법을 생각했습니다.
const Card = memo(({ todo, toDos, inputted, setToDos }: TodoProps) => {
// 투두 토글
const completeToDo = (copied: ToDo[]) => {
const mapped = copied.map((e) => {
if (e.id === todo.id) e.isDone = !e.isDone;
return e;
});
setToDos(mapped);
};
// 투두 업데이트
// 업데이트할 내용은 props 로 받은 input
const updateToDo = (copied: ToDo[]) => {
if (inputted.title === todo.title && inputted.body === todo.body) {
alert("바뀐 내용이 없네요!");
return;
} else if (inputted.title === "" || inputted.body === "") {
alert("입력 값이 없는 것 같아요 확인 부탁");
return;
} else {
// 현재 컴포넌트 데이터(todo)의 id 와 일치하는 id를 가진 객체를 toDos 배열에서 찾아서
// 찾은 객체의 title, body 값을 변경
const mapped = copied.map((e) => {
if (e.id === todo.id) {
return {
...e,
title: inputted.title,
body: inputted.body,
};
} else {
return e;
}
});
setToDos(mapped);
}
};
// 투두 삭제
const deleteToDo = (copied: ToDo[]) => {
// 현재 컴포넌트 데이터의 id 와 일치하지 않는 값만 반환(현재 값은 삭제해야 하므로)
const filtered = copied.filter((e) => e.id !== todo.id);
setToDos(filtered);
};
// 클릭 핸들러
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
// 불변성 유지
const copied = [...toDos];
if (e.currentTarget.id === "fin_cancel") {
completeToDo(copied);
} else if (e.currentTarget.id === "update") {
updateToDo(copied);
} else if (e.currentTarget.id === "del") {
deleteToDo(copied);
}
};
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 update" id="update" onClick={handleClick}>
수정
</div>
<div className="btn fin" id="fin_cancel" onClick={handleClick}>
{todo.isDone ? "취소" : "완료"}
</div>
</div>
</section>
);
});
export default Card;
4가지 props 를 받은 Card 는 위와 같이 처리하고 있었습니다.
완성은 했지만 무언가 찝찝했었는데, 특강을 통해 관점을 크게 바꿀 수 있었습니다.
수정한 코드
특강에서 여러가지를 배웠지만 요약하면 다음과 같습니다.
- setState 를 prop으로 주는 것은 좋지 않은 생각 - 자식 컴포넌트에서 setState 할일이 엄청 많지 않다면.
- form 의 값들을 처리하는 state 는 그냥 form 컴포넌트에 있어도 충분하다.
- container 컴포넌트에서 배열 state 하나만 만들고 이것을 변경하는 custom 함수들을 만들어서 얘네를 내려줘서 처리.
이 중 먼저 첫번째 문제는 setState를 상습적으로 내려주곤 했던 저에게 좋은 가르침이 되었습니다.
혼자 코드를 작성하면서 의문점이 이런 부분이었는데, 컨벤션을 새롭게 배운 느낌입니다.
두번째 문제는 이렇게 생각하긴 했는데 Form 으로 분리해서 관리하려면 더 복잡하고 redux 든 context API 든 상태관리를 따로 해줘야 될 것 같았습니다. 하지만! 역시 그냥 가능했습니다.
첫번째, 두번째 문제를 해결하는 방안이 세번째 항목의 요점인 것 같습니다.
부모 컴포넌트에서 state 하나만 만들고 이를 변경하는 setState 는 custom 함수로 감싸서(?) 사용하는 것입니다.
이렇게 하면 props 로 내려줄 때에도 setState 를 바로 내려주지 않아도 되고, 함수의 목적 또한 명확하게 제한해서 사용할 수 있었습니다.
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;
위처럼 추가, 삭제, 토글 함수를 만들어 주고, 부모 컴포넌트에는 toDos 배열 state 하나만 사용합니다.
그리고 Form 에서,
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;
이전에 부모컴포넌트에 있던 개별 toDo 객체 state를 만들고, 이 안에서만 처리하도록 합니다.
어차피 form 에서는 생각해보면 toDo 의 추가만 이루어지기 때문에, setState 를 통째로 받을 필요 없이,
addToDo 만 받으면 되는 것이었습니다!
Card 컴포넌트는 아래와 같습니다.
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;
역시 여기서도 처리해야하는 일은 완료여부 토글과 삭제 뿐이므로, 해당 기능을 하는 custom 함수 deleteToDo, toggleIsDone 을 내려주고 두가지 일만 처리합니다!
요약
리액트 특강을 통해 props와 컴포넌트 구조의 중요성을 재인식했습니다.
주요 개선 사항은 state 관리를 부모 컴포넌트에서 일관되게 하고,
필요한 기능을 custom 함수로 정의하여 자식 컴포넌트에 전달하는 것입니다.
이를 통해 props의 수를 줄이고, 각 컴포넌트의 역할을 명확히 하여 코드의 가독성과 유지보수성을 높였습니다.
이렇게 할 경우 구체적 이점은 다음과 같습니다.
- 구조적 명확성:
- 각 함수가 무엇을 하는지 명확하게 알 수 있어 코드의 가독성이 높아집니다. 예를 들어,
addToDo
,deleteToDo
,toggleIsDone
등의 함수명은 각 함수의 목적을 분명히 드러냅니다. - 부모 컴포넌트에서 상태 관리를 일관되게 할 수 있으며, 상태 변경 로직이 분산되지 않습니다.
- 각 함수가 무엇을 하는지 명확하게 알 수 있어 코드의 가독성이 높아집니다. 예를 들어,
- 캡슐화와 재사용성:
- custom 함수는 상태 변경 로직을 캡슐화하므로, 필요할 때마다 쉽게 재사용할 수 있습니다.
- 자식 컴포넌트는 필요한 기능만 props로 받아 사용하므로, 불필요한 상태 변경 로직을 알 필요가 없습니다.
- 유지보수 용이성:
- 상태 변경 로직이 부모 컴포넌트에 집중되어 있어 수정이 필요할 때 해당 부분만 변경하면 됩니다.
- 코드의 한 부분만 수정하면 되므로 버그 발생 가능성이 줄어듭니다.
- 테스트 용이성:
- 상태 변경 함수가 분리되어 있어 각각의 함수를 독립적으로 테스트할 수 있습니다.
- 특정 기능을 하는 함수만 테스트하면 되므로 테스트 코드 작성이 수월합니다.
'react' 카테고리의 다른 글
[240522 TIL] Context API (0) | 2024.05.22 |
---|---|
[240519 WIL] Custom Hook (0) | 2024.05.19 |
[240515 TIL] jsx, useState, props, 불변성 등 (0) | 2024.05.15 |
[240514 TIL] 리액트 리렌더링 (0) | 2024.05.14 |
useMemo, useCallback 다시 정리 (0) | 2024.04.17 |