본문 바로가기
개발

[프로젝트로 리액트 공부해보자01] 라우트 분리, 레이아웃 분리!

by mlheein 2025. 4. 30.

나는 프로젝트 경험이 적다. 항상 시간에 쫓겨 효율을 따지지 않으면 처리하기에 급급해왔다. 프론트엔드 개발자인데 기초 기둥 세우는게 너무 부족하다는 것을 느끼고 학습에 열망이 생겨버렸다. 사실 생긴지는 참 오래됐는데, 이제야 시간을 내기 시작했다. 어느새 퇴근 후 저녁 먹고 노트북 앞에 앉아 ai에게 끝없는 질문을 하며 공부를 하기 시작했다.

 

사수가 없어서 ai에게 많이 물어보는 편인데, 알지 않는가! ai가 항상 100점은 아니라는 것을 ㅜ.ㅜ 항상 되묻는다. "이 방법이 저 방법보다 나아? 그럼 뭐가 나아서 이걸 추천해준거야? 이게 맞다고 하는거야?" 물어보면 80퍼센트가 "너 말이 맞아! 그것도 좋은걸"이다. 이래서는 신뢰가 많이 떨어지지만.. 그래도 도움이 안되는건 아니니, ai의 도움을 받아 만들어보고 내가 사용하고 프로젝트를 진행하면서 놓친 부분, 혹은 '아 이건 이런 방법을 하는게 훨씬 효율적이고 재사용하기에도 좋았겠구나.'를 깨닫게 되지 않겠는가! 홧팅 홧팅~

 

우선 라우트와 레이아웃을 공부하고 싶었다.  프로젝트를 설계할 때, 이런 구조를 위해서는 어떻게 만드는게 좋은 방법일까를 깨닫고 싶다. 한참 걸리겠지만 알아가는게 재밌다.

 

조건 1. PublicRoutes : 로그인 안해도 사용할 수 있는 페이지 관리 route

조건 2. PrivateRoutes : 로그인 후 세션을 가지고 있어야만 접근 가능한 페이지 관리 route

조건 3. 권한에 따라서 페이지 접근 허용 제어가 가능해야한다.

 

생각보다 이것저것 검색해보면 간단한 route를 사용하는 것들만 많이 보인다. App.js에서 <Router>를 바로 사용하는것. 추후 유지보수를 위해 routes페이지를 분리하고, 객체로, 동적으로 관리하고싶다. 

 

routes/index.jsx

import { createBrowserRouter } from 'react-router-dom';
import PublicRoutes from './PublicRoutes';
import ProtectedRoutes from './ProtectedRoutes';

// ==============================|| ROUTING RENDER ||============================== //

const router = createBrowserRouter([
  PublicRoutes,
  ProtectedRoutes
], { 
  basename: '/'  // 환경 변수 대신 직접 값 사용
});

export default router;

 

layout/Header.jsx

import { AppBar, Toolbar, IconButton, Typography, Box, Avatar } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import NotificationsIcon from '@mui/icons-material/Notifications';

const Header = ({ onMenuClick }) => {
  // const { user } = useAuth();
  const user = {
    name: 'John Doe',
    avatar: 'https://via.placeholder.com/150'
  };

  return (
    <AppBar 
      position="fixed" 
      sx={{ 
        zIndex: (theme) => theme.zIndex.drawer + 1,
        bgcolor: 'background.paper',
        color: 'text.primary',
        boxShadow: 'none',
        borderBottom: '1px solid',
        borderColor: 'divider'
      }}
    >
      <Toolbar>
        {/* <IconButton
          color="inherit"
          aria-label="open drawer"
          edge="start"
          onClick={onMenuClick}
          sx={{ mr: 2 }}
        >
          <MenuIcon />
        </IconButton> */}
        <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
          Emotion Diary
        </Typography>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
          <IconButton color="inherit">
            <NotificationsIcon />
          </IconButton>
          <Avatar 
            alt={user?.name || 'User'} 
            src={user?.avatar}
            sx={{ width: 32, height: 32 }}
          />
        </Box>
      </Toolbar>
    </AppBar>
  );
};

export default Header;

 

layout/MainLayout.jsx

import { useState } from 'react';
import { Box, CssBaseline, Drawer, useTheme } from '@mui/material';
import { Outlet } from 'react-router-dom';
import Header from './Header';
import Sidebar from './Sidebar';
import Footer from './Footer';

const drawerWidth = 240;

const MainLayout = () => {
  const [menuOpen, setMenuOpen] = useState(false);
  const theme = useTheme();

  const handleDrawerToggle = () => {
    setMenuOpen(!menuOpen);
  };

  return (
    <Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'background.default' }}>
      <CssBaseline />
      <Header onMenuClick={handleDrawerToggle} />
      
      <Box
        component="nav"
        sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
      >
        <Drawer
          variant="permanent"
          open
          sx={{
            display: { xs: 'none', sm: 'block' },
            '& .MuiDrawer-paper': { 
              boxSizing: 'border-box', 
              width: drawerWidth,
              borderRight: '1px solid',
              borderColor: 'divider'
            },
          }}
        >
          <Sidebar />
        </Drawer>

        {/* <Drawer
          variant="temporary"
          open={menuOpen}
          onClose={handleDrawerToggle}
          ModalProps={{
            keepMounted: false,
            hideBackdrop: false,
            disablePortal: true
          }}
          sx={{
            display: { xs: 'none', sm: 'block' },
            '& .MuiDrawer-paper': {
              boxSizing: 'border-box',
              width: drawerWidth,
              borderRight: '1px solid',
              borderColor: 'divider',
              mt: '64px' // Header height
            },
          }}
        >
          <Sidebar />
        </Drawer> */}
      </Box>

      <Box
        component="main"
        sx={{
          flexGrow: 1,
          pt: 3,
          width: { sm: `calc(100% - ${drawerWidth}px)` },
          mt: '64px',
          display: 'flex',
          flexDirection: 'column',
          minHeight: 'calc(100vh - 64px)'
        }}
      >
        <Box sx={{ flex: 1 }}>
          <Outlet />
        </Box>
        <Footer />
      </Box>
    </Box>
  );
};

export default MainLayout;

 

routes/ProtectedRoutes.jsx

import AuthGuard from '../components/auth/AuthGuard';
import DashboardPage from '../pages/dashboard/index';
import MainLayout from '../layout/MainLayout';
const ProtectedRoutes = {
  path: '/',
  element: <AuthGuard><MainLayout /></AuthGuard>,
  children: [
    {
      index: true, // 이 라우트는 '/' 경로일 때 렌더링됨
      element: <DashboardPage />
    },
    {
      path: 'dashboard', // 제거할 예정..
      element: <DashboardPage />
    }
  ]
};

export default ProtectedRoutes;



// 혹은

import AuthGuard from '../components/auth/AuthGuard';
import DashboardPage from '../pages/dashboard/index';
import MainLayout from '../layout/MainLayout';
const ProtectedRoutes = {
  path: '/',
  element: <AuthGuard><MainLayout /></AuthGuard>,
  children: [
    {
      path: '',  // 빈 문자열은 루트 경로를 의미
      element: <DashboardPage />
    },
    {
    ...
    }
  ]
};

export default ProtectedRoutes;

고민을 참 많이 했다. ProtectedRoutes의 기본 element안에 DashboardPage를 넣고 싶었는데 아직 이 부분은 좀 헷갈린다. 저렇게 구조를 맞춘 이유는, 로그인 안해도 되는 페이지 제외하고 권한 체크를 해야 하니 기본 element를 감싸주는게 맞는거 같았고, public 경로를 제외하고 그 외 모든 페이지는 MainLayout으로 감싸줘야하니, 저렇게 마무리했다. 더 좋은 방법을 알고 싶다!!



components/auth/AuthGuard.jsx

- 인증 담당

import { Navigate, useLocation } from 'react-router-dom';

const AuthGuard = ({ children, permission }) => {
  const location = useLocation();

//   if (!isAuthenticated) {
//     // 로그인하지 않은 경우 로그인 페이지로 리다이렉트
//     return <Navigate to="/login" state={{ from: location }} replace />;
//   }

//   if (permission && !user.permissions?.includes(permission)) {
//     // 권한이 없는 경우 접근 거부 페이지로 리다이렉트
//     return <Navigate to="/unauthorized" replace />;
//   }

  return children;
};

export default AuthGuard;

여기서 정말 한참 고민했다.. 여기에다 return <MainLayout>{children}</MainLayout>으로 감싸야하는건지 return children으로 끝내야하는지.. 하지만 이 파일에서는 인증만 담당하게 할것으로 판단하여 children으로 마무리했다.

 

pages/dashboard/index.jsx

import { Box, Typography } from "@mui/material";

const DashboardPage = () => {
  console.log('DashboardPage');
  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center'}}>
      <Typography variant="h1">Dashboard</Typography>
      <Typography variant="h2">Welcome to the dashboard</Typography>
    </Box>
);
};

export default DashboardPage;

 

 

이하 ai의 반응


 

현재 구조는 다음과 같이 작동합니다:

  1. AuthGuard가 인증을 체크
  2. MainLayout이 레이아웃을 제공
  3. MainLayout의 Outlet에 children 라우트들이 렌더링

이 구조는 잘 설계되어 있습니다. 각 컴포넌트의 책임이 명확하게 분리되어 있습니다:

  • AuthGuard: 인증만 담당
  • MainLayout: 레이아웃과 Outlet 제공
  • 라우트: 페이지 컴포넌트 렌더링

중첩 구조도 적절합니다:

AuthGuard (인증)
  └─ MainLayout (레이아웃)
      └─ Outlet (children 라우트들)
          └─ DashboardPage (또는 다른 페이지)

 

이 구조의 장점:

  1. 관심사 분리가 잘 되어있음
  2. 각 컴포넌트의 책임이 명확함
  3. 라우팅과 레이아웃이 자연스럽게 결합됨
  4. 새로운 페이지 추가가 쉬움

따라서 현재 구조는 잘 설계되어 있다고 볼 수 있습니다. 수정이 필요한 부분은 없어 보입니다.


후에 하고 싶은 것들

:

1. 모바일 / 웹 폴더 나눠서 프로젝트 진행

2. 권한 제대로 추가해서 페이지 분리하기

3. 권한 없이 경로 검색으로 페이지 접근 시, 강제 이동 기능 추가는 어떤 파일에 어떤 방식으로 넣어줘야 하나?

4. 쓸데없는 deps로 레이아웃이나 라우트를 복잡하게 만들고 싶지 않은데 어떻게 해야할까!