관리 메뉴

거니의 velog

(5) React-Router 3 본문

SpringBoot_React 풀스택 프로젝트

(5) React-Router 3

Unlimited00 2024. 2. 26. 20:29

(10) 경로 처리를 위한 useParams()

* 위의 화면과 같이 특정 번호의 경로를 사용하는 경우 컴포넌트에서는 주소창에 있는 경로의 일부를 활용해야 한다. React-Router에서는 useParams()를 이용해서 지정된 변수를 추출할 수 있다. ReadPage 컴포넌트에서 tno 변수로 전달되는 값을 추출해서 출력한다.

import React from "react";
import { useParams } from "react-router-dom";

const ReadPage = () => {
  const { tno } = useParams();

  return (
    <div className="text-3xl font-extrabold">
      Todo Read Page Component {tno}
    </div>
  );
};

export default ReadPage;

* useParams() 경로의 일부가 변수로 처리된 경우 해당 이름의 변수를 추출할 수 있다. 브라우저를 통해서 전달되는 번호의 값을 화면에서 확인해 볼 수 있다. 아래 화면은 '/todo/read/33'과 같은 경로가 호출되는 경우의 결과이다.



[1] useSearchParams()
* useParams()가 경로 자체의 값을 사용하는데 비해 '?' 이후에 나오는 쿼리스트링은 useSearchParams()를 이용할 수 있다. 예를 들어 목록 페이지에는 페이징/검색 기능으로 인해서 '/todo/list?page=3&size=20' 과 유사한 형태의 경로와 쿼리스트링이 사용되는데 이 경우 useSearchParams()를 이용해서 원하는 쿼리스트링의 값을 추출할 수 있다.

import React from "react";
import { useSearchParams } from "react-router-dom";

const ListPage = () => {
  const [queryParams] = useSearchParams();

  const page = queryParams.get("page") ? parseInt(queryParams.get("page")) : 1;
  const size = queryParams.get("size") ? parseInt(queryParams.get("size")) : 10;

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

export default ListPage;

* 수정된 코드가 반영되면 경로에 쿼리스트링이 있는 경우(/todo/list?page=2&size=20)에는 해당 값을 이용하고 그렇지 않으면 1, 10과 같은 값을 사용하게 된다.

쿼리스트링에 파라미터가 있는 경우
쿼리스트링이 없는 경우


[2] useNavigate()
* React-Router를 이용하면 고정된 링크로 이동할 때도 있지만, 대부분은 상황에 따라서 동적으로 데이터를 처리해서 이동하는 경우가 더 많다. 이럴 때는 <Navigate>나 <Link> 대신에 useNavigate()를 이용해서 해결한다.
* IndexPage.js에는 목록 페이지(LIST)와 등록 페이지(ADD)로 이동할 수 있는 링크를 제공하는데 <Link> 대신에 useNagivate()를 이용해서 이를 처리해 보겠다. pages/todo/IndexPage.js 컴포넌트의 각 링크에 대한 이벤트 처리는 다음과 같이 적용한다.

import React, { useCallback } from "react";
import BasicLayout from "../../layouts/BasicLayout";
import { Outlet, useNavigate } from "react-router-dom";

const IndexPage = () => {
  const navigate = useNavigate();

  const handleClickList = useCallback(() => {
    navigate({ pathname: "list" });
  }, []);

  const handleClickAdd = useCallback(() => {
    navigate({ pathname: "add" });
  }, []);

  return (
    <BasicLayout>
      <div className="w-full flex m-2 p-2 ">
        <div
          className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
          onClick={handleClickList}
        >
          LIST
        </div>
        <div
          className="text-xl m-1 p-2 w-20 font-extrabold text-center underline"
          onClick={handleClickAdd}
        >
          ADD
        </div>
      </div>
      <div className="flex flex-wrap w-full">
        <Outlet />
      </div>
    </BasicLayout>
  );
};

export default IndexPage;

* useCallback에서 두 번째 매개변수로 의존성 배열을 전달해야 하는데,  여기서는 컴포넌트의 범위에서 가져온 어떤 변수에도 의존하지 않으므로 두 함수에 빈 의존성 배열([ ])을 전달하면 된다.


* 새로운 Todo를 등록할 수 있는 페이지 컴포넌트를 pages/todo/AddPage.js 파일을 생성해서 추가한다.

import React from "react";

const AddPage = () => {
  return <div className="text-3xl font-extrabold">Todo Add Page</div>;
};

export default AddPage;

* router/todoRouter.js에는 '/todo/add' 경로에 대한 설정을 추가한다.

import React, { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";

const Loading = <div>Loading...</div>;
const TodoList = lazy(() => import("../pages/todo/ListPage"));
const TodoRead = lazy(() => import("../pages/todo/ReadPage"));
const TodoAdd = lazy(() => import("../pages/todo/AddPage"));

const todoRouter = () => {
  return [
    {
      path: "list",
      element: (
        <Suspense fallback={Loading}>
          <TodoList />
        </Suspense>
      ),
    },
    {
      path: "",
      element: <Navigate replace to="list" />,
    },
    {
      path: "read/:tno",
      element: (
        <Suspense fallback={Loading}>
          <TodoRead />
        </Suspense>
      ),
    },
    {
      path: "add",
      element: (
        <Suspense fallback={Loading}>
          <TodoAdd />
        </Suspense>
      ),
    },
  ];
};

export default todoRouter;

* '/todo/'로 접근하면 todo/IndexPage.js가 제공하는 하위 메뉴들을 볼 수 있고 'ADD'를 클릭해서 이동이 가능해진다.


(11)  동적 페이지 이동

* 조회 화면에서는 수정/삭제로 이동할 수 있는 버튼을 추가해 본다. 수정/삭제 작업은 매번 달라지는 번호(tno)를 사용하기 때문에 고정된 링크가 아니라 useParams()로 찾아낸 번호를 사용해야만 한다. 또한, 조회 화면에서 다시 목록(todo/list) 화면으로 이동할 때는 페이지 번호가 매번 달라질 수 있으므로 이에 대한 동적 처리 역시 필요하다.



[1] 조회 -> 수정/삭제 이동
* ReadPage에서는 수정/삭제가 가능한 '/todo/modify/번호' 경로로 이동할 수 있는 기능을 추가한다.

import React, { useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";

const ReadPage = () => {
  const { tno } = useParams();
  const navigate = useNavigate();

  const moveToModify = useCallback(
    (tno) => {
      navigate({ pathname: `/todo/modify/${tno}` });
    },
    [navigate] // navigate 함수를 의존성 배열에 추가
  );

  return (
    <div className="text-3xl font-extrabold">
      Todo Read Page Component {tno}
      <div>
        <button onClick={() => moveToModify(33)}>Test Modify</button>
      </div>
    </div>
  );
};

export default ReadPage;

* 브라우저에서는 화면에 임시로 만든 <button> 을 이용해서 '/todo/modify/33'으로 이동하는 것을 확인할 수 있다. 테스트를 위해 '/todo/read/33'으로 접근한 후에 버튼을 클릭해서 '/todo/modify/33'으로 이동하는지 확인한다.

클릭!
해당 주소로 잘 이동하는 것을 볼 수 있다.


[쿼리스트링의 유지]
* 조회 페이지는 다시 목록으로 이동할 수 있기 때문에 page와 size 처럼 쿼리스트링으로 전달되는 데이터들을 유지하면서 이동해야 한다. 예를 들어 아래와 같이 사용자가 page, size를 유지하고 있는 경우를 생각해 보자.

http://localhost:3000/todo/read/33?page=3&size=10

* 위의 상황에서 'Test Modify'를 클릭하면 쿼리스트링은 유지가 되지 않는 것을 확인할 수 있다.

* 왜냐하면, 전에도 말했듯, 자바스크립트는 페이지의 이동과 같은 '새로고침'이 일어나면 상태값이 초기화되기 때문이다.
* 따라서, useSearchParams()를 이용해서 쿼리스트링으로 전달된 데이터를 확인하고 createSearchParams()라는 React-Router의 함수를 이용해서 '/todo/modify/xx'로 이동 시에 필요한 쿼리스트링을 만들어내서 navigate()를 이용한 이동 시에 활용한다.

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

const ReadPage = () => {
  const { tno } = useParams();
  const navigate = useNavigate();
  const [queryParams] = useSearchParams(); // 파라미터는 여러 건[page, size]이 올 수 있으므로, [구조 분해 문법 > 배열 비구조화 할당]을 통해 한 번에 받아 옴.
  const page = queryParams.get("page") ? parseInt(queryParams.get("page")) : 1;
  const size = queryParams.get("size") ? parseInt(queryParams.get("size")) : 10;
  const queryStr = createSearchParams({ page, size }).toString();

  const moveToModify = useCallback(
    (tno) => {
      navigate({ pathname: `/todo/modify/${tno}`, search: queryStr });
    },
    [navigate, queryStr] // navigate 함수, queryStr을 의존성 배열에 추가
  );

  return (
    <div className="text-3xl font-extrabold">
      Todo Read Page Component {tno}
      <div>
        <button onClick={() => moveToModify(tno)}>Test Modify</button>
      </div>
    </div>
  );
};

export default ReadPage;

* useCallback에서의 의존성 배열에 queryStr이 빠져있어서 ESLint에서 해당 경고가 발생하는데, queryStruseCallback 내부에서 사용되므로 해당 변수를 의존성 배열에 추가하여 문제를 해결할 수 있다.
* 브라우저를 통해서 조회 화면에서 수정 화면으로 이동할 때 쿼리스트링이 유지되는지 확인해 보자.



[2] 조회 -> 목록 이동
* 조회 화면에서는 다시 목록으로 이동하는 경우도 많기 때문에 이를 위한 함수도 추가한다.

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

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

  const moveToList = useCallback(
    () => {
      navigate({ pathname: `/todo/list`, search: queryStr });
    },
    [navigate, queryStr] // navigate 함수, queryStr을 의존성 배열에 추가
  );

  return (
    <div className="text-3xl font-extrabold">
      Todo Read Page Component {tno}
      <div>
        <button onClick={() => moveToModify(tno)}>Test Modify</button>
        <button onClick={() => moveToList()}>Test List</button>
      </div>
    </div>
  );
};

export default ReadPage;

* 화면에는 'Test List'를 이용해서 쿼리스트링을 유지한 채로 목록 화면으로 이동하는 것을 확인할 수 있다.



[3] 수정/삭제 페이지
* 수정/삭제 페이지는 pages/todo 폴더 내에 ModifyPage라는 이름으로 작성한다.

import React from "react";

const ModifyPage = () => {
  return <div className="text-3xl font-extrabold">Todo Modify Page</div>;
};

export default ModifyPage;

* todoRouter.js를 이용해서 ModifyPage의 라우팅 설정을 추가한다.

import React, { Suspense, lazy } from "react";
import { Navigate } from "react-router-dom";

const Loading = <div>Loading...</div>;
const TodoList = lazy(() => import("../pages/todo/ListPage"));
const TodoRead = lazy(() => import("../pages/todo/ReadPage"));
const TodoAdd = lazy(() => import("../pages/todo/AddPage"));
const TodoModify = lazy(() => import("../pages/todo/ModifyPage"));

const todoRouter = () => {
  return [
    {
      path: "list",
      element: (
        <Suspense fallback={Loading}>
          <TodoList />
        </Suspense>
      ),
    },
    {
      path: "",
      element: <Navigate replace to="list" />,
    },
    {
      path: "read/:tno",
      element: (
        <Suspense fallback={Loading}>
          <TodoRead />
        </Suspense>
      ),
    },
    {
      path: "add",
      element: (
        <Suspense fallback={Loading}>
          <TodoAdd />
        </Suspense>
      ),
    },
    {
      path: "modify/:tno",
      element: (
        <Suspense fallback={Loading}>
          <TodoModify />
        </Suspense>
      ),
    },
  ];
};

export default todoRouter;

* 브라우저에서는 조회 화면에서 수정/삭제로 이동하거나 직접 '/todo/modify/33'과 같은 주소를 통해 ModifyPage의 동작을 확인한다.



[수정/삭제 처리 후 이동]
*  수정의 경우 수정된 결과를 확인할 수 있는 조회 화면으로 다시 이동이 가능해야 하고, 삭제의 경우 목록 화면으로 이동하는 경우가 많다. ModifyPage에서는 이를 위한 기능들을 useNavigate()를 이용해서 작성해 두도록 한다(API 서버를 이용하면서 완성하도록 하자).

import React from "react";
import { useNavigate } from "react-router-dom";

const ModifyPage = ({ tno }) => {
  const navigate = useNavigate();
  const moveToRead = () => {
    navigate({ pathname: `/todo/read/${tno}` });
  };
  const moveToList = () => {
    navigate({ pathname: `/todo/list` });
  };

  return <div className="text-3xl font-extrabold">Todo Modify Page</div>;
};

export default ModifyPage;

* 리액트를 이용하는 애플리케이션의 경우 가장 먼저 결정해야 하는 포인트는 현재 구성하려는 애플리케이션이 URL 중심으로 처리되어야 하는지에 대한 고민이다. 대부분의 프로젝트의 경우 URL을 기준으로 SNS나 메일, 검색엔진 등을 이용해서 정보를 배포하므로 본격적인 프로젝트의 개발 전에 이동해야 하는 모든 경로들을 설계할 필요가 있다.


 

'SpringBoot_React 풀스택 프로젝트' 카테고리의 다른 글

(7) 스프링 부트와 API 서버 2  (0) 2024.02.28
(6) 스프링 부트와 API 서버 1  (0) 2024.02.26
(4) React-Router 2  (0) 2024.02.26
(3) React-Router 1  (0) 2024.02.26
(2) 개발 환경설정  (0) 2024.02.26