관리 메뉴

거니의 velog

(13) 리액트와 API 서버 통신 3 본문

SpringBoot_React 풀스택 프로젝트

(13) 리액트와 API 서버 통신 3

Unlimited00 2024. 2. 28. 19:57

5. 목록 데이터 처리

* 조회는 단순하게 서버의 데이터를 보여주는 기능만으로 구현할 수 있지만, 목록 데이터는 페이지 처리나 여러 링크 등이 추가되어서 더 복잡한 구성을 하게 된다. 가장 먼저 구현할 기능은 브라우저 경로에 있는 쿼리스트링으로 페이지 번호/사이즈에 따라서 서버를 호출하고 결과를 출력하는 것이다.


(1) 목록 데이터 가져오기

* 해당 데이터를 가져오는 기능은 useEffect()를 사용해서 구현하는 것으로 컴포넌트는 components/todo 폴더 내에 ListComponent로 작성한다.

* ListComponent는 useCustomMove()를 이용해서 현재 경로의 page와 size를 구성하고 API 서버를 호출한다. 서버의 데이터는 dtoList라는 배열 데이터와 pageNumList라는 페이지 번호들이 존재하고, 이전(prev)/다음(next) 등의 추가적인 데이터들이 있다.

import React, { useEffect, useState } from "react";
import { getList } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";

const initState = {
  dtoList: [],
  pageNumList: [],
  pageRequestDTO: null,
  prev: false,
  next: false,
  totalCount: 0,
  prevPage: 0,
  nextPage: 0,
  totalPage: 0,
  current: 0,
};

const ListComponent = () => {
  const { page, size } = useCustomMove();

  // serverData는 나중에 사용
  const [serverData, setServerData] = useState(initState);

  useEffect(() => {
    getList({ page, size }).then((data) => {
      console.log(data);
      setServerData(data);
    });
  }, [page, size]);

  return <div>Todo List Component</div>;
};

export default ListComponent;

* ListComponent는 useCustomMove를 이용하기 때문에 현재 컴포넌트에서 필요한 page나 size와 같은 데이터를 쉽게 구할 수 있다. 마지막으로 작성된 ListComponent를 ListPage에서 import 한다.

import React from "react";
import ListComponent from "../../components/todo/ListComponent";

const ListPage = () => {
  return (
    <div className="p-4 w-full bg-white">
      <div className="text-3xl font-extrabold">Todo List Page Component</div>
      <ListComponent />
    </div>
  );
};

export default ListPage;

* 브라우저에서 '/todo/list'를 호출해서 ListComponent가 정상적으로 추가되었는지 확인한다.

* 마지막으로 서버에서 가져온 데이터들을 ListComponent에서 출력해 준다.

import React, { useEffect, useState } from "react";
import { getList } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";

(...)

const ListComponent = () => {
  (...)

  return (
    <div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
      <div className="flex flex-wrap mx-auto justify-center p-6">
        {serverData.dtoList.map((todo) => (
          <div
            key={todo.tno}
            className="w-full min-w-[400px] p-2 m-2 rounded shadow-md"
          >
            <div className="flex">
              <div className="font-extrabold text-2xl p-2 w-1/12">
                {todo.tno}
              </div>
              <div className="text-1xl m-1 p-2 w-8/12 font-extrabold">
                {todo.title}
              </div>
              <div className="text-1xl m-1 p-2 w-2/10 font-medium">
                {todo.dueDate}
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ListComponent;

* 브라우저에서는 '/todo/list'를 호출했을 경우 1페이지가 출력되는 것을 확인할 수 있고, '?page=3&size=10'을 이용해서 원하는 결과를 확인할 수 있다.


(2) 페이징 처리

* API 서버를 통해서 전달받은 데이터에는 목록뿐 아니라 페이징 처리에 필요한 모든 경로가 같이 전달된다.

* 페이징의 경우 현재 개발중인 기능이 아니더라도 모든 목록 관련된 기능에서 사용해야 하므로 공통의 컴포넌트로 제작해서 사용하는 것이 유용하다.

* components 폴더 내에 공통으로 많이 사용될 컴포넌트들을 모아둘 common 폴더를 생성하고 PageComponent.js 를 추가한다.

* PageComponent는 ListComponent가 받아오는 서버의 데이터와 useCustomMove()에서 만들어진 moveToList()를 movePage 속성으로 전달받도록 구성하고 이를 활용한다.

import React from "react";

const PageComponent = ({ serverData, movePage }) => {
  return (
    <div className="m-6 flex justify-center">
      {serverData.prev ? (
        <div
          className="m-2 p-2 w-16 text-center font-bold text-blue-400"
          onClick={() => movePage({ page: serverData.prevPage })}
        >
          Prev
        </div>
      ) : (
        <></>
      )}

      {serverData.pageNumList.map((pageNum) => (
        <div
          key={pageNum}
          className={`m-2 p-2 w-12 text-center rounded shadow-md text-white ${
            serverData.current === pageNum ? "bg-gray-500" : "bg-blue-400"
          }`}
          onClick={() => movePage({ page: pageNum })}
        >
          {pageNum}
        </div>
      ))}

      {serverData.next ? (
        <div
          className="m-2 p-2 w-16 text-center font-bold text-blue-400"
          onClick={() => movePage({ page: serverData.nextPage })}
        >
          Next
        </div>
      ) : (
        <></>
      )}
    </div>
  );
};

export default PageComponent;

* ListComponent는 PageComponent를 import 해서 추가한다.

import React, { useEffect, useState } from "react";
import { getList } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import PageComponent from "../common/PageComponent";

(...)

const ListComponent = () => {
  const { page, size, moveToList } = useCustomMove(); // moveToList가 추가적으로 필요

  // serverData는 나중에 사용
  const [serverData, setServerData] = useState(initState);

  useEffect(() => {
    getList({ page, size }).then((data) => {
      console.log(data);
      setServerData(data);
    });
  }, [page, size]);

  return (
    <div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
      <div className="flex flex-wrap mx-auto justify-center p-6">
        {serverData.dtoList.map((todo) => (
          <div
            key={todo.tno}
            className="w-full min-w-[400px] p-2 m-2 rounded shadow-md"
          >
            <div className="flex">
              <div className="font-extrabold text-2xl p-2 w-1/12">
                {todo.tno}
              </div>
              <div className="text-1xl m-1 p-2 w-8/12 font-extrabold">
                {todo.title}
              </div>
              <div className="text-1xl m-1 p-2 w-2/10 font-medium">
                {todo.dueDate}
              </div>
            </div>
          </div>
        ))}
      </div>

      <PageComponent
        serverData={serverData}
        movePage={moveToList}
      ></PageComponent>
    </div>
  );
};

export default ListComponent;

* 화면에서는 목록의 아래쪽에 페이지 번호들이 출력되는 것을 확인할 수 있고 페이지 번호를 클릭했을 때 주소창이 변경되면서 새로운 데이터로 갱신되는 것을 확인할 수 있다. 화면 아래쪽의 페이지 번호들은 데이터의 수에 따라서 'Next'나 'Prev'가 보이게 된다.

* 화면에서 페이지 번호를 클릭하면 주소창이 변경되고, 서버로부터 새로운 데이터가 전송되는 것을 확인할 수 있다.


(3) 동일 페이지 클릭 시 문제

* 완성된 ListPage와 ListComponent를 이용하면 페이지 번호들을 클릭했을 때 현재 페이지와 다른 페이지 번호를 클릭하면 정상적으로 동작하는데 비해, 현재 페이지를 다시 클릭했을 때는 서버 호출을 하지 않는다는 사실을 알게 된다.

* 이렇게 동작하는 이유는 ListComponent의 useEffect()가 사용한 page와 size로 설정된 의존성 때문이다. useEffect()의 의존성 설정 대상이 변경되어야만 useEffect()가 지정한 함수가 실행된다. 현재와 같이 page, size 속성값이 변경되었을 때만 서버를 호출하도록 구성되어 있다면 동일한 페이지나 사이즈에 대해서는 다시 호출될 필요가 없게 된다.

* 동일한 page와 size 라고 하더라도 매번 서버를 호출하고 싶다면 컴포넌트 내부에 매번 변하는 상태(state)값을 이용하는 것이 좋다. 예를 들어 true/false 값이 번갈아 가면서 변경되거나 계속 올라가는 숫자, 현재 시간 등을 이용할 수 있다. 예제에서는 true/false 값을 이용해 본다.

* useCustomMove 의 내부에 refresh 변수를 상태로 선언하고 moveToList() 가 호출될 때마다 이를 변경하도록 한다.

import { useState } from "react";
import {
  createSearchParams,
  useNavigate,
  useSearchParams,
} from "react-router-dom";

const getNum = (param, defaultValue) => {
  if (!param) {
    return defaultValue;
  }
  return parseInt(param);
};

const useCustomMove = () => {
  const navigate = useNavigate();
  const [refresh, setRefresh] = useState(false); // 추가
  const [queryParams] = useSearchParams();

  const page = getNum(queryParams.get("page"), 1);
  const size = getNum(queryParams.get("size"), 10);

  const queryDefault = createSearchParams({ page, size }).toString(); // 새로 추가

  const moveToList = (pageParam) => {
    let queryStr = "";
    if (pageParam) {
      const pageNum = getNum(pageParam.page, 1);
      const sizeNum = getNum(pageParam.size, 10);

      queryStr = createSearchParams({
        page: pageNum,
        size: sizeNum,
      }).toString();
    } else {
      queryStr = queryDefault;
    }

    navigate({
      pathname: `../list`,
      search: queryStr,
    });

    setRefresh(!refresh); // 추가
  };

  const moveToModify = (num) => {
    console.log(queryDefault);

    navigate({
      pathname: `../modify/${num}`,
      search: queryDefault, // 수정시에 기존의 쿼리 스트링 유지를 위해
    });
  };

  return { moveToList, moveToModify, page, size, refresh }; // refresh 추가
};

export default useCustomMove;

* useCustomMove에서 반환되는 refresh 값은 ListComponent에서 useEffect() 에서 사용하도록 설정한다. refresh 값은 페이지 번호를 클릭하면 매번 변경되기 쉽기 때문에 useEffect() 의 조건이 된다.

const ListComponent = () => {
  const { page, size, refresh, moveToList } = useCustomMove(); // refresh 추가
  const [serverData, setServerData] = useState(initState);

  useEffect(() => {
    getList({ page, size }).then((data) => {
      console.log(data);
      setServerData(data);
    });
  }, [page, size, refresh]); // refresh 추가
 
  (...)
 
}

* 브라우저의 실행 결과를 보면 동일한 페이지 번호를 클릭했을 때 서버를 통해 매번 새로운 데이터를 가져오는 부분을 확인할 수 있다.


(4) 조회 페이지 이동

* ListComponent에서 마지막으로 구현할 기능은 특정 Todo의 번호(tno)를 클릭해서 조회 화면으로 이동하는 것이다. 이 기능 역시 useCustomMove()를 이용해서 moveToRead()와 같은 함수를 하나 추가해 준다.

import { useState } from "react";
import {
  createSearchParams,
  useNavigate,
  useSearchParams,
} from "react-router-dom";

(...)

const useCustomMove = () => {
  (...)

  const moveToRead = (num) => {
    console.log(queryDefault);

    navigate({
      pathname: `../read/${num}`,
      search: queryDefault,
    });
  };

  return { moveToList, moveToModify, moveToRead, page, size, refresh }; // moveToRead 추가
};

export default useCustomMove;

* ListComponent에서는 반환되는 moveToRead를 이용해서 각 Todo에 대한 이벤트를 처리한다.

import React, { useEffect, useState } from "react";
import { getList } from "../../api/todoApi";
import useCustomMove from "../../hooks/useCustomMove";
import PageComponent from "../common/PageComponent";

(...)

const ListComponent = () => {
  const { page, size, refresh, moveToList, moveToRead } = useCustomMove(); // moveToRead 추가
  const [serverData, setServerData] = useState(initState);

  (...)

  return (
    <div className="border-2 border-blue-100 mt-10 mr-2 ml-2">
      <div className="flex flex-wrap mx-auto justify-center p-6">
        {serverData.dtoList.map((todo) => (
          <div
            key={todo.tno}
            className="w-full min-w-[400px] p-2 m-2 rounded shadow-md"
            onClick={() => moveToRead(todo.tno)} // 클릭 이벤트 추가 처리
          >
            <div className="flex">
              <div className="font-extrabold text-2xl p-2 w-1/12">
                {todo.tno}
              </div>
              <div className="text-1xl m-1 p-2 w-8/12 font-extrabold">
                {todo.title}
              </div>
              <div className="text-1xl m-1 p-2 w-2/10 font-medium">
                {todo.dueDate}
              </div>
            </div>
          </div>
        ))}
      </div>

      <PageComponent
        serverData={serverData}
        movePage={moveToList}
      ></PageComponent>
    </div>
  );
};

export default ListComponent;

* 브라우저에서는 목록 페이지에서 특정 번호를 클릭해서 조회 페이지로 이동하는데, 문제가 없는지 확인하고 다시 조회 페이지에서 목록 페이지로 이동이 가능한지 확인한다.