본문으로 바로가기

2. React-Router

category 개발/연습프로젝트기록 2024. 3. 23. 18:56

목표

  • React-Router를 활용해서 브라우저 주소창에 따라서 다양한 컴포넌트를 보여주도록 구상
    • 목록페이지, 등록페이지, 조회 페이지, 수정/삭제 페이지
    • 모든 페이지는 동일한 레이아웃과 동일한 상단 메뉴를 사용하는 구조로 하나의 레아이웃을 구성하고 예제마다 필요한 부분만을 구현하는 형태 구현

 

리액트 프로젝트에서 React-Router 를 이용하는 이유는 주소에 대한 공유 작업을 처리할 수 있기 때문이다.

 

 

2.1 React-Router 추가

 

npm install react-router-dom 

 

package.json 에 추가된 모습

 

 

2.2 React-Router 설정

 

src 폴더 아래 router 폴더를 생성한 후 root.js 파일을 만들어준뒤 기본 라우팅 설정을 해준다.

 

 

root.js

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

const root = createBrowserRouter([

])

export default root;

 

App.js

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

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

export default App;

 

 

실행화면

 

현재는 아무런 설정을 하지 않았기 때문에 빈화면 나오는게 정상임

 

 

페이지용 컴포넌트 추가와 설정

프로젝트의 src 폴더에 pages 라는 이름의 폴더 생성하고 MainPage.js 파일을 추가해준다.

 

MainPage.js

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

export default MainPage;

 

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 root = createBrowserRouter([
    {
        path: "",
        element: <Suspense fallback={Loading}><Main/></Suspense>
    }
])

export default root;
  • 경로가 '/' 거나 아무것도 없을 때는 MainPage 컴포넌트를 보여준다
  • <Suspense>와 lazy()는 필요한 순간까지 컴포넌트를 메모리상으로 올리지 않도록 지연로딩을 위해서 사용한다.
  • 아직 컴포넌트의 처리가 끝나지 않았다면 화면에 'Loading...' 메시지를 출력한다.

 

실행 결과

'/about' 경로를 추가해 줄것이다.

 

 

AboutPage.js

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

export default AboutPage;

 

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;

 

실행결과

 

 

 

2.3 <Link> 를 통한 이동

  • React 는 single page application이라 브라우저 주소창을 변경한다는 것은 모든 것을 지우고 새로 시작한다는 의미이다.
  • 따라서 리액트 애플리케이션은 단순히 보이는 컴포넌트가 변경되는 것이 아니라 완전히 처음부터 애플리케이션이 로딩되고 처리된다는 뜻!!
  • 리액트와 같은 SPA 에서는 새로운 창을 띄우거나 브라우저의 '새로고침' 과 같이 새로운 경로를 실행하는 것을 매우 조심해야 한다.
  • 기존의 HTML 에서 사용했던 <a> 태그는 브라우저 주소창을 변경하면서  애플리케이션 자체의 로딩부터 새로 시작되기 때문에 React-Router에서는 사용하지 않도록 주의해야한다.
  • <Link>를 이용해야한다.

MainPage.js

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;
  • <Link> 는 리액트 내부에서 해당 컴포넌트만을 처리한다.
  • 현재 <Suspense> 와 lazy() 를 이용해서 해당 컴포넌트가 필요한 순간까지는 로딩하지 않도록 되어 있으므로 브라우저에서 '/' 혹은 빈 경로로 접근 할 경우 MainPage 컴포넌트만을 로딩해서 보여준다. 
  • 화면에서 <Link> 로 처리된 About을 클릭하면 AboutPage 컴포넌트만 추가적으로 로딩되는 것을 확인 가능하다.

 

SPA 방식의 리액트 애플리케이션은 처음에 필요한 모든 컴포넌트를 로딩하기 때문에 초기 실행 시간이 오래걸리는 단점이 있다.

이를 해결하기 위해서 <Suspense>와 <Lazy>를 이용해서 분할 로딩을 하는데 이를 '코드 분할' 이라고 한다.

 

 

2.4 레이아웃 컴포넌트와 children

 

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

BasicLayout 컴포넌트는 화면 상단에 공통적인 메뉴와 링크를 보여주고 아래쪽으로 각 페이지 컴포넌트를 출력하는 구조로 구현할것이다.

리액트의 컴포넌트는 'children' 속성을 활용해서 컴포넌트 내부에 다른 컴포넌트를 적용할 수 있다.

 

 

BasicLayout.js

import React from 'react';

const BasicLayyout = ({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 sapce-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-14 px-5 py-40'>
                    <h1 className='text-2xl md:text-4xl'>
                        Sidebar
                    </h1>
                </aside>
            </div>
        </>
    );
};

export default BasicLayyout;

 

 

MainPage.js

import { Link } from "react-router-dom";
import BasicLayyout from "../layouts/BasicLayyout";

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

export default MainPage;

 

AboutPage.js

import BasicLayyout from "../layouts/BasicLayyout";

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

export default AboutPage;

 

 

 

실행결과

 

이제 Header 부분도 별도의 컴포넌트로 구성할 것이다.

src 아래로 components->menus 폴더를 생성하고 BasicMenu.js 를 생성했다.

 

 

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;

 

 

BasicLayout.js

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

const BasicLayyout = ({children}) => {
    return (
        <>
            <BasicMenu></BasicMenu>

            <div className='bg-white my-5 w-full flex flex-col sapce-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'>
                    <h1 className='text-2xl md:text-4xl'>
                        Sidebar
                    </h1>
                </aside>
            </div>
        </>
    );
};

export default BasicLayyout;

 

 

실행 결과

 

 

 

 

2.5 <Outlet> 기능

  • 중첩적인 라우팅 설정 시 레이아웃을 유지할 수 있다.
  • ex. /todo/list 와 같이 하위 경로에 맞는 페이지 컴포넌트를 제작할 때 IndexPage의 구조가 쥬지될 수 있게 됨
  • 좀 더 세밀한 레이아웃의 재사용 단위를 구성할 수 있음

 

앞으로 구현할 기능 정리

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

 

 

BasicMenu 컴포넌트에 /todo/로 이동할 수 있는 링크를 추가해준다.

 

 

IndexPage.js

import React from 'react';
import BasicLayyout from '../../layouts/BasicLayout';
import { Outlet } from 'react-router-dom';

const IndexPage = () => {
    return (
        <BasicLayyout>
            <div className='w-full flex m-2 p-2'>
                <div className='text-xl m-1 p-2 w-20 font-extrabold text-center underline'>
                    LIST
                </div>
                <div className='text-xl m-1 p-2 w-20 font-extrabold text-center underline'>
                    ADD
                </div>
            </div>
            <div className='flex flex-wrap w-full'>
                <Outlet/>
            </div>
        </BasicLayyout>
    );
};

export default IndexPage;

 

ListPage.js

import React from 'react';

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

export default ListPage;

 

root.js

 {
        path: "todo",
        element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
        children:[
            {
                path: "list",
                element: <Suspense fallback={Loading}><TodoList/></Suspense>
            }
        ]
    },

 

  • 위의 설정을 적용하면 todo/list로 접근할 경우 <Outlet> 부분이 ListPage 컴포넌트로 처리됨

 

실행 결과

 

 

2.6 중첩 라우팅의 분리와 리다이렉션

  • children 속성을 이용해서 중첩적인 라우팅 설정을 적용하게 되면, 페이지가 많아질수록 root.js 파일이 너무 복잡해지는 단점이 있다.
  • 별도의 함수에서 children 속성값에 해당하는 설정을 반환하는 방식으로 구현 가능

 

todoRouter.js

  • 기존의 root.js 파일의 설정 일부를 별도의 파일로 분리하고 설정을 반환하게 함
import React from 'react';
import { lazy } from 'react';
import { Suspense } from 'react';

const Loading = <div>Loading....</div>
const TodoList = lazy(() => import("../pages/todo/ListPage"))

const todoRouter = () => {
    return [
        {
            path:"list",
            element: <Suspense fallback={Loading}><TodoList/></Suspense>
        }
    ]
};

export default todoRouter;

 

그러면 root.js에서는 children 설정에 todoRouter 를 호출하도록 바꾸면 된다.

 

root.js

{
        path: "todo",
        element: <Suspense fallback={Loading}><TodoIndex/></Suspense>,
        children: todoRouter()
},

 

 

다음으로는 /todo/ 이하의 경로가 지정되지 않았을때  /todo/list 로 리다이렉션 처리가 되도록 할것이다.

 

리액트에서는 <Navigate> 의 replace 속성을 이용해 리다이렉션 처리를 할 수 있다.

 

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

 

 

2.7 URL Params

  • 특정한 번호의 Todo를 조회할 경우 /todo/read/123 과 같은 경로로 이동하도록 처리
  • ':' 을 활용해 처리 가능

 

todoRouder.js

 {
            path:"read/:tno",
            element: <Suspense fallback={Loading}><TodoRead/></Suspense>
        }

 

 

 

2.8 useParams()

  • 지정된 변수 추출 가능

ReadPage.js

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

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

 

 

실행 결과

 

 

useSearchParams()

  • ? 이후에 나오는 쿼리스트링 처리
  • ex) /todo/list?page=1&size=10

ListPage.js

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;

 

 

화면

 

 

useNavigate()

  • 고정된 링크 이동이 아닌, 동적으로 데이터를 처리해서 이동하는 방법

 

IndexPage.js

const IndexPage = () => {
    const navigate = useNavigate();
    const handleClickList = useCallback(()=>{
        navigate({pathname:'list'})
    })
    const handleClickAdd = useCallback(()=>{
        navigate({pathname:'add'})
    })


    return (
        <BasicLayyout>
            <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>
        </BasicLayyout>
    );
};

 

 

동적 페이지 이동을 테스트 하기 위해서, 수정/삭제로 이동할 수 있는 기능을 추가해 볼것이다.

  • 수정/삭제의 경우 매번 달라지는 번호를 사용하기 때문에 useParams()로 번호를 찾아서 구현해됨
  • 조회 화면에서 다시 목록 화면으로 이동할 때는 페이지 번호가 매번 달라질 수 있으므로 동적 처리 필요

ReadPage.js

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

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

    const moveToModify = useCallback(() => {
        navigate(`/todo/modify/${tno}`);
    }, [navigate, tno]);

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

export default ReadPage;

 

페이지 이동 결과

 

 

쿼리스트링의 유지

  • 조회 페이지는 다시 목록으로 이동할 수 있기 때문에 page 와 size 처럼 쿼리스트링으로 전달되는 데이터들을 유지하면서 이동해야 한다.
  • useSearchParams() 를 이용해서 쿼리스트링으로 전달된 데이터를 확인하기
  • createSearchParams() 함수를 이용해서 /todo/modify/xx 로 이동시에 필요한 쿼리스트링을 만들어내기
  • navigate() 로 이동하기

ReadPage.js

import React, { useCallback } from 'react';
import { createSearchParams, useNavigate, useParams, useSearchParams } from 'react-router-dom';

const ReadPage = () => {
    const { tno } = useParams();
    const navigate = useNavigate();
    const [queryParams] = useSearchParams();
    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
        });
    }, [tno, page, 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;

 

실행 결과

 

 

 

2.9 수정 / 삭제 페이지

  • 수정의 경우는 수정된 결과를 확인할 수 있는 조회 화면으로 다시 이동 가능해야됨
  • 삭제의 경우 목록 화면으로 이동

 

'개발 > 연습프로젝트기록' 카테고리의 다른 글

6. 리액트와 상품 API 서버 연동  (0) 2024.04.01
5. 상품 API 서버 구성하기  (0) 2024.04.01
4. 리액트와 API 서버 통신  (0) 2024.03.28
3. API 서버  (0) 2024.03.24
1. 개발 환경설정  (0) 2024.03.23