프로젝트 구조
.
├── App.css
├── App.js
├── App.test.js
├── app-config.js
├── components
│ ├── AddTodo.jsx
│ └── TodoItem.jsx
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
├── services
│ └── ApiService.js
└── setupTests.js
App.js에서 props로 전달받는 매개변수들은 아래와 같다.
// import 생략
const TodoItem = (props) => {
const [item, setItem] = useState(props.item);
const [readOnly, setReadOnly] = useState(true);
const { deleteItem, update } = props;
const deleteHandler = () => {
deleteItem(item.id);
};
const offReadOnlyMode = () => {
console.log('Event!', readOnly);
setReadOnly(false);
};
const enterKeyEventHandler = (e) => {
if (e.key === 'Enter') {
setReadOnly(true);
update(item);
}
};
const editEventHandler = (e) => {
setItem({ ...item, title: e.target.value });
};
const checkboxEventHandler = (e) => {
item.done = e.target.checked;
setItem(item);
update(item);
};
return (
<div className="item">
<input
type="checkbox"
name={item.id}
id={item.id}
checked={item.done}
onChange={checkboxEventHandler}
style={{ marginRight: '5px' }}
/>
<input
value={item.title}
className="item-text"
id={item.id}
name={item.id}
onClick={offReadOnlyMode}
onChange={editEventHandler}
onKeyPress={enterKeyEventHandler}
readOnly={readOnly}
/>
<button onClick={deleteHandler}>
<span className="material-icons">highlight_off</span>
</button>
</div>
);
};
export default TodoItem;
<input>
체크박스checked
는 디폴트로 itme.done
이 false이기에 비활성화onChange
속성에 이벤트 핸들러 할당하여 사용자가 체크하면 해당 아이템의 done 속성을 토글한다.<input>
onChange
속성에 이벤트 핸들러 할당하여(editEventHandler
) 키입력 할 때마다 item을 새 값으로 저장한다.onKeyPress
: 엔터 누르면 수정된 내용을 저장한다.@material-ui/core
활용하여 스타일링
App.js로부터 props로 add
함수를 전달받아서 버튼 클릭 시 호출되도록 한다.
import React from 'react';
import { useState } from 'react';
const AddTodo = (props) => {
const [inputText, setInputText] = useState('');
const { add } = props;
const onInputChange = (e) => {
setInputText(e.target.value);
};
const onButtonClick = () => {
add(inputText);
setInputText('');
};
const enterEvevtHandler = (e) => {
if (e.key === 'Enter') {
onButtonClick();
}
};
return (
<div className="form">
<input
type="text"
value={inputText}
onChange={onInputChange}
onKeyDown={enterEvevtHandler}
/>
<button onClick={onButtonClick}>
<span
style={{
padding: '0.3rem',
borderRadius: '5px',
border: 'solid',
backgroundColor: 'ButtonFace',
}}
>
ADD
</span>
</button>
</div>
);
};
export default AddTodo;
<input>
필드에 title
입력➡️ 사용자가 <input>
필드에 입력한 내용을 저장해야한다.
onInputChange
onButtonClick
enterEvevtHandler
const [inputText, setInputText] = useState('');
state에 사용자 입력값 기억하기 위해 useState
사용한다.
const onInputChange = (e) => {
setInputText(e.target.value);
};
// ...
return (
// ...
<input
type="text"
value={inputText} // 변경된 상태를 화면에 출력
onChange={onInputChange} // 변할때마다 상태 저장
onKeyDown={enterEvevtHandler} // 엔터클릭 시 리스트에 추가
/>
<button onClick={onButtonClick}>
<span
style={{
padding: '0.3rem',
borderRadius: '5px',
border: 'solid',
backgroundColor: 'ButtonFace',
}}
>
ADD
</span>
</button>
</div>
);
};
전체 소스 코드
import React from 'react';
import { useEffect, useState } from 'react';
import TodoItem from './components/TodoItem';
import { Paper, List, Container } from '@material-ui/core';
import AddTodo from './components/AddTodo';
import { call } from './services/ApiService';
const uuid = require('uuid');
const App = () => {
const [items, setItems] = useState([]);
useEffect(() => {
call('/todo', 'GET', null).then((res) => {
setItems(res.data.data);
});
}, []);
const add = (item) => {
const id = uuid.v4();
const newItem = { id, title: item, done: false };
setItems(items.concat(newItem));
call('/todo', 'POST', newItem).then((res) => {
setItems(res.data.data);
});
};
const deleteItem = (item) => {
const itemId = { id: item };
call('/todo', 'DELETE', itemId).then((res) => {
setItems(res.data.data);
});
};
const update = (item) => {
console.log(item);
call('/todo', 'PUT', item).then((res) => {
setItems(res.data.data);
});
};
const todoItems =
items.length > 0 &&
items.map((item, idx) => (
<Paper key={idx}>
<List key={idx + 1}>
<TodoItem
item={item}
key={item.id}
deleteItem={deleteItem}
update={update}
/>
</List>
</Paper>
));
return (
<div>
<div className="container">
<div className="heading">
<h1>TODO LIST</h1>
</div>
<AddTodo add={add} />
{todoItems}
</div>
</div>
);
};
export default App;
컴포넌트의 state에 추가할 todo 아이템을 기억하도록 설정한다.
useEffect(() => {
call('/todo', 'GET', null).then((res) => {
setItems(res.data.data);
});
}, []);
매개변수로 콜백함수와 리스트를 전달한다.
빈 리스트를 전달하여서 앱이 시작되고 처음 마운트될 때 한 번만 콜백함수를 호출한다.
콜백함수 call
은 백엔드 서버의 /todo
경로로 HTTP GET 요청을 보낸다. call의 세번째 매개변수는 옵션인데, GET은 바디를 보내지 않으니 null 설정
call
은 Promise를 반환한다. 따라서 .then
메서드 체이닝으로 call함수 실행완료 시 res
를 넘겨준다. res
는 백엔드서버에 저장되어있는 todo 아이템들이 data
속성에 저장되어 있다. 응답으로 받은 todo 아이템들을 프론트 어플리케이션의 state로 넘겨준다.
todo 아이템의 기본 구조
let itemStructure = {
id : "item's id",
title : "something to do",
done : false
}
const add = (item) => {
const id = uuid.v4();
const newItem = { id, title: item, done: false };
setItems(items.concat(newItem));
call('/todo', 'POST', newItem).then((res) => {
setItems(res.data.data);
});
};
ADD 버튼 클릭 시 실행된다.
매개변수로 사용자가 입력한 todo의 내용(title)을 전달한다. 아이템을 생성하고 기존 state에 저장되어 있던 items(리스트)에 추가해준 뒤, state를 새로 저장한다. 백엔드로 새 아이템을 보내서 데이터베이스에 저장
const deleteItem = (item) => {
const itemId = { id: item };
call('/todo', 'DELETE', itemId).then((res) => {
setItems(res.data.data);
});
};
delete버튼 클릭 시 실행된다.
매개변수로 해당 컴포넌트 오브젝트를 받는다. 오브젝트에서 id를 구조분해하여 추출하고 id를 백엔드 서버로 전송하여 데이터베이스에서 삭제
const update = (item) => {
console.log(item);
call('/todo', 'PUT', item).then((res) => {
setItems(res.data.data);
});
};
할일을 클릭하여 수정한 후 엔터키를 누르면 실행된다. 백엔드로 변경된 내용을 보내서 수정한다.
const todoItems =
items.length > 0 &&
items.map((item, idx) => (
<Paper key={idx}>
<List key={idx + 1}>
<TodoItem
item={item}
key={item.id}
deleteItem={deleteItem}
update={update}
/>
</List>
</Paper>
));
return (
<div>
<div className="container">
<div className="heading">
<h1>TODO LIST</h1>
</div>
<AddTodo add={add} />
{todoItems}
</div>
</div>
);
};
별게없다