개요
여기에서는 React를 사용하여, 이전 글 [Spring Boot] REST API 개발 (MyBatis,HSQLDB) 에서 예제로 만든 CRUD REST API를 호출하여 CRUD를 처리하는 방법을 보여준다. React는 함수형 컴포넌트를 사용하였다.
개발환경은 다음과 같다:
● npm : 9.6.7
● node : 18.17.0
● npx : 9.6.7
● react : 18.2.0
● react-dom : 18.2.0
● react-router-dom : 6.14.2
● bootstrap : 5.3.1
※ 관련글 목록: http://yellow.kr/lifeView.jsp?s=Frontend
예제 애플리케이션 설명
만들려는 예제 Books 관리 시스템은 REST API를 호출하여 다음과 같은 기능을 구현한다:
● 책 정보 리스트
● 책 정보 등록
● 책 정보 조회
● 책 정보 수정
● 책 정보 삭제
예제 Application의 Flow는 다음과 같다. 여기에서는 Client 부분에 해당한다(3000 port 사용).
‘Spring Boot REST 애플리케이션(8081 port 사용)’은 [Spring Boot] REST API 개발 (MyBatis,HSQLDB) 글을 참조
그리고 CORS(cross-origin 리소스 공유) 오류가 나타나는 것을 해결하기 위해 ‘Spring Boot REST 애플리케이션’의 Book Controller에 @CrossOrigin 어노테이션을 사용하였다.
React App 생성
우선 React 앱을 만들 폴더를 선택한 다음 터미널 또는 cmd에서 다음 명령을 실행하여 React 앱을 만든다.
C:\app\ReactProjects\npx create-react-app books-react-app
Install packages
위에서 React App을 생성한 후 React 프로젝트 폴더로 이동하여 다음의 패키지를 설치한다.
● bootstrap
● react-router-dom
● axios
C:\app\ReactProjects\cd books-react-app
C:\app\ReactProjects\books-react-app\npm install bootstrap
C:\app\ReactProjects\books-react-app\npm install react-router-dom
C:\app\ReactProjects\books-react-app\npm install axios
1. bootstrap
Bootstrap은 웹 프론트엔드 개발에서 가장 인기 있는 CSS 프레임워크 중 하나이다. 기본적으로 디자인과 레이아웃을 쉽게 구축할 수 있도록 도와주는 도구 모음이다. HTML, CSS, JavaScript를 사용하여 반응형 웹 페이지를 빠르게 개발하고 스타일을 적용하는 데 사용된다.
Bootstrap을 사용하면 디자인에 대한 고민 없이 빠르게 웹 페이지를 구축할 수 있다. 클래스 이름을 사용하여 스타일을 적용하는 방식이며, 적은 노력으로 많은 기능을 구현할 수 있다.
2. react-router-dom
react-router-dom은 React 애플리케이션에서 클라이언트 측 라우팅을 관리하는 패키지이다. React에서는 페이지를 새로고침하지 않고도 URL을 변경하여 다른 뷰를 보여주는 것을 SPA(Single Page Application) 방식으로 구현한다. react-router-dom은 SPA를 구현하는 데 도움을 주는 React용 라우팅 라이브러리이다.
주요 컴포넌트와 개념은 다음과 같다:
- BrowserRouter: 브라우저에 기반한 라우터 컴포넌트로, react-router-dom의 라우팅을 구현할 때 최상위 컴포넌트로 감싸주는 역할을 한다.
- Route: 특정 경로와 컴포넌트를 매핑하는 역할을 한다. 경로와 일치할 때 해당 컴포넌트를 렌더링한다.
- Link: 클릭 가능한 링크를 생성하는 역할을 한다. a 태그 대신 사용하면 새로고침 없이 라우터의 경로를 변경할 수 있다.
- Switch: Route들 중에서 처음으로 일치하는 하나의 컴포넌트만 렌더링하도록 도와주는 역할을 한다.
- useParams: URL의 동적 경로 매개변수를 추출하는 Hook이다.
이러한 컴포넌트들을 활용하여 React 애플리케이션 내에서 라우팅을 구현할 수 있다.
3. axios
axios는 브라우저와 Node.js를 위한 Promise 기반 HTTP 클라이언트이다. 서버와 HTTP 요청을 주고받을 때 사용되며, 데이터를 가져오거나 서버로 데이터를 보낼 때 유용하게 사용할 수 있다.
주요 기능 및 사용법은 다음과 같다:
- GET 요청: axios.get(url) 형식으로 서버로부터 데이터를 가져올 수 있다.
- POST 요청: axios.post(url, data) 형식으로 서버로 데이터를 보낼 수 있다.
- PUT 요청: axios.put(url, data) 형식으로 서버에 데이터를 업데이트할 수 있다.
- DELETE 요청: axios.delete(url) 형식으로 서버의 데이터를 삭제할 수 있다.
- 인터셉터(interceptors): 요청 전, 응답 전에 특정 작업을 할 수 있는 인터셉터를 활용하여 편리하게 요청과 응답을 처리할 수 있다.
- 요청과 응답 설정: 기본적으로 JSON 형식의 데이터를 보내고 받도록 설정되어 있으며, 필요에 따라 헤더와 파라미터를 설정할 수 있다.
axios는 널리 사용되는 HTTP 클라이언트 라이브러리 중 하나이며, 다양한 API 요청에 유연하게 사용할 수 있다.
Routing과 Components
예제 애플리케이션이 완성되면 Project Structure는 다음과 같다.
◎ src/App.js
우선 App.js 를 수정하자. 여기에 Route를 추가한다.
import React from 'react';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import BookList from "./pages/BookList";
import BookAdd from "./pages/BookAdd";
import BookView from "./pages/BookView";
import BookEdit from "./pages/BookEdit";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route exact path="/" element={<Home/>} />
<Route path="/list" element={<BookList/>} />
<Route path="/add" element={<BookAdd/>} />
<Route path="/edit/:id" element={<BookEdit/>} />
<Route path="/view/:id" element={<BookView/>} />
</Routes>
</BrowserRouter>
);
}
delete는 화면이 필요없기 때문에 공통 함수로 밑에서 보게 될 api.js에서 처리한다.
◎ src/index.js
index.js를 수정하여, bootstrap과 axios를 선언하고 axios의 base URL을 추가한다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import axios from 'axios';
const root = ReactDOM.createRoot(document.getElementById('root'));
axios.defaults.baseURL = "http://yellow.pe.kr:8081/api/books"
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
이제 src/pages 디렉토리를 생성하고, 6개의 js 파일을 생성한다.
- Home.js
- BookList.js (Book 목록)
- BookAdd.js (Book 신규 등록)
- BookView.js (Book 조회)
- BookEdit.js (Book 수정)
- api.js (Book 삭제 공통 함수로 BookList.js와 BookView.js에서 사용)
◎ src/pages/Home.js
import React from 'react';
import { Link } from "react-router-dom";
export default function Home() {
return (
<div className="container">
<div className="card">
<div className="card-header">
<h2 className="text-center mt-5 mb-3">Home</h2>
</div>
<div className="card-body">
<p><Link to={`/list`} className="btn btn-outline-success">Book 목록 보기</Link></p>
<p><Link to={`/add`} className="btn btn-outline-success">Book 등록하기</Link></p>
</div>
</div>
</div>
);
}
◎ src/pages/BookList.js
import React, { useState, useEffect} from 'react';
import { Link } from "react-router-dom";
import axios from 'axios';
import { deleteBook } from './api'; // delete 공통 함수 : api.js
export default function BookList() {
const [bookList, setBookList] = useState([]);
useEffect(() => {
fetchBookList()
}, []);
const fetchBookList = () => {
axios.get('/')
.then((response) => {
setBookList(response.data);
})
.catch((error) => {
console.log("Error while fetching books:", error);
});
}
const handleDeleteConfirm = (id) => {
if (window.confirm("정말로 삭제하시겠습니까?")) {
deleteBook(id) // delete 공통 함수 호출 : api.js
.then(() => {
console.log("Book deleted successfully.");
fetchBookList();
})
.catch((error) => {
console.log("Error while deleting book:", error);
});
}
}
return (
<div className="container">
<h2 className="text-center mt-5 mb-3">Book 목록</h2>
<div className="card">
<div className="card-header">
<Link className="btn btn-outline-primary mx-1" to="/">Home</Link>
<Link className="btn btn-outline-primary mx-1" to="/add">Book 등록</Link>
</div>
<div className="card-body">
<table className="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>제목</th>
<th>저자</th>
<th width="220px">Action</th>
</tr>
</thead>
<tbody>
{bookList.map((book, key)=>{
return (
<tr key={key}>
<td>{book.bookId}</td>
<td>{book.title}</td>
<td>{book.author}</td>
<td>
<Link to={`/view/${book.bookId}`} className="btn btn-outline-info mx-1">조회</Link>
<Link to={`/edit/${book.bookId}`} className="btn btn-outline-success mx-1">수정</Link>
<button onClick={()=>handleDeleteConfirm(book.bookId)} className="btn btn-outline-danger mx-1">삭제</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
}
◎ src/pages/BookAdd.js
import React, {useState} from 'react';
import { Link, useNavigate } from "react-router-dom";
import axios from 'axios';
export default function BookAdd() {
const [bookId, setBookId] = useState('');
const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
const [publisher, setPublisher] = useState('');
const [releaseDate, setReleaseDate] = useState('');
const [isbn, setIsbn] = useState('');
const navigate = useNavigate();
const handleSave = (event) => {
event.preventDefault();
const newBook = {
bookId: bookId,
title: title,
author: author,
publisher: publisher,
releaseDate: releaseDate,
isbn: isbn
};
axios.post("/", newBook)
.then((response) => {
console.log("Book added successfully.");
navigate("/list");
})
.catch((error) => {
console.log("Error while adding book:", error);
});
}
return (
<div className="container">
<h2 className="text-center mt-5 mb-3">Book 등록</h2>
<div className="card">
<div className="card-header">
<Link className="btn btn-outline-primary mx-1" to="/">Home</Link>
<Link className="btn btn-outline-primary mx-1" to="/list">Book 목록</Link>
</div>
<div className="card-body">
<form>
<div className="form-group">
<label htmlFor="bookId">ID</label>
<input
onChange={(event) => {setBookId(event.target.value)}}
value={bookId}
type="text"
className="form-control"
id="bookId"
name="bookId"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="title">제목</label>
<input
onChange={(event) => {setTitle(event.target.value)}}
value={title}
type="text"
className="form-control"
id="title"
name="title"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="author">저자</label>
<input
onChange={(event) => {setAuthor(event.target.value)}}
value={author}
type="text"
className="form-control"
id="author"
name="author"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="publisher">출판사</label>
<input
onChange={(event) => {setPublisher(event.target.value)}}
value={publisher}
type="text"
className="form-control"
id="publisher"
name="publisher"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="releaseDate">출판일</label>
<input
onChange={(event) => {setReleaseDate(event.target.value)}}
value={releaseDate}
type="text"
className="form-control"
id="releaseDate"
name="releaseDate"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="isbn">ISBN</label>
<input
onChange={(event) => {setIsbn(event.target.value)}}
value={isbn}
type="text"
className="form-control"
id="isbn"
name="isbn"
required>
</input>
</div>
<button onClick={handleSave} type="button" className="btn btn-outline-primary mt-3">
저장
</button>
</form>
</div>
</div>
</div>
);
}
◎ src/pages/BookView.js
import React, {useState, useEffect} from 'react';
import { Link, useParams, useNavigate } from "react-router-dom";
import axios from 'axios';
import { deleteBook } from './api'; // delete 공통 함수 : api.js
export default function BookView() {
const [id, setId] = useState(useParams().id);
const [book, setBook] = useState({bookId:'', title:'', author:'', publisher:'', releaseDate:'', isbn:''});
useEffect(() => {
axios.get(`/${id}`)
.then((response) => {
setBook(response.data);
})
.catch((error) => {
console.log("Error while geting book:", error);
})
}, []);
const navigate = useNavigate();
const handleDeleteConfirm = (id) => {
if (window.confirm("정말로 삭제하시겠습니까?")) {
deleteBook(id) // delete 공통 함수 호출 : api.js
.then(() => {
console.log("Book deleted successfully.");
navigate("/list");
})
.catch((error) => {
console.log("Error while deleting book:", error);
});
}
}
return (
<div className="container">
<h2 className="text-center mt-5 mb-3">Book 조회</h2>
<div className="card">
<div className="card-header">
<Link className="btn btn-outline-primary mx-1" to="/">Home</Link>
<Link className="btn btn-outline-primary mx-1" to="/list">Book 목록</Link>
<Link to={`/edit/${book.bookId}`} className="btn btn-outline-success mx-1">수정</Link>
<button onClick={()=>handleDeleteConfirm(book.bookId)} className="btn btn-outline-danger mx-1">삭제</button>
</div>
<div className="card-body">
<b className="text-muted">ID:</b>
<p>{book.bookId}</p>
<b className="text-muted">제목:</b>
<p>{book.title}</p>
<b className="text-muted">저자:</b>
<p>{book.author}</p>
<b className="text-muted">출판사:</b>
<p>{book.publisher}</p>
<b className="text-muted">출판일:</b>
<p>{book.releaseDate}</p>
<b className="text-muted">ISBN:</b>
<p>{book.isbn}</p>
</div>
</div>
</div>
);
}
◎ src/pages/BookEdit.js
import React, { useState, useEffect } from 'react'
import { Link, useParams, useNavigate } from "react-router-dom"
import axios from 'axios'
export default function BookEdit() {
const [id, setId] = useState(useParams().id);
const [bookId, setBookId] = useState('');
const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
const [publisher, setPublisher] = useState('');
const [releaseDate, setReleaseDate] = useState('');
const [isbn, setIsbn] = useState('');
useEffect(() => {
axios.get(`/${id}`)
.then((response) => {
let book = response.data;
setBookId(book.bookId);
setTitle(book.title);
setAuthor(book.author);
setPublisher(book.publisher);
setReleaseDate(book.releaseDate);
setIsbn(book.isbn);
})
.catch(function (error) {
console.log(error);
})
}, []);
const navigate = useNavigate();
const handleSave = (event) => {
event.preventDefault();
const newBook = {
bookId: bookId,
title: title,
author: author,
publisher: publisher,
releaseDate: releaseDate,
isbn: isbn
};
axios.put(`/${id}`, newBook)
.then((response) => {
console.log("Book edited successfully.");
navigate("/list");
})
.catch((error) => {
console.log("Error while editing book:", error);
});
}
return (
<div className="container">
<h2 className="text-center mt-5 mb-3">Book 수정</h2>
<div className="card">
<div className="card-header">
<Link className="btn btn-outline-primary mx-1" to="/">Home</Link>
<Link className="btn btn-outline-primary mx-1" to="/list">Book 목록</Link>
</div>
<div className="card-body">
<form>
<div className="form-group">
<label htmlFor="bookId">ID</label>
<input
onChange={(event) => {setBookId(event.target.value)}}
value={bookId}
type="text"
className="form-control"
id="bookId"
name="bookId"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="title">제목</label>
<input
onChange={(event) => {setTitle(event.target.value)}}
value={title}
type="text"
className="form-control"
id="title"
name="title"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="author">저자</label>
<input
onChange={(event) => {setAuthor(event.target.value)}}
value={author}
type="text"
className="form-control"
id="author"
name="author"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="publisher">출판사</label>
<input
onChange={(event) => {setPublisher(event.target.value)}}
value={publisher}
type="text"
className="form-control"
id="publisher"
name="publisher"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="releaseDate">출판일</label>
<input
onChange={(event) => {setReleaseDate(event.target.value)}}
value={releaseDate}
type="text"
className="form-control"
id="releaseDate"
name="releaseDate"
required>
</input>
</div>
<div className="form-group">
<label htmlFor="isbn">ISBN</label>
<input
onChange={(event) => {setIsbn(event.target.value)}}
value={isbn}
type="text"
className="form-control"
id="isbn"
name="isbn"
required>
</input>
</div>
<button onClick={handleSave} type="button" className="btn btn-outline-primary mt-3">
저장
</button>
</form>
</div>
</div>
</div>
);
}
◎ src/pages/api.js
import axios from 'axios';
export const deleteBook = async (id) => {
try {
const response = await axios.delete(`/${id}`);
} catch (error) {
throw error;
}
}
App 실행
이제 App을 실행해보자.
C:\app\ReactProjects\books-react-app\npm start
이후 브라우저에서 다음의 URL을 실행한다.
● http://localhost:3000
Book 목록 화면을 보자.
● http://localhost:3000/list
새로운 Book을 등록하자.
● http://localhost:3000/add
[저장] 버튼을 누르면, “Book 목록” 화면으로 이동한다. 등록된 Book을 확인할 수 있다.
● http://localhost:3000/list
ID “1003”인 “신화의 이미지”를 조회해보자.
● http://localhost:3000/view/1003
……
수정, 삭제도 정상적으로 수행된다.