관리 메뉴

거니의 velog

(3) React-Router 1 본문

SpringBoot_React 풀스택 프로젝트

(3) React-Router 1

Unlimited00 2024. 2. 26. 17:17

* 웹사이트를 만들 때 중요한 작업 중 하나는 IA(Information Architecture)를 기획하는 것이다. IA는 쉽게 말해서 '메뉴 경로'를 정리한 것이다. 각 페이지 간의 링크나 <form>을 통해서 필요한 정보를 조회하거나 처리하는 작업을 설계한다.
* 리액트는 기본적으로 SPA(Single Page Application)이기 때문에 기존의 웹 프로그램과 동작 방식이 다르다. 기존의 웹 애플리케이션이 주로 페이지 단위로 개발되는 것에 비해 리액트는 여러 개의 컴포넌트(Component)를 사용해서 개발하는데, 하나의 페이지(Single Page)에서 보여주는 방식으로 작성되기 때문에 기존과는 전혀 다른 접근이 필요하다. 이번 장에서는 기존의 웹페이지와 유사하게 브라우저의 주소창에 따라 다른 화면에 보이게 처리하기 위해서는 React-Router를 활용하는 방법에 관해서 알아보자.
* 이번 장의 개발 목표는 다음과 같다.

1. React-Router를 적용해서 페이지의 이동이 가능하도록 컴포넌트들을 구성

2. TailWind CSS를 이용해서 공통의 레이아웃을 구성하고 이를 통해서 페이지 기반의 애플리케이션을 구성

1. 개발 목표의 이해

* 이번 장에서는 React-Router를 활용해서 마치 기존의 웹페이지처럼 브라우저 주소창에 따라서 다양한 컴포넌트를 보여주도록 구성한다. 예제는 애플리케이션을 구성하기 위해서 목록 페이지, 등록 페이지, 조회 페이지, 수정/삭제 페이지를 구성한다.
* 모든 페이지는 동일한 레이아웃과 동일한 상단 메뉴를 사용하는 구조로 하나의 레이아웃을 구성하고 예제마다 필요한 부분만을 구현하는 형태로 진행한다(구성되는 레이아웃은 나중에 로그인 처리를 통해서 특정 메뉴들을 노출할 수 있도록 구성될 것이다).
* 리액트 프로젝트에서 React-Router를 이용하는 이유는 SPA를 마치 기존의 웹 서비스와 동일하게 이용하거나 주소에 대한 공유 작업을 쉽게 처리할 수 있기 때문이다. 브라우저의 주소창은 SNS 등을 통해서 다른 사람들이 쉽게 접근할 수 있는 방법이 된다.


(1) React-Router 추가

* 현재 예제 프로젝트는 mall 이라는 폴더를 기준으로 TailWind CSS가 적용되어 있는 상태이다.

* 터미널 환경을 이용해서 React-Router 모듈을 추가한다.

$ npm install react-router-dom

* React-Router가 정상적으로 추가되었다면 프로젝트 내에서 package.json 파일의 일부가 다음과 같이 변경된 것을 확인한다.


(2) React-Router 설정

* React-Router 설정은 다양하지만 예제에서 사용하는 방식은 '브라우저 주소창에 맞는 컴포넌트를 보여준다'고 할 수 있다. 프로젝트 내에서 router 폴더를 생성하고 root.js 파일에서 기본 라우팅 설정을 추가한다.

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

const root = createBrowserRouter([]);

export default root;

* root.js는 createBrowserRouter() 를 통해서 어떤 경로(path)에는 어떤 컴포넌트를 보여줄 것인지를 결정하는 역할을 한다. 경로의 추가는 파라미터로 전달되는 배열의 내용물로 결정된다. 실행되는 리액트 애플리케이션을 root.js를 이용해서 경로에 맞는 컴포넌트를 보여 주어야 한다. 이를 처리하기 위해서 프로젝트 실행 시 가장 먼저 실행되는 App.js 파일을 수정해 준다.

import React from "react";
import { RouterProvider } from "react-router-dom";
import root from "./router/root";

const App = () => {
  return <RouterProvider router={root} />;
};

export default App;

* 프로젝트를 실행하면 root.js에 있는 설정을 활용해서 주소창의 경로를 기준으로 컴포넌트들을 보여주게 된다. 아직은 아무런 설정이 존재하지 않기 때문에 빈 화면만을 보게 된다.



[1] 페이지용 컴포넌트 추가와 설정
* 위와 같이 빈 화면이 정상적으로 출력되는 것을 확인한다면 실제 페이지에 해당하는 컴포넌트를 추가하고 이를 설정해서 주소창에 따라서 다른 컴포넌트가 보이는지 확인한다.
* 프로젝트의 src 폴더에 pages라는 이름의 폴더를 생성하고 MainPage.js 파일을 추가한다.

import React from "react";

const MainPage = () => {
  return (
    <div className="text-3xl">
      <div>Main Page</div>
    </div>
  );
};

export default MainPage;

* MainPage는 React-Router의 동작을 확인하기 위한 것이므로 아직은 단순한 문자열만 출력되도록 한다. 리액트 프로젝트가 실행될 때 첫 화면은 MainPage 컴포넌트가 보일 수 있도록 root.js 파일에 설정을 추가한다.

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

const Loading = <div>Loading...</div>;
const Main = lazy(() => import("../pages/MainPage"));

const root = createBrowserRouter([
  {
    path: "",
    element: (
      <Suspense fallback={Loading}>
        <Main />
      </Suspense>
    ),
  },
]);

export default root;

* root.js 파일은 여러 곳이 수정되었는데 내용을 정리하면 다음과 같다

- 경로가 '/' 혹은 아무 것도 없을 때는 MainPage 컴포넌트를 보여준다.

- <Suspense> 와 lazy() 는 필요한 순간까지 컴포넌트를 메모리상으로 올리지 않도록 지연로딩을 위해서 사용된다.

- 아직 컴포넌트의 처리가 끝나지 않았다면 화면에 간단히 'Loading...' 메시지를 보여주도록 한다.

* root.js 설정이 반영되면 http:localhost:3000 주소가 호출되었을 때 MainPage 컴포넌트가 출력되는 것을 볼 수 있다.

* root.js 설정에서 lazy()를 이용하는 지연로딩이 어떻게 동작하는지를 확인해 보기 위해 또 다른 페이지용 컴포넌트로 AboutPage.js를 추가한다.

import React from "react";

const AboutPage = () => {
  return <div className="text-3xl">About Page</div>;
};

export default AboutPage;

* AboutPage 컴포넌트는 브라우저의 경로가 '/about'인 경우에 동작하도록 root.js 파일에 설정을 추가한다.

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

const Loading = <div>Loading...</div>;
const Main = lazy(() => import("../pages/MainPage"));
const About = lazy(() => import("../pages/AboutPage"));

const root = createBrowserRouter([
  {
    path: "",
    element: (
      <Suspense fallback={Loading}>
        <Main />
      </Suspense>
    ),
  },
  {
    path: "about",
    element: (
      <Suspense fallback={Loading}>
        <About />
      </Suspense>
    ),
  },
]);

export default root;

* 변경된 설정이 반영된 후에 '/about' 경로로 접근하면 AboutPage 컴포넌트가 보이게 된다.


(3) <Link>를 통한 이동

* 앞의 화면과 같이 브라우저 주소창에서 원하는 컴포넌트가 출력되도록 구성할 수 있지만, 리액트가 SPA라는 사실을 항상 기억해야 한다. 브라우저 주소창을 변경한다는 것은 모든 것을 지우고 새로 화면을 재구성한다는 의미가 된다. 따라서 리액트 애플리케이션은 단순히 눈에 보이는 컴포넌트가 변경되는 것이 아니라 완전히 처음부터 새로 애플리케이션이 로딩되고 처리된다는 뜻이 된다. 이 때문에 리액트와 같은 SPA에서는 새로운 창을 띄우거나 브라우저의 '새로고침'과 같이 새로운 경로를 실행하는 것을 매우 조심해야 한다. 이는 자바스크립트가 브라우저에서 새로고침될 시 상태값이 초기화되기 때문이다.
* 기존의 HTML에서 사용했던 <a> 태그는 브라우저 주소창을 변경하면서 애플리케이션 자체의 로딩부터 새로 시작되기 때문에 React-Router에서는 사용하지 않도록 주의해야 한다. React-Router를 활용하는 경우에 다른 경로에 대한 링크는 <Link> 를 이용한다. MainPage 컴포넌트에 '/about' 경로로 이동할 수 있는 링크를 추가한다.

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

const MainPage = () => {
  return (
    <div>
      <div className="flex">
        <Link to={"/about"}>About</Link>
      </div>
      <div className="text-3xl">Main Page</div>
    </div>
  );
};

export default MainPage;

* MainPage에 추가된 <Link>는 리액트 내부에서 해당 컴포넌트만을 처리한다. 현재 예제에서는 <Suspense>와 lazy()를 이용해서 해당 컴포넌트가 필요한 순간까지는 로딩하지 않도록 되어 있으므로 브라우저에서 '/' 혹은 빈 경로로 접근할 경우 MainPage 컴포넌트만을 로딩해서 보여준다.

* 화면에서 <Link>로 처리된 'About'을 클릭하면 AboutPage 컴포넌트만 추가적으로 로딩되는 것을 확인할 수 있다.

* 반면에 직접 브라우저 주소창에서 '/about' 경로를 호출할 때는 화면상의 결과는 동일하지만, 실행 과정에서 리액트가 아예 처음부터 다시 실행된다. 현재 예제의 경우 애플리케이션의 크기가 작아서 크게 문제가 되지 않겠지만 규모가 커지면 리액트가 시작되는 것만으로도 많은 시간이 소모된다.

* SPA 방식의 리액트 애플리케이션은 처음에 필요한 모든 컴포넌트를 로딩하기 때문에 초기 실행 시간이 오래 걸리는 단점이 있다. 이를 해결하기 위해서 <Suspense>와 <Lazy>를 이용해서 분할 로딩을 하는데 이를 '코드 분할(Code Spliting)'이라고 한다.



[1] 페이지 컴포넌트 레이아웃
* React-Router를 이용하면 마치 웹페이지 간의 이동처럼 컴포넌트들을 처리할 수 있기 때문에 각 페이지 역할을 하는 컴포넌트는 다른 페이지 컴포넌트를 볼 수 있게 <Link>를 제공해서 사용자들이 브라우저의 새로고침을 가능하면 피할 수 있게 구성하는 것이 좋다.
* 프로젝트에서는 공통의 레이아웃 템플릿을 구성하고 메뉴 구조를 만들어서 자주 사용하는 링크들에 대한 처리를 재사용할 수 있게 구성한다.


(4) 레이아웃 컴포넌트와 children

* 프로젝트 내에 layouts 폴더를 생성하고 BasicLayout.js라는 이름으로 컴포넌트를 생성한다.

* BasicLayout 컴포넌트는 화면 상단에 공통적인 메뉴와 링크를 보여주고 아래쪽으로 각 페이지 컴포넌트를 출력하는 구조로 작성한다. 리액트의 컴포넌트는 'children' 속성을 활용해서 컴포넌트 내부에서 다른 컴포넌트를 적용할 수 있다.
* 우선 간단한 확인을 위한 BasicLayout.js는 다음과 같이 <nav>와 <div>로 구분해서 작성한다(React-Router의 경우에는 <Outlet>이라는 컴포넌트를 이용하는 방식도 가능하다. 이에 대한 예제는 조금 뒤에서 살펴보자).

import React from "react";

const BasicLayout = ({ children }) => {
  return (
    <>
      <header className="bg-teal-400 p-5">
        <h1 className="text-2xl md:text-4xl">Header</h1>
      </header>
      <div className="bg-white my-5 w-full flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
        <main className="bg-sky-300 md:w-2/3 lg:w-3/4 px-5 py-40">
          {children}
        </main>
        <aside className="bg-green-300 md:w-1/3 lg:w-1/4 px-5 py-40">
          <h2 className="text-2xl md:text-4xl">Sidebar</h2>
        </aside>
      </div>
    </>
  );
};

export default BasicLayout;

* BasicLayout의 선언부에는 '{children}'으로 자신이 출력해야 하는 다른 내용물을 전달받고, 화면의 내용을 담당하는 <div> 내에 이를 이용한다. MainPage 컴포넌트의 내용물은 <BasicLayout>을 이용해서 수정한다.

import React from "react";
import BasicLayout from "../layouts/BasicLayout";

const MainPage = () => {
  return (
    <BasicLayout>
      <div className="text-3xl">Main Page</div>
    </BasicLayout>
  );
};

export default MainPage;

* BasicLayout이 반영된 후에는 MainPage의 내용이 아래와 같이 적용된 것을 확인할 수 있다.

* 동일한 방식으로 AboutPage 역시 <BasicLayout>을 적용하도록 코드를 수정했다.

import React from "react";
import BasicLayout from "../layouts/BasicLayout";

const AboutPage = () => {
  return (
    <BasicLayout>
      <div className="text-3xl">About Page</div>
    </BasicLayout>
  );
};

export default AboutPage;

(5) 상단 메뉴 컴포넌트 구성

* 레이아웃 화면 상단에 Header 부분에 들어가야 할 부분은 별도의 컴포넌트를 구성해서 활용한다. components, menu 폴더를 생성하고 menu 폴더 안에 BasicMenu.js 파일을 추가한다.

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

const BasicMenu = () => {
  return (
    <nav id="navbar" className="flex bg-blue-300">
      <div className="w-4/5 bg-gray-500">
        <ul className="flex p-4 text-white font-bold">
          <li className="pr-6 text-2xl">
            <Link to={"/"}>Main</Link>
          </li>
          <li className="pr-6 text-2xl">
            <Link to={"/about"}>About</Link>
          </li>
        </ul>
      </div>
      <div className="w-1/5 flex justify-end bg-orange-300 p-4 font-medium">
        <div className="text-white text-sm m-1 rounded">Login</div>
      </div>
    </nav>
  );
};

export default BasicMenu;

* 메뉴가 나오도록 구성한다.

import React from "react";
import BasicMenu from "../components/menus/BasicMenu";

const BasicLayout = ({ children }) => {
  return (
    <>
      <BasicMenu></BasicMenu>
      <div className="bg-white my-5 w-full flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
        <main className="bg-sky-300 md:w-2/3 lg:w-3/4 px-5 py-40">
          {children}
        </main>
        <aside className="bg-green-300 md:w-1/3 lg:w-1/4 px-5 py-40">
          <h2 className="text-2xl md:text-4xl">Sidebar</h2>
        </aside>
      </div>
    </>
  );
};

export default BasicLayout;

* MainPage와 AboutPage에 BasicLayout이 적용되어 있다면 브라우저에는 메뉴가 적용된 모습을 확인할 수 있고, 메뉴를 이동한 페이지의 이동이나 뒤로 가기 등 브라우저에서 주소창을 통한 동작 모두 가능해진다.



[1] 새로운 단위 기능과 라우팅
* 애플리케이션의 컴포넌트가 많아질수록 React-Router의 설정은 복잡해지고 메뉴 구조 역시 복잡해지게 된다. 웹에서 관련 있는 기능들을 묶어서 '모듈'이라고 부르기도 한다. 예컨대 게시판 모듈, 회원 모듈, 상품 모듈과 같이 기능들의 목적을 모듈의 단위로 삼는 경우가 많다.
* 각 모듈은 내부적으로 자신만의 메뉴를 가지는 경우가 많은데 이를 위해서 React-Router의 <Outlet> 을 활용하면 편리하다. 예제에서는 앞으로 개발할 Todo 기능 개발을 염두에 두고 다음과 같은 경로들을 작성할 것이다.

경로설명쿼리
/todo/list목록 페이지page, size
/todo/add등록 페이지 
/todo/read/번호조회 페이지번호, page, size
/todo/modify/번호수정/삭제 페이지번호, page, size

* 이처럼 '/todo/'로 시작하는 경로를 처리하기 위해서 React-Router는 하위 경로를 children으로 지정할 수 있는 기능을 제공한다. 새로운 Todo 기능을 위해서 BasicMenu 컴포넌트에는 '/todo/'로 이동할 수 있는 링크를 추가한다(경로의 마지막을 '/'로 처리하는 부분을 주의).

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

const BasicMenu = () => {
  return (
    <nav id="navbar" className="flex bg-blue-300">
      <div className="w-4/5 bg-gray-500">
        <ul className="flex p-4 text-white font-bold">
          <li className="pr-6 text-2xl">
            <Link to={"/"}>Main</Link>
          </li>
          <li className="pr-6 text-2xl">
            <Link to={"/about"}>About</Link>
          </li>
          <li className="pr-6 text-2xl">
            <Link to={"/todo/"}>Todo</Link>
          </li>
        </ul>
      </div>
      <div className="w-1/5 flex justify-end bg-orange-300 p-4 font-medium">
        <div className="text-white text-sm m-1 rounded">Login</div>
      </div>
    </nav>
  );
};

export default BasicMenu;

* 브라우저에서 새로운 Todo 메뉴가 추가된 것을 확인할 수 있고, 클릭 시에는 '/todo/' 경로에 맞는 컴포넌트가 존재하지 않기 때문에 에러가 발생하는 것을 볼 수 있다.


 

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

(6) 스프링 부트와 API 서버 1  (0) 2024.02.26
(5) React-Router 3  (0) 2024.02.26
(4) React-Router 2  (0) 2024.02.26
(2) 개발 환경설정  (0) 2024.02.26
(1) 들어가며  (1) 2024.02.26