Spring WebFlux를 이용한 chat 프로그램 - Dynamic Route 설정

2025. 5. 3. 10:56JAVA/Spring Boot

기본 Vite에서 라우팅 하는 방법은 다음과 같다.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Signup from './pages/Signup.jsx';
import Login from './pages/Login';

import "./App.css"

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/signup" element={<Signup />} />
      </Routes>
    </Router>
  );
}

모든 페이지를 Import 하고 일일이 Route를 입력해 주는 방식이다.

근데 처음 Vite를 접한 나에게는 이게 너무 비효율 적으로 보인다.

그래서 ChatGPT에 문의를 했서 다음과 같이 처리 했다.

아~!!! 그전에 먼저 프론트엔드 구조는 다음과 같다.

위의 구조를 가지고 시작한다.


DB Table 생성 

CREATE TABLE routes (
  id SERIAL PRIMARY KEY,
  path VARCHAR(255) NOT NULL,
  component_name VARCHAR(255) NOT NULL,
  UNIQUE (path, component_name)
);

 

Data Insert

INSERT INTO routes (path, component_name) VALUES ('/login', 'auth/Login');
INSERT INTO routes (path, component_name) VALUES ('/signup', 'auth/Signup');

 

백엔드 작업

 

Route.java

package com.company.common.database.entity;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

import lombok.Data;

/**
 * routes 테이블과 매핑되는 라우터 엔티티
 */
@Table("routes")
@Data
public class Route {
	
    @Id
    private Long id; // 내부적으로 PK 역할 (Auto Increment)

    private String path;

    private String componentName;
}

 

RouteService.java

package com.company.route.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.company.common.database.entity.Route;
import com.company.route.service.RouteService;

import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/v1/routes")
@RequiredArgsConstructor
public class RouteController {

	private final RouteService routeService;

    /**
     * 모든 라우트 목록 조회 API
     */
    @GetMapping
    public Flux<Route> getAllRoutes() {
        return routeService.findAllRoutes();
    }
}

 

RouteRepository.java

package com.company.route.repository;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;

import com.company.common.database.entity.Route;

/**
 * 화면(Route)용 PostgreSQL R2DBC Repository
 */
public interface RouteRepository extends ReactiveCrudRepository<Route, Long> {
}

 

RouteController.java

package com.company.route.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.company.common.database.entity.Route;
import com.company.route.service.RouteService;

import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/v1/routes")
@RequiredArgsConstructor
public class RouteController {

	private final RouteService routeService;

    /**
     * 모든 라우트 목록 조회 API
     */
    @GetMapping
    public Flux<Route> getAllRoutes() {
        return routeService.findAllRoutes();
    }
}

DB에서 Route 정보를 모두 조회해서 반환해 주는게 끝이다.


 

프론트엔드 작업

 

App.jsx

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { routeService } from './api/service/route/RouteService';

import "./App.css"

/**
 * 1. Vite의 import.meta.glob를 사용 사용하여 page 폴더 하위의 모든 .jsx 파일을 스캔한다.
 */
const pages = import.meta.glob("./pages/**/*.jsx");

/**
 * 컴포넌트 이름에 맞는 컴포넌트를 동적으로 로딩
 * @param {string} componentName - ex) "ChatRoom", "Signup"
 */
async function loadComponent(componentName) {
  // 경로 기반 정확한 파일 매칭
  const targetPath = `./pages/${componentName}.jsx`;
  // 파일명에 컴포넌트 이름이 포함된 파일을 찾는다
  if (pages[targetPath]) {
    const module = await pages[targetPath](); // 동적 import
    return module.default;
  }

  console.error(`컴포넌트를 찾을 수 없습니다. [${targetPath}]`)
  throw new Error(`컴포넌트를 찾을 수 없습니다: ${componentName}`);
}

function App() {

  const [routeList, setRouteList] = useState([]);
  const [components, setComponents] = useState({});

  /*
   * 2. DB에서 Route 정보를 조회해서 Component와 RouteList를 생성한다.
   *    DB에서 조회한 Route의 componentName과 일치하는 항목이 있는지 조회해서 loadComponent 함수를 통해 Components에 추가한다.
   *    또한. Route 목록도 저장한다.
   */
  useEffect( () => {

    const fetchRoutes = async () => {
      try {
        // Database에서 route 목록 조회
        const response = await routeService.select();
        const routes = response.data;
        const comps = {};
  
        for (const route of routes) {
          try {
            //componentName과 일치하는 Component 항목을 찾는다.
            const comp = await loadComponent(route.componentName);
            comps[route.componentName] = comp;
          } catch (err) {
            console.error(err);
          }
        }
  
        setComponents(comps); // Components 생성
        setRouteList(routes); // Route 목록 생성
  
      } catch (error) {
        console.error("라우트 불러오기 실패:", error);
      }
    };
  
    fetchRoutes();

  }, []);

  return (
    <Router>
      <Routes>
        {/* 3. Route 목록을 Loof 돌리면서 route의 componentName에 해당하는 component를 조회한다. */}
        {routeList.length === 0 ? (
          <Route path="*" element={<div>라우트 불러오는 중...</div>} />
        ) : (
          routeList.map((route, index) => {
            const Component = components[route.componentName];
            if (!Component) return null;

            return (
              <Route
                key={index}
                path={route.path}
                element={<Component />}
              />
            );
          })
        )}
      </Routes>
    </Router>
  );
}

export default App;

설명은 주석에 최대한 달았으니 참고 하면 된다.

 

RouteService.jsx

import axiosInstance from "../../instance/axiosInterceptor";

/**
 * Route 처리 Service
 */
class RouteService {

    /**
     * 모든 라우트 목록 조회
     * @returns 
     */
    async select() {
        return await axiosInstance.get('/routes')
    }
}

export const routeService = new RouteService();

 

위와 같이 Route 정보를 DB에서 동적으로 관리하도록 처리 완료했다.

추후 권한이 필요한 경우 여기서 확장하여 처리하면 될 듯하다.