본문 바로가기

Front End/React

[React Exam] react-router-dom 사용해보기 1

라우팅이란 사용자가 요청한 URL에 따라 알맞는 페이지를 보여주는 것으로 웹애플리케이션이 커지게 되면 하나의 페이지로 구성하기가 매우 어렵습니다. 이럴 때 여러 페이지로 구성된 웹애플리케이션을 페이지별로 컴포넌트들을 분리해가면서 프로젝트를 관리하기 위해 필요한 것이 바로 라우팅 시스템입니다.

react-router-dom은 리액트의 라우팅 관련 라이브러리들 중에서 가장 많이 사용되고 있으며 컴포넌트 기반으로 라우팅 시스템을 설정할 수 있습니다.

간단하게 사용법을 알아 보도록 하겠습니다.

 

1. 프로젝트 생성 및 라이브러리 설치

npx create-react-app router1
cd router1
npm i react-bootstrap bootstrap react-bootstrap-icons
npm i dayjs holderjs 
npm i react-router-dom

 

public 폴더의 모든 파일을 삭제하고 index.html 파일을 다음과 같이 만듭니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

 

src 폴더의 모든 파일을 지우고 App1.js파일을 다음과 같이 만듭니다.

function App1() {
  return <h2>Hello World</h2>
}

export default App1;

 

index.js 파일을 다음과 같이 만듭니다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App1';
import { BrowserRouter } from 'react-router-dom';

document.querySelector("title").innerText = "react-router-dom tutorial";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
     <BrowserRouter  future={{v7_startTransition: true, v7_relativeSplatPath:true}}>
      <App />
     </BrowserRouter>    
  </React.StrictMode>
);

 

document.querySelector("title").innerText = "react-router-dom tutorial";로 타이틀을 변경했습니다.

그리고 <App/> 컴포넌트를 <BrowserRouter></ BrowserRouter > 감싸 주시면 됩니다.

그러면 브라우저에 경고가 2개 나오는데 이때는 future 속성을 위와 같이 추가해주면 경고는 사라집니다.

 

각각의 페이지를 나타낼 컴포넌트들을 다음과 같이 만듭니다.

function Home(){
  return (
    <div>
      <h2>
        메인 화면입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

function Page1(){
  return (
    <div>
      <h2>
        첫번째 페이지입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>  
      </h2>
    </div>
  );
}

function Page2(){
  return (
    <div>
      <h2>
        두번째 페이지입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

function NotFound() {
  return (
    <div>
      <h2>
        존재하지 않는 주소입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

 

그리고 링크를 누르면 이동할 메뉴를 만듭니다. 

이동할때는 <Link/>태그를 사용합니다.

<Link to="이동할 주소">표시될 텍스트</Link>
function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/home">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
    </nav>
  );
}

 

이제 주소를 직접 만들어 보겠습니다.

react-router-dom의 Routes와 Route 컴포넌트를 사용합니다. Routes안에 Route 컴포넌트를 사용합니다.

      <Routes>
        <Route path="만들주소 " element={표시될컴포넌트}/>
      </Routes>

 

<Route path="/" element={<Menu />}/> 는 /(root)에 Menu컴포넌트를 표시하겠다는 것입니다.

function App2() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Menu />}/>
        <Route path="/home" element={<Home />} />
        <Route path="/page1" element={<Page1 />} />
        <Route path="/page2" element={<Page2 />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </>
  );
}

 

전체 소스 입니다.

App2.css

nav, div {
    margin: 10px;
}
nav>ul>li {
    list-style-type: none;
    display: inline;
    margin: 0px 10px;
}

 

App2.js

import { Button } from "react-bootstrap";
import { Link, Outlet, Route, Routes } from "react-router-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import './App2.css'

function Home(){
  return (
    <div>
      <h2>
        메인 화면입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

function Page1(){
  return (
    <div>
      <h2>
        첫번째 페이지입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>  
      </h2>
    </div>
  );
}

function Page2(){
  return (
    <div>
      <h2>
        두번째 페이지입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

function NotFound() {
  return (
    <div>
      <h2>
        존재하지 않는 주소입니다.
        {` `}
        <Link to="/">
          <Button variant="outline-danger" size="sm">Home</Button>
        </Link>
      </h2>
    </div>
  );
}

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/home">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
    </nav>
  );
}

function App2() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Menu />}/>
        <Route path="/home" element={<Home />} />
        <Route path="/page1" element={<Page1 />} />
        <Route path="/page2" element={<Page2 />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </>
  );
}

export default App2;

 

 

index.js파일의 다음만 변경합니다.


import
App from './App1'; 를 import App from './App2';으로 변경합니다.

 

다음과 같이 메뉴가 표시되며 각각의 링크를 클릭해서 각각의 컴포넌트 페이지로 이동이 가능합니다.

 

Home 버튼을 눌러 메뉴화면으로 복귀가 가능합니다.

 

주소 표시줄에 아무 주소나 입력하면 <NotFound/> 컴포넌트를 출력합니다.

 

메뉴바는 항상 나타나야 합니다. 이럴때는 중첩 라우팅을 이용하면 됩니다.

 

/ 밑으로 4개의 주소를 중첩해서 넣었습니다. 서브페이지들은 /를 빼고 작성합니다. 

그러면 주소는 "상위라우터 Path/하위라우터 Path"형태로 만들어 집니다.

    <Routes>
        <Route path="/" element={<Menu />}>
            <Route index element={<Home />} />
            <Route path="page1" element={<Page1 />} />
            <Route path="page2" element={<Page2 />} />
            <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>

 

"<Route index element={} />" 하위메뉴를 선택하지 않았을때 기본적으로 보여줄 하위 컴포넌트를 지정하는것 입니다.

 

<Outlet/>으로 부모 라우트의 컴포넌트에서 자식 라우트 컴포넌트의 위치를 지정해 주어야 합니다.

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
      <hr/>   {/* 선추가 */}
      <Outlet/> {/* 하위 컴포넌트가  표시될 위치 */}
    </nav>
  );
}

 

App3.js파일 전체입니다. 각각의 컴포넌트에서 버튼을 제거 했습니다.

import { Button } from "react-bootstrap";
import { Link, Outlet, Route, Routes } from "react-router-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import './App2.css'

function Home(){
  return (
    <div>
      <h2>메인 화면입니다. </h2>
    </div>
  );
}

function Page1(){
  return (
    <div>
      <h2>첫번째 페이지입니다. </h2>
    </div>
  );
}

function Page2(){
  return (
    <div>
      <h2>두번째 페이지입니다. </h2>
    </div>
  );
}

function NotFound() {
  return (
    <div>
      <h2>존재하지 않는 주소입니다.</h2>
    </div>
  );
}

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
      <hr/>
      <Outlet/>
    </nav>
  );
}

function App3() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Menu />}>
            <Route index element={<Home />} />
            <Route path="page1" element={<Page1 />} />
            <Route path="page2" element={<Page2 />} />
            <Route path="*" element={<NotFound />} />  
        </Route>
      </Routes>
    </>
  );
}

export default App3;

 

index.js파일의 다음만 변경합니다.


import
 App from './App2'; 를 import App from './App3';으로 변경합니다.

 

결과 화면입니다. 하단에 서브 페이지가 표시 됩니다. 기본 서브페이지는 index를 지정한 <Home/> 컴포넌트 입니다.

 

메뉴를 눌러 이동해 봅니다.

 

 

<Outlet/> 없이도 구현이 가능합니다.

다음과 같이 와일드카드를 이용하여 주소를 생성합니다. 하위에 주소가 존재한다고 지정해 주는것 입니다.

      <Routes>
        <Route path="/*" element={<Menu />}/>
      </Routes>

 

그리고 보여줄 컴포넌트에 <Outlet/> 자리에  <Router>를 추가해 줍니다.

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
      <hr/>
      <Routes>
        <Route index element={<Home />} />
        <Route path="page1" element={<Page1 />} />
        <Route path="page2" element={<Page2 />} />
        <Route path="*" element={<NotFound />} />  
      </Routes>
    </nav>
  );
}

 

 

완성된 App4.js파일입니다.

import { Button } from "react-bootstrap";
import { Link, Outlet, Route, Routes } from "react-router-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import './App2.css'

function Home(){
  return (
    <div>
      <h2>메인 화면입니다. </h2>
    </div>
  );
}

function Page1(){
  return (
    <div>
      <h2>첫번째 페이지입니다. </h2>
    </div>
  );
}

function Page2(){
  return (
    <div>
      <h2>두번째 페이지입니다. </h2>
    </div>
  );
}

function NotFound() {
  return (
    <div>
      <h2>존재하지 않는 주소입니다.</h2>
    </div>
  );
}

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/page2">Page2</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
      <hr/>
      <Routes>
        <Route index element={<Home />} />
        <Route path="page1" element={<Page1 />} />
        <Route path="page2" element={<Page2 />} />
        <Route path="*" element={<NotFound />} />  
      </Routes>
    </nav>
  );
}

function App3() {
  return (
    <>
      <Routes>
        <Route path="/*" element={<Menu />}/>
      </Routes>
    </>
  );
}

export default App3;

 

index.js파일의 다음만 변경합니다.


import
 App from './App3'; 를 import App from './App4';으로 변경합니다.

 

결과 화면은 같습니다.

 

 

useParams(), useLocation(), useSearchParams()

useParams(), useLocation(), useSearchParams()을 이용한 파라메터 처리를 해보겠습니다.

useParams()


/comments : 전체 목록보기

/comments/1 : 1번글 보기
/comments/2 : 2번글 보기
/comments/3 : 3번글 보기

위와 같이 처리하기 위해서는 useParam()을 이용합니다.

라우터를 다음과 같이 정의 합니다.

            <Route path="comments" element={<Comments />} >
              <Route path=":id" element={<Comment />} />
            </Route>

 

그럼 Comment 에서 다음과 같이 하면 id값을 확인할 수 있습니다.

    // URL 파라미터 사용하기
    const params = useParams();
    console.log(`파라메터 : ${JSON.stringify(params)}`);
    console.log(`파라메터 : ${params.id}`);
   

 

/comments/1 요청시 params.id 값은 1이 됩니다.
이 id값을 이용하여 원하는 데이터만 찾아서 처리를 하면 id값마다 다른 처리가 가능합니다.

 

useLocation()

페이지 정보를 처리할떄는 useLocation()을 이용합니다.

    const pageInfo = useLocation();
    console.log(JSON.stringify(pageInfo));
    console.log(pageInfo.search); // Query String 확인

 

다음과 같은 정보를 확인할 수 있습니다. 여기에서 search키를 확인하면 요청한 QueryString을 확인할 수 있습니다.

{"pathname":"/hanjas/102","search":"?show=false","hash":"","state":null,"key":"zfy8bgps"}

?show=false

 

useSearchParams()

하지만 위의 search는 문자열이므로 또 다시 파싱을 해야 합니다.

예를 들어 "?a=100&b=200&c=300"의 경우 각각 a에는 100을  b에는 200을  c에는 300을 보낸것인데 이것을 파싱해 줘야 합니다. 이것을 점눈으로 담당하는 useSearchParams()을 이용하면 편리합니다.

 

다음과 같이 하면 각각의 변수에 각각의 값을 얻을 수 있습니다. 하지만 타입이 무조건 string이므로 필요에 따라 값을 형변환 해서 사용해야 합니다. 

    //  Query String 사용
    const [searchParams, setSearchParams] = useSearchParams();
   
    console.log(typeof(searchParams) + " : " + searchParams);
    for(let key of searchParams.keys()){
        console.log(key + " : " + searchParams.get(key));
    }


    ?a=100&b=200&c=300 요청 시

   a = 100
   b = 200
   c = 300 
 
  으로 처리가 가능합니다.

 

 

그럼 사자성어 20개를 가지고 실습을 진행해 보도록 하겠습니다.

 

Data.js 파일을 다음과 같이 만듭니다.

export const HANJA = [
{"id": 101, "h":"萬死一生","k":"만사일생","t":"만 번의 죽을 고비에서 살아난다는 말로, 겨우 죽음을 모면하는 것"},
{"id": 102, "h":"三間草家","k":"삼간초가","t":"세 칸짜리 초가집이라는 뜻으로 작고 보잘 것 없는 집."},
{"id": 103, "h":"二人同心","k":"이인동심","t":"두 사람의 마음이 하나가 됨. 절친한 친구 사이"},
{"id": 104, "h":"一字千金","k":"일자천금","t":"아주 빼어난 글이나 시문"},
{"id": 105, "h":"敎學相長","k":"교학상장","t":"가르치고 배우면 서로가 성장한다."},
{"id": 106, "h":"九十春光","k":"구십춘광","t":"노인의 마음이 청년같이 젊음을 이름"},
{"id": 107, "h":"男女老少","k":"남녀노소","t":"남자와 여자, 늙은이와 젊은이. 모든 사람."},
{"id": 108, "h":"門前成市","k":"문전성시","t":"권세가의 문 앞이 방문객으로 몹시 붐빔"},
{"id": 109, "h":"百年大計","k":"백년대계","t":"먼 뒷날까지 걸친 큰 계획"},
{"id": 110, "h":"白面書生","k":"백면서생","t":"글만 읽고 세상일에 경험이 없는 사람"},
{"id": 111, "h":"三生有幸","k":"삼생유행","t":"세 번 태어날 행운이 있다는 뜻으로 지극히 운 좋음"},
{"id": 112, "h":"先行後敎","k":"선행후교","t":"선인의 행위를 들어 후학을 가르침."},
{"id": 113, "h":"自問自答","k":"자문자답","t":"제가 묻고 제가 대답함."},
{"id": 114, "h":"作心三日","k":"작심삼일","t":"한번 결심한 것이 사흘을 가지 않음. 곧 결심이 굳지 못함"},
{"id": 115, "h":"長生不死","k":"장생불사","t":"오랫동안 살아 죽지 아니함."},
{"id": 116, "h":"千年一淸","k":"천년일청","t":"黃河같은 濁流가 맑아지기를 천년 동안 바란다. 가능하지 않은 일을 바라는 것."},
{"id": 117, "h":"靑山流水","k":"청산유수","t":"청산에 흐르는 물. 거침없이 잘하는 말에 비유."},
{"id": 118, "h":"八方美人","k":"팔방미인","t":"여러 방면의 일에 두루 능통한 사람"},
{"id": 119, "h":"風月主人","k":"풍월주인","t":"맑은 바람과 밝은 달을 벗하여 노는 한가한 사람. 자연 풍경을 좋아하는 사람."},
{"id": 120, "h":"木人石心","k":"목인석심","t":"의지가 굳어 어떠한 유혹에도 마음이 흔들리지 않는 사람"}
];

 

사자성어 목록을 보여줄 컴포넌트를 만듭니다.

Hanjas.js파일을 다음과 같이 만듭니다.

import { NavLink, Outlet } from 'react-router-dom';
import {HANJA} from './Data'
const Hanjas = ()=>{
    return (
        <>
            <div className='box'>        
            {
                HANJA.map((hanja, index)=>{
                    let content = `${hanja.k}`;
                    let no = `${index+1}`.padStart(3, 0) + '. '
                    return (
                        <div key={hanja.h} >
                            {no}
                            {/*
                            <Link key={hanja.h} to={`/hanjas/${hanja.id}`}>{content}</Link> <br/>
                            */}
                            {/* Link 대신 NavLink 컴포넌트를 사용하면 링크에서 사용하는 경로가
                                현재 라우트의 경로와 일치하는 경우 특정 CSS를 적용할 수 있습니다. */
}
                            <NavLink
                            to={`/hanjas/${hanja.id}?show=false`}
                            style={
                                ({ isActive }) => {
                                    return { background: isActive ? "silver" : "", };
                                }
                            }
                            >
                            {content}
                            </NavLink>  
                        </div>
                    )
                })
            }
            </div>
            <div>
                <Outlet/>
            </div>
        </>
    )
}

export default Hanjas;

 

왼쪽의 사자성어를 클릭하면 id값을 가지고 가서 오른쪽에 사자성어에 대한 설명을 표시하도록 만들었습니다.

App2.css 파일을 수정합니다.

nav, div {
    margin: 5px;
}
nav>ul>li {
    list-style-type: none;
    display: inline;
    margin: 0px 10px;
}

.box {
    width: 130px;
    float: left;
}

 

 

1개의 사자성어를 표시할 컴포넌트를 만듭니다.

Hanja.js 파일을 다음과 같이 만듭니다.

import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { HANJA } from './Data'
import { Button } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';

const Hanja = ()=>{
    // URL 파라미터 사용하기
    const params = useParams();
    // console.log(`파라메터 : ${JSON.stringify(params)}`);
    // console.log(`파라메터 : ${params.id}`);

    // 넘어온 아이디에 해당하는 한자를 찾는다.
    const hanja = HANJA.find((h)=>h.id===parseInt(params.id));
    // console.log(hanja);
   
    const pageInfo = useLocation();
    // console.log(JSON.stringify(pageInfo));
    // console.log(pageInfo.search); // Query String 확인

    //  Query String 사용
    const [searchParams, setSearchParams] = useSearchParams();
    /*
    console.log(typeof(searchParams) + " : " + searchParams);
    for(let key of searchParams.keys()){
        console.log(key + " : " + searchParams.get(key));
    }
    */
    let show = searchParams.get('show')==='true';
    console.log(`${typeof(show)} : ${show}`);
    let title = show ? '설명 숨기기' : '설명 보이기';
    const clickHandler = (e)=>{
        let text = e.target.innerText;
        if(text==='설명 보이기'){
            e.target.innerText = '설명 숨기기';
            show = true;
            setSearchParams({show : true});
        }else{
            e.target.innerText = '설명 보이기';
            show = false;
            setSearchParams({show : false});
        }
    }
    return (
        <div>
            <h2>
                {hanja.h}({hanja.k})
                {` `}
                <Button variant='outline-success' size="sm" onClick={clickHandler}>{title}</Button>
            </h2>
            {show ? <h2>{hanja.t}</h2> : ""}
        </div>
    )
}

export default Hanja;

 

파라메터로 id값을 받아 사자성어 목록 중 원하는 사자 성어만 얻어옵니다.

그리고 Query String으로 show 값을 받아 true이면 설명을 보이고 false이면 설명을 숨기는 기능을 버튼에 추가해 보았습니다.

 

이제 App5.js파일을 수정해서 결과를 확인해 보도록 하겠습니다.

import { Link, Outlet, Route, Routes } from "react-router-dom";
import './App2.css'
import Hanjas from "./Hanjas";
import Hanja from "./Hanja";

function Home(){
  return (
    <div>
      <h2>메인 화면입니다. </h2>
    </div>
  );
}

function Page1(){
  return (
    <div>
      <h2>첫번째 페이지입니다. </h2>
    </div>
  );
}

function NotFound() {
  return (
    <div>
      <h2>존재하지 않는 주소입니다.</h2>
    </div>
  );
}

function Menu(){
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/page1">Page1</Link>
        </li>
        <li>
          <Link to="/hanjas">사자성어</Link>
        </li>
        <li>
          <Link to="/notFound">NotFound</Link>
        </li>
      </ul>
      <hr/>
      <Outlet/>
    </nav>
  );
}

function App5() {
  return (
    <>
      <Routes>
        <Route path="/" element={<Menu />}>
            <Route index element={<Home />} />
            <Route path="page1" element={<Page1 />} />
            <Route path="hanjas" element={<Hanjas />} >
              <Route path=":id" element={<Hanja />} />
            </Route>
            <Route path="*" element={<NotFound />} />  
        </Route>
      </Routes>
    </>
  );
}

export default App5;

 

 

index.js파일의 다음만 변경합니다.


import
 App from './App4'; 를 import App from './App5';로 변경합니다.

 

결과 화면은 다음과 같습니다.

 

이렇게 react-router-dom에 대하여 간단히 살펴 보았습니다.

App1.js
0.00MB
App2.css
0.00MB
App2.js
0.00MB
App3.js
0.00MB
App4.js
0.00MB
App5.js
0.00MB
Data.js
0.00MB
Hanja.js
0.00MB
Hanjas.js
0.00MB
index.js
0.00MB

 

 

index.html
0.00MB