관리 메뉴

거니의 velog

(11) 리액트와 API 서버 통신 1 본문

SpringBoot_React 풀스택 프로젝트

(11) 리액트와 API 서버 통신 1

Unlimited00 2024. 2. 28. 17:09

* 대부분의 프론트엔드 관련 기술들은 서버와의 비동기 통신을 포함한다. 리액트 역시 Ajax로 서버의 데이터를 이용해서 필요한 기능을 완성한다. 이번 장은 이전에 구성한 서버를 이용해서 Todo의 기능을 완성해 보자.

* 이번 장의 개발 목표는 다음과 같다.

(1) Axios 라이브러리를 이용한 서버와의 통신

(2) useEffect()를 활용한 비동기 처리와 상태 변경

(3) 커스텀 훅을 이용한 공통 코드 재사용하기

(4) 컴포넌트에서 모달창을 이용해서 결과 보여주기

1. 개발 목표의 이해

* React-Router로 브라우저의 주소창을 처리했고, 스프링 부트로 서버사이드에서 원하는 데이터를 처리할 수 있게 되었다면 이제 각 화면 내부에 필요한 컴포넌트들을 추가해서 실제 내용을 구현해 본다.

* 목록 화면은 '/todo/list' 경로로 접근한다. 화면에서 페이지 번호를 출력하고 해당 페이지의 번호를 클릭하면 다른 페이지로 이동이 가능하도록 구현한다. 페이지 이동에서 가장 중요한 부분은 주소창의 경로가 변경되는 것을 기준으로 해당 값들을 이용하게 설계한다는 점이다. 이렇게 하는 이유는 GET 방식의 주소는 사용자들이 일반 웹페이지처럼 링크를 사용해서 메시지를 보내거나 메일 등의 링크로 설정할 수 있도록 하기 위함이다.

* '/todo/list'의 메뉴 중에서 'ADD' 링크를 통하면 새로운 Todo를 등록할 수 있는 화면으로 이동하게 되고, 새로운 Todo가 등록되면 모달창에서 결과를 보여준다. 서버의 처리가 끝나기 전까지 다른 컴포넌트를 보여주지 않도록 처리되어야 한다. 화면에서 모달창이 닫히면 추가된 결과를 확인할 수 있는 목록 화면인 '/todo/list'로 이동시켜 준다.

* 목록 화면에서 특정한 번호를 클릭하면 페이지 정보를 유지하면서 '/todo/read/번호?page=XX&size=XX'로 이동하게 된다. 이 화면 역시 사용자가 메신저 등을 이용해 외부에 노출할 수 있도록 주소창에 필요한 모든 정보를 출력해야 한다.

* 조회 화면에서는 수정/삭제가 가능한 화면으로 이동하거나 원래의 목록 화면으로 돌아갈 수 있는 기능을 제공한다. 이때는 기존의 페이지가 유지되도록 한다.

* 수정/삭제 화면에서 수정 작업을 할 때는 모달창에서 결과를 보여주고 조회 화면으로 이동시켜서 결과를 다시 확인할 수 있게 한다.

* 삭제 처리 후에는 모달 창에서 결과를 보여 주고 목록 화면으로 이동시킨다.


2. Ajax 통신 처리

* Ajax 를 이용하는 부분은 Axios 라이브러리를 추가해서 개발한다. Axios의 경우 기본적으로 데이터 형식이 JSON을 사용하기 때문에 개발 분량도 적다는 장점이 있다.

* 프로젝트의 터미널 환경에서 Axios 라이브러리를 추가한다.

$ npm install axios

* Axios를 이용하는 부분은 컴포넌트에서 쉽게 사용할 수 있도록 별도의 함수들로 제작한다. 프로젝트 내에 api 폴더를 추가하고 todoApi.js 파일을 추가한다.

import axios from "axios";

export const API_SERVER_HOST = "http://localhost:8080";

const prefix = `${API_SERVER_HOST}/api/todo`;

export const getOne = async (tno) => {
  const res = await axios.get(`${prefix}/${tno}`);
  return res.data;
};

export const getList = async (pageParam) => {
  const { page, size } = pageParam;
  const res = await axios.get(`${prefix}/list`, {
    params: { page: page, size: size },
  });
  return res.data;
};

* getOne() 은 특정 번호의 Todo를 조회하기 위해서 사용하고, getList()는 페이지 처리를 위해서 사용한다.


3. useEffect()

* 비동기 처리는 함수를 호출하고 호출 결과를 기다려서 다음 처리가 진행되는 방식이 아니고 나중에 결과를 처리하기 때문에 조금 다른 방식의 처리가 필요하다.

* 리액트의 경우 컴포넌트에서 비동기 방식으로 호출했다면 호출 결과를 처리한 후에 상태(state)를 변경해서 처리한다. 문제는 컴포넌트의 상태가 변경되었기 때문에 컴포넌트는 다시 렌더링이 호출된다는 점이다. 다시 렌더링이 되면서 비동기 호출을 하게 되고 잠시 후 다시 상태가 갱신되는 악순환이 계속된다.

* 리액트에서는 useEffect()로 컴포넌트 내에 특정한 상황을 만족하는 경우에만 특정한 동작을 수행하는 방법을 제공한다. useEffect()를 사용해서 주로 다음과 같은 상황을 처리한다.

* 컴포넌트 실행 과정에서 한 번만 실행해야 하는 비동기 처리

* 컴포넌트의 여러 상태 중에서 특정한 상태만 변경되었을 경우에 비동기 처리

(1) 조회를 위한 컴포넌트

* pages 폴더는 주로 URL 처리를 위한 컴포넌트이고, 실제 작업은 components 폴더를 이용해서 구성한다. 작성하려는 기능들은 todo 폴더를 작성해서 컴포넌트들을 추가한다. todo 폴더에 조회 기능을 위한 ReadComponent.js 파일을 추가한다.

* ReadComponent는 특정한 번호(tno)의 값에 의해서 todoApi.js의 getOne()을 호출하도록 구성한다. useEffect()를 사용해서 번호(tno)가 변경되었을 때만 Axios를 이용하는 getOne()을 호출하도록 구성한다. 비동기 통신으로 가져온 데이터는 컴포넌트 상태로 반영한다.

import React, { useEffect, useState } from "react";
import { getOne } from "../../api/todoApi";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: null,
  complete: false,
};

const ReadComponent = ({ tno }) => {
  const [todo, setTodo] = useState(initState); // 아직 todo는 사용하지 않음

  useEffect(() => {
    getOne(tno).then((data) => {
      console.log(data);
      setTodo(data);
    });
  }, [tno]);

  return <div></div>;
};

export default ReadComponent;

* pages/todo 폴더 내 ReadPage 컴포넌트에서는 작성된 ReadComponent를 import한다(기타 폰트의 크기나 넓이, 배경 색상 등을 같이 조정한다).

import React, { useCallback } from "react";
import {
  useNavigate,
  useParams,
  createSearchParams, // 페이지 이동 시에 필요한 쿼리스트링을 생성하여 파라미터를 다음 페이지로 전달
  useSearchParams, // 쿼리 스트링으로 전달된 데이터 확인
} from "react-router-dom";
import ReadComponent from "../../components/todo/ReadComponent";

const ReadPage = () => {
  (...)

  return (
    /* text-3xl 삭제 */
    <div className="font-extrabold w-full bg-white mt-6">
      <div className="text-2xl">Todo Read Page Component {tno}</div>
      <ReadComponent tno={tno}></ReadComponent>
    </div>
  );
};

export default ReadPage;

* 프로젝트를 실행하고 '/todo/read/33'과 같이 ReadPage 컴포넌트를 호출하면 화면에는 변화가 없지만 브라우저 내 개발자 도구로 확인해 보면 서버와 통신이 이루어지고 ReadComponent의 useEffect()가 호출된 것을 확인할 수 있다.

* 실행 전에 스프링 부트로 개발된 API 서버가 시작되어 있어야 바르게 동작한다.

- http://localhost:3000/todo/read/34

* 위 그림에서 Axios 호출이 두 번 되는 이유는 비동기 통신을 이용하는 경우 <StrictMode> 설정으로 비동기 호출은 위의 그림 왼쪽과 같이 두 번 호출 되는 경우가 발생한다. 이를 해결하려면 src/index.js 파일을 수정해서 strict 모드를 사용하지 않도록 수정해야 한다.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

// 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();

수정 이후에는 1번만 잘 동작하는 모습을 확인할 수 있다.

* ReadComponent 내부에서는 useState를 사용해서 서버에서 가져온 데이터를 보관했기 때문에 이를 출력한다. 출력할 때는 공통적인 스타일과 구성이 많으므로 컴포넌트는 아니지만 JSX를 반환하는 함수를 사용해서 코드의 양을 줄일 수 있다.

import React, { useEffect, useState } from "react";
import { getOne } from "../../api/todoApi";

const initState = {
  tno: 0,
  title: "",
  writer: "",
  dueDate: null,
  complete: false,
};

const makeDiv = (title, value) => (
  <div className="flex justify-center">
    <div className="relative mb-4 flex w-full flex-wrap items-stretch">
      <div className="w-1/5 p-6 text-right font-bold">{title}</div>
      <div className="w-4/5 p-6 rounded-r border border-solid shadow-md">
        {value}
      </div>
    </div>
  </div>
);

const ReadComponent = ({ tno }) => {
  const [todo, setTodo] = useState(initState);

  useEffect(() => {
    getOne(tno).then((data) => {
      console.log(data);
      setTodo(data);
    });
  }, [tno]);

  return (
    <div className="border-2 border-sky-200 mt-10 m-2 p-4">
      {makeDiv("Tno", todo.tno)}
      {makeDiv("Writer", todo.writer)}
      {makeDiv("Title", todo.title)}
      {makeDiv("Due Date", todo.dueDate)}
      {makeDiv("Complete", todo.complete ? "Completed" : "Not Yet")}
    </div>
  );
};

export default ReadComponent;

* 코드 상에서 화면을 구성하는 부분은 거의 동일하기 때문에 makeDiv() 라는 함수로 중복적인 코드를 없애도록 작성되었다. 브라우저를 통해서 보이는 결과는 아래 화면과 같다.