🧑🌾 리액트와 스프링부트 그리고 JPA 를통해 todoList를 만들어 볼 예정입니다. 리액트의 공부는 시작단계이기 때문에 참고자료를 많이 활용하였습니다.
🔎 Key-Point
1. 리액트의 활용 및 최적화
2. 스프링부트 및 JPA를 통해 간단하게 데이터 저장
💡 React 설치
- VSCode의 터미널을 실행합니다.
미리 설치해둔 node js의 명령어 입력 및 리액트 파일을 설치합니다.
- 파일이 정상적으로 설치가 되었다면 localhost:3000에 이런 화면이 생성이 됩니다. 그러면 리액트 설치와 관련된 준비는 모두 마쳤습니다.
💡 프로젝트 초기화 및 App.js
리액트는 기본 생성되어있는 src/App.js 파일을 중심으로 화면에 나타나게 됩니다.
💡 TodoListTemplate 컴포넌트
- 헤더 부분 파일 부분 컴포넌트 입니다.
import React from "react";
import '../css/TodoListTemplate.css';
const TodoListTemplate = ({form,children}) =>{
return(
<main className="todo-list-template">
<div className="todo-list-title">
오늘할일
</div>
<section className="form-wrapper">
{form}
</section>
<section className="todoItemList-wrapper">
{children}
</section>
</main>
);
};
export default TodoListTemplate;
💡Form 컴포넌트
- src 밑에 js 파일을 만들고 Form.js 파일을 만들어줍니다.
import React, { useState } from 'react';
import '../css/Form.css';
// *** Form.js 에서 Hook(useState) 사용으로 인해 수정
// const Form = ({ value, onChange, onCreate, onKeyPress }) => {
const Form = ({ onCreate }) => {
// React Hook > 클래스 타입에서는 사용 X
const [ input, setInput ] = useState('');
// input 값 변경
const handleChange = (event) => {
setInput(event.target.value);
}
// Enter key event
const handleKeyPress = (event) => {
// 눌려진 키가 Enter key 인 경우 handleCreate 호출
if(event.key === 'Enter') {
onCreate(input);
setInput('');
}
}
return (
<div className="form">
<input
value={input}
placeholder="오늘 할 일을 입력하세요.."
onChange={handleChange}
onKeyPress={handleKeyPress} />
<div className="create-button" onClick={() => {
onCreate(input);
setInput('');
}
}>
추가
</div>
</div>
);
};
export default Form;
App 컴포넌트 하위에 TodoListTemplate 컴포넌트가 존재하며, TodoListTemplate 컴포넌트의 props 인 children 으로 "오늘 할 일 템플릿입니다" 라는 값이 들어왔음을 확인할 수 있습니다.
아래는 src/css 파일에 Form.css 파일입니다.
.form {
display: flex;
}
.form input {
flex: 1; /* 버튼을 뺀 빈 공간을 모두 채워줍니다 */
font-size: 1.25rem;
outline: none;
border: none;
border-bottom: 1px solid #353b54e0;
}
.create-button {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 1rem;
padding-right: 1rem;
margin-left: 1rem;
background: #353b54e0;
border-radius: 3px;
color: white;
font-weight: 600;
cursor: pointer;
}
.create-button:hover {
background: #353b54e0;
}
이후에 App.js에 import 시켜줍니다.
import Form from './components/js/Form';
기본적인 틀이 완성이 됩니다.
💡 TodoItemList 컴포넌트
- 위와 마찬가지로 js폴더에 TodoItemList js 파일을 만들어줍니다.
import React from 'react';
import TodoItem from './TodoItem';
class TodoItemList extends React.Component {
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// shouldComponentUpdate(nextProps, nextState) {
// return this.props.todos !== nextProps.todos;
// }
render() {
const { todos, onToggle, onRemove } = this.props;
console.log(todos);
const todoList = todos.map(
({ id, content, isComplete }) => (
<TodoItem
id={id}
content={content}
isComplete={isComplete}
onToggle={onToggle}
onRemove={onRemove}
key={id} />
)
);
return (
<div>
{todoList}
</div>
);
}
}
export default TodoItemList;
TodoItem 컴포넌트는 5가지 props 를 받는다.
- content: todo 내용
- isComplete : 체크박스 on/off 상태를 의미하며, 오늘 할 일의 완료 유무를 판단
- id : TodoItem 의 Key 값
- onToggle : 체크박스를 on/off 시키는 함수
- onRemove : TodoItem 을 삭제시키는 함수
위 코드의 Line 9 와 Line 10 을 보면, 해당 컴포넌트의 최상위 DOM 의 클릭 이벤트에는 onToggle 을 설정하고, x 가 있는 부분에는 onRemove 를 설정해주었다.
또한 onRemove 를 호출하는 곳에는 e.stopPropagation() 이라는 함수가 호출된 것을 확인할 수 있다.
만약에 이 함수를 호출하지 않으면 x 를 눌렀을 때 onRemove 만 실행되는 것이 아니라 해당 DOM 의 부모의 클릭 이벤트에 연결되어 있는 onToggle 도 실행되게 된다.
즉 onRemove → onToggle 순으로 함수가 호출되면서 코드가 의도치 않게 작동하여 삭제가 제대로 진행되지 않게 된다.
e.stopPropagation() 은 이벤트의 확산을 멈춰준다. 즉, 삭제 부분에 들어간 이벤트가 해당 부모의 이벤트까지 전달되지 않도록 해준다. 따라서 onToggle 은 실행되지 않고 onRemove 만 실행된다.
💡TodoItem 컴포넌트
import React from 'react';
import '../css/TodoItem.css';
class TodoItem extends React.Component {
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// shouldComponentUpdate(nextProps, nextState) {
// return this.props.isComplete !== nextProps.isComplete;
// }
render() {
const { content, isComplete, id, onToggle, onRemove } = this.props;
console.log(id);
return (
<div className="todo-item" onClick={() => onToggle(id)}>
<div className="todo-item-remove" onClick={(e) => {
e.stopPropagation(); // onToggle 이 실행되지 않도록 함
onRemove(id)
}
}>
×
</div>
<div className={`todo-item-text ${isComplete && 'isComplete'}`}>
<div>
{content}
</div>
</div>
{
isComplete && (<div className="isComplete-mark">✓</div>)
}
</div>
)
}
}
export default TodoItem;
- js파일을 만들어주면 기본적인 js파일 생성은 끝이납니다.
- TodoItemList 컴포넌트와 마찬가지로 TodoItem 컴포넌트도 최적화가 필요합니다.
TodoItem 컴포넌트 isComplete 값의 변경 및 TodoItem 컴포넌트 추가 및 삭제의 경우에도 모든 컴포넌트가 렌더링되고 있기 때문입니다.
아래는 TodoItem css 파일입니다.
.todo-item {
padding: 1rem;
display: flex;
align-items: center; /* 세로 가운데 정렬 */
cursor: pointer;
transition: all 0.15s;
user-select: none;
background-color: whitesmoke;
margin: 0.3rem;
border-radius: 5px 5px 5px 5px;
}
.todo-item:hover {
background: #353b544d;
}
/* todo-item 에 마우스가 있을때만 .remove 보이기 */
.todo-item:hover .todo-item-remove {
opacity: 1;
}
/* todo-item 사이에 윗 테두리 */
.todo-item + .todo-item {
border-top: 1px solid #f1f3f5;
}
.todo-item-remove {
margin-right: 1rem;
color: #e64980;
font-weight: 600;
opacity: 0;
}
.todo-item-content {
flex: 1; /* 체크, 엑스를 제외한 공간 다 채우기 */
word-break: break-all;
}
.isComplete {
text-decoration: line-through;
color: #adb5bd;
}
.isComplete-mark {
font-size: 1.5rem;
line-height: 1rem;
margin-left: 1rem;
color: #353b54e0;;
font-weight: 800;
}
💡App. js
- 위 파일들을 import 시키고 값들을 받아오는 코드를 만들어줍니다.
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// input : "",
todos : [
]
}
// this.handleChange = this.handleChange.bind(this);
this.handleCreate = this.handleCreate.bind(this);
// this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.handleInitInfo = this.handleInitInfo.bind(this);
}
componentDidMount() {
this.handleInitInfo()
}
handleInitInfo() {
fetch("/api/todos")
.then(res => res.json())
.then(todos => this.setState({todos : todos}))
.catch(err => console.log(err))
}
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// input 값 변경
// handleChange(event) {
// this.setState({
// input: event.target.value
// });
// }
// *** Form.js 에서 Hook(useState) 사용으로 인해 state 에서 input 을 제외하고
// parameter 로 받는다.
// 등록
handleCreate(inputValue) {
const { todos } = this.state;
if (inputValue === "") {
alert("오늘 할 일을 입력해주세요!");
return;
}
// 화면에서 먼저 변경사항을 보여주는 방법으로 이용
this.setState({
// input: "",
// concat 을 사용하여 배열에 추가
todos: todos.concat({
id: 0, // 임의의 id를 부여하여 key error 를 방지
content: inputValue,
isComplete: false
})
});
// 처리
const data = {
body: JSON.stringify({"content" : inputValue}),
headers: {'Content-Type': 'application/json'},
method: 'post'
}
fetch("/api/todos", data)
.then(res => {
if(!res.ok) {
throw new Error(res.status);
} else {
return this.handleInitInfo();
}
})
.catch(err => console.log(err));
}
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// Enter Key 이벤트
// handleKeyPress(event) {
// if (event.key === "Enter") {
// this.handleCreate();
// }
// }
// 수정
handleToggle(id) {
const { todos } = this.state;
const isComplete = todos.find(todo => todo.id === id).isComplete;
if(!window.confirm(isComplete ? "미완료 처리 하시겠습니까?" : "완료 처리 하시겠습니까?")) {
return;
}
// 파라미터로 받은 id 를 가지고 몇 번째 아이템인지 찾는다.
const index = todos.findIndex(todo => todo.id === id);
// 선택한 객체를 저장한다.
const selected = todos[index];
// 배열을 복사한다.
const nextTodos = [...todos];
// 기존의 값을 복사하고 isComplete 값을 덮어쓴다.
nextTodos[index] = {
...selected,
isComplete : !selected.isComplete
};
this.setState({
todos : nextTodos
});
const data = {
headers: {'Content-Type':'application/json'},
method: 'put'
}
fetch("/api/todos/" + id, data)
.then(res => {
if(!res.ok) {
throw new Error(res.status);
} else {
return this.handleInitInfo();
}
})
.catch(err => console.log(err));
}
// 삭제
handleRemove(id) {
const { todos } = this.state;
const removeContent = todos.find(todo => todo.id === id).content;
if(!window.confirm("'" + removeContent + "' 을 삭제하시겠습니까?")) {
return;
}
this.setState({
todos : todos.filter(todo => todo.id !== id)
});
const data = {
headers: {'Content-Type':'application/json'},
method: 'delete'
}
fetch("/api/todos/" + id, data)
.then(res => {
if(!res.ok) {
throw new Error(res.status);
} else {
return this.handleInitInfo();
}
})
.catch(err => console.log(err));
}
render() {
return (
<TodoListTemplate form={(
<Form
// *** Form.js 에서 Hook(useState) 사용으로 인해 제거
// value={this.state.input}
// onChange={this.handleChange}
// onCreate={this.handleCreate}
// onKeyPress={this.handleKeyPress}
onCreate={this.handleCreate}
/>
)}>
<TodoItemList
todos={this.state.todos}
onToggle={this.handleToggle}
onRemove={this.handleRemove} />
</TodoListTemplate>
);
}
}
export default App;
💡 완료
💡자바 코드는 글이 길어질거 같아 github 코드로 대체합니다.
(BeginAdvanture/todolist (github.com))
자바 코드를 테스트 해보면 아래와 같이 값이 들어간것을 확인할수 있습니다.
다시 VSCode로 돌아와 package.json 파일의 localhost 주소를 변경해줍니다.
{
"name": "todolist",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-scripts": "3.4.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8081"
}
💡다시 프론트앤드와 백앤드 서버를 터미널로 구동시켜주면 작업이 완료됩니다.
🏇 서버를 재구동 하더라도 데이터가 H2 Database에 저장되어있는것을 확인할 수 있습니다.