최상위 컴포넌트인 App에서 크게 두 자식 컴포넌트를 가지는 것으로 구상을 했다.
CreateTodo와 TodoList로, 전자는 onCreate prop을 받아서 데이터를 만들고 후자는 App의 Data를 받아서 map을 돌려 배열 안의 각각의 요소({title : "말하기", ... id:"1"})를 TodoItem이라는 새로운 컴포넌트로 생성한다.
// App.js
function App() {
const [data, setData] = useState([]);
// todoId 생성
let todoId = useRef(0);
// 생성
const onCreate = ({ title, content, isDone }) => {
const newTodo = {
title,
content,
isDone,
id: parseInt(todoId.current) + 1,
};
todoId++;
setData([...data, newTodo]);
};
// 삭제
const onRemove = (targetId) => {
const newTodo = data.filter((i) => i.id !== targetId);
setData(newTodo);
};
return (
<div>
<Header />
<CreateToDo onCreate={onCreate} />
<TodoList todoList={data} onRemove={onRemove} />
</div>
);
}
export default App;
//CreateToDo.jsx
const CreateToDo = ({ onCreate }) => {
const [todos, setTodos] = useState({ title: "", content: "", isDone: false, id: "" });
// input의 ref 생성
const titleInput = useRef();
const contentInput = useRef();
// 제목과 내용을 입력할 때
const handleChangeState = (e) => {
setTodos({ ...todos, [e.target.name]: e.target.value });
};
// 추가하기 버튼을 눌렀을 때
const handleAddTodo = () => {
// 글자수 제한하기
if (todos.title.length < 2) {
titleInput.current.focus();
return;
}
if (todos.content.length < 5) {
contentInput.current.focus();
return;
}
onCreate(todos);
alert("해야 할 일이 생겼어요!");
};
return (
<div>
<form>
제목
<input ref={titleInput} type="text" name="title" value={todos.title} onChange={handleChangeState} placeholder="제목을 만들어주세요."></input>
내용<input ref={contentInput} type="text" name="content" value={todos.name} onChange={handleChangeState} placeholder="할 일을 입력해주세요"></input>
</form>
<button onClick={handleAddTodo}>추가하기</button>
</div>
);
};
CreateToDo에서는 추가하기 버튼을 눌렀을 때, 입력한 글자수가 최소 글자수를 넘는지 확인한다. 넘지 못한다면 useRef
로 참조한 값의 current(input)에 focus를 주고 Return하여 사용자가 반드시 최소 글자수를 작성하게 만들었다.
이 부분을 만족한다면 App으로 부터 받은 onCreate
에 할 일 데이터인 todos를 인자로 주고 할 일이 생성이 됐음을 alert를 이용해 알려준다.
// TodoList.jsx
function TodoList({ todoList, onRemove }) {
console.log(todoList);
return (
<div>
{/* todo item 하나하나 보여주기 -> map */}
{todoList.map((i) => {
return <TodoItem todoItem={i} key={i.id} onRemove={onRemove} />;
})}
</div>
);
}
TodoList 컴포넌트에서는 App으로부터 전체 데이터가 담긴 todoList와 삭제를 하는 onRemove를 프롭으로 전달 받는다. 이때 onRemove는 실제로 사용하지 않고 단지 TodoList의 자식 컴포넌트에게 전달하기 위해 값을 받고 있다 (이 부분은 프롭 드릴링이라고 한다 추후에 context를 사용해 리팩토링 하겠다.)
todoList는 배열 형태로, 각각의 할 일을 객체 형태로 담고 있다.
즉,
[
{title: '집 가기', content: '저쩌구', ...},
{title: '밥 먹기', content: "어쩌구", ...}
]
이런 형태라는거다.
이 상태에서 배열 안의 하나하나의 요소가 todo Item으로서 사용할 수 있으므로 map 메서드를 사용하고, 반환값으로 새로운 컴포넌트인 TodoItem
을 반환한다. 이때 맵을 통해 자식 컴포넌트를 반환하는 과정에서 key 값을 주지 않으면 콘솔에 경고가 뜬다. 이유는 따로 더 자세히 작성해보겠다.
// TodoItem.jsx
function TodoItem({ todoItem, onRemove }) {
// 삭제
const handleRemove = () => {
window.confirm("정말 삭제하시겠습니까?") && onRemove(todoItem.id);
alert("삭제되었습니다!");
};
return (
<article>
<h2>{todoItem.title}</h2>
<p>{todoItem.content}</p>
<div>
<button onClick={handleRemove}>삭제</button>
<button>완료</button>
</div>
</article>
);
}
삭제 버튼을 눌렀을 때 window.confirm을 통해 한 번 더 확인하고, 확인을 누른다면 부모로부터 받은 prop인 onRemove로 이 아이템의 Id를 전달하여 삭제를 진행한다. (App 컴포넌트의 onRemove 함수 참고)
생각보다 내가 modifier 함수를 사용할 때 문제가 많았다.
map을 사용하기 위해 배열을 돌려줘야하는데 modifier 함수에 그냥 setData({...data, newData}) 이런식으로 객체를 넘겨준다든지,얘한테 어떤 형태로 넘겨줘야하는지 작성하면서 헷갈렸다. 실행을 돌리고 에러가 나오면 그때 그때 수정하면서 진행했다.
그래도 리액트가 오류에 대해서 자세하게 언급해줘서 잘 수정할 수 있었고, 컴포넌트를 독립적으로 사용하는 것에 신경쓰면서 list와 item을 잘 나눴다고 생각한다.