[React] REST API 호출

개요

여기에서는 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

……

수정, 삭제도 정상적으로 수행된다.

[React] REST API 호출
Tagged on:     

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.