본문 바로가기

react

[리액트를 다루는 기술] 13장 리액트 라우터로 SPA 개발하기

 

13.1 라우팅이란?

 

- 사용자가 요청한 URL에 따라 알맞은 페이지를 보여주는 것

 

- 리액트에서 라우트 시스템을 구축하기 위해 사용할 수 있는 두가지 

 

1) 리액트 라우터 (React Router)

 

- 컴포넌트 기반으로 라우팅 시스템을 설정할 수 있음 

npm install react-router-dom

 

 

2) Next.js

 

- 리액트 프로젝트의 프레임워크 

- 리액트 프로젝트 설정을 하는 기능, 라우팅 시스템, 최적화 , 다국어 시스템 지원, 서버 사이드 렌더링 등 다양한 기능을 제공

- 라우팅 시스템은 파일 경로 기반으로 작동

 

 

13.2 싱글 페이지 애플리케이션이란?

 

멀티 페이지 애플리케이션 

 

- 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고, 페이지를 로딩할 때마다 서버에서 CSS,JS,이미지 파일 등의 리소스를 전달받아 브라우저 화면에 보여 줌 

 

- 사용자 인터랙션이 많고 다양한 정보를 제공하는 모던 웹 애플리케이션에는 부적합 

 

싱글 페이지 애플리케이션 

 

- 하나의 페이지로 이루어진 애플리케이션 

 

- 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 우선 웹 애플리케이션을 브라우저에 불러와서 실행시킨 후에 사용자와의 인터랙션이 발생하면 필요한 부분만 자바스크립트를 사용하여 업데이트 하는 방식

 

- 새로운 데이터가 필요하다면 서버 API를 호출하여 필요한 데이터만 새로 불러와 애플리케이션에서 사용할 수 있음 

 

- 링크를 눌러서 다른 페이지로 이동할 때 서버에 다른 페이지의 html을 새로 요청하는 것이 아니라, 브라우저의 History API를 사용하여 브라우저의 주소창의 값만 변경하고 기존에 페이지에 띄웠던 웹 애플리케이션을 그대로 유지하면서 라우팅 설정에 따라 또 다른 페이지를 보여주게 됨 

 

 

13.3 리액트 라우터 적용 및 기본 사용법 

 

13.3.1 프로젝트에 라우터 적용 

 

- 프로젝트에 리액트 라우터를 적용할 때는 src/index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter 컴포넌트를 사용하여 감싸면 됨 

 

- BrowserRouter 컴포넌트는 웹 애플리케이션에 HTML5의 Histroy API를 사용하여 페이지를 새로 불러오지 않고도 주소를 변경하고 현재 주소의 경로에 관련된 정보를 리액트 컴포넌트에서 사용할 수 있도록 해줌 

 

index.js

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


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

 

 

13.3.2 페이지 컴포넌트 만들기 

 

pages/Home.js

const Home = () => {
    return (
        <div>
            <h1>홈</h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
        </div>
    );
};

export default Home;

 

 

pages/About.js

const About = () => {
    return (
        <div>
            <h1>소개</h1>
            <p>리액트 라우터를 사용해 보는 프로젝트입니다. </p>
        </div>
    );
};

export default About;

 

 

13.3.3 Route 컴포넌트로 특정 경로에 원하는 컴포넌트 보여주기 

 

- 사용자의 브라우저 주소 경로에 따라 우리가 원하는 컴포넌트를 보여주려면 Route 컴포넌트를 통해 라우트 설정을 해주어야 함 

 

- Route 컴포넌트는 Routes 컴포넌트 내부에서 사용되어야 함 

 

<Route path="주소규칙" element={보여 줄 컴포넌트 JSX} />

 

App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />}/>
      <Route path="/about" element={<About />}/>
    </Routes>
  );
};

export default App;

 

 

13.3.4 Link 컴포넌트를 사용하여 다른 페이지로 이동하는 링크 보여주기 

 

- Link 컴포넌트는 페이지를 새로 불러오는 것을 막고 Histroy API를 통해 브라우저 주소의 경로만 바꾸는 기능이 내장되어 있음 

 

<Link to="경로">링크 이름</Link>

 

- Home 페이지에서 About 페이지로 이동할 수 있도록 Link 컴포넌트를 Home 페이지 컴포넌트에서 사용 

 

pages/Home.js

import { Link } from 'react-router-dom';

const Home = () => {
    return (
        <div>
            <h1>홈</h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
            <Link to="/about">소개</Link>
        </div>
    );
};

export default Home;

 

 

13.4 URL 파라미터와 쿼리스트링 

 

13.4.1 URL 파라미터 

 

- 주소의 경로에 유동적인 값을 넣는 형태 

- ID 또는 이름을 사용하여 특정 데이터를 조회할 때 사용 

 

pages/Profile.js

 

- username URL 파라미터를 통하여 프로필을 조회한 뒤에 프로필이 존재 하지 않으면 '존재하지 않는 프로필입니다.'라는 문구를 보여주고 존재한다면 프로필 정보를 보여줌

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

const data={
    velopert: {
        name: '김민준',
        description: '리액트를 좋아하는 개발자',
    },
    gildong: {
        name: '홍길동',
        description: '고전 소설 홍길동전의 주인공',
    },
};

const Profile = () => {
    const params = useParams(); 
    const profile = data[params.username];

    return (
        <div>
            <h1>사용자 프로필</h1>
            {profile ? (
                <div>
                    <h2>{profile.name}</h2>
                    <p>{profile.description}</p>
                </div>
            ): (
                <p>존재하지 않는 프로필입니다.</p>
            )}
        </div>
    );
};

export default Profile;

 

- URL 파라미터는 useParams라는 Hook을 사용하여 객체 형태로 조회할 수 있음 

- URL 파라미터의 이름은 라우트 설정을 할 때 Route 컴포넌트의 path props를 통해 설정 

 

App.js

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';
import Profile from './pages/Profile';

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Home />}/>
      <Route path="/about" element={<About />}/>
      <Route path="/profiles/:username" element={<Profile />} />
    </Routes>
  );
};

export default App;

 

- URL 파라미터는 /profile/:username과 같이 경로에 :를 사용하여 설정 

 

pages/Home.js

import { Link } from 'react-router-dom';

const Home = () => {
    return (
        <div>
            <h1>홈</h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
          <ul>
            <li>
                <Link to="/about">소개</Link>
            </li>
            <li>
                <Link to="/profiles/velopert">velopert의 프로필</Link>
            </li>
            <li>
                <Link to="/profiles/gildong">gildong의 프로필</Link>
            </li>
            <li>
                <Link to="/profiles/void">존재하지 않는 프로필</Link>
            </li>
          </ul>
        </div>
    );
};

export default Home;

 

 

13.4.2 쿼리 스트링 

 

- 주소의 뒷부분에 ? 문자열 이후에 key=value로 값을 정의하며 &로 구분하는 형태 

- 키워드 검색, 페이지네이션, 정렬 방식 등 데이터 조회에 필요한 옵션을 전달할 때 사용 

 

pages/About.js

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


const About = () => {
    const location = useLocation();

    return (
        <div>
            <h1>소개</h1>
            <p>리액트 라우터를 사용해 보는 프로젝트입니다. </p>
            <p>쿼리 스트링: {location.search}</p>
        </div>
    );
};

export default About;

 

- useLocation Hook은 location 객체를 반환 하는데, 이 객체는 현재 사용자가 보고 있는 페이지의 정보를 지니고 있음 

 

  -> pathname : 현재 주소의 경로 (쿼리스트링 제외)

  -> search : 맨 앞의 ? 문자를 포함한 쿼리스트링 값

  -> hash : 주소의 # 문자열 뒤의 값

  -> state : 페이지로 이동할 떄 임의로 넣을 수 있는 상태 값

  -> key : location 객체의 고유값. 초기에는 default이며 페이지가 변경될 때마다 고유의 값이 생성됨

 

- 쿼리스트링은 location.search 값을 통해 조회 가능 

 

- 쿼리스트링 값이 현재 ?detail=true&mode=1로 표시

- 이 문자열에서 앞에 있는 ?를 지우고, & 문자열로 분리한 뒤 key와 value를 파싱하는 작업을 해야 함 

 

- 리액트 라우터에서는 v6부터 useSearchParams Hook을 통해서 쿼리스트링을 더욱 쉽게 다룰 수 있음

 

- useSearchParams는 배열 타입의 값을 반환

- 첫 번째 원소는 쿼리파라미터를 조회하거나 수정하는 메서드들이 담긴 객체를 반환 

   -> get 메서드를 통해 특정 쿼리파라미터를 조회

   -> set 메서드를 통해 특정 쿼리파라미터를 업데이트

- 두 번째 원소는 쿼리 파라미터를 객체 형태로 업데이트 할 수 있는 함수를 반환 

 

 

 

 

pages/About.js

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


const About = () => {
    const [searchParams, setSearchParams] = useSearchParams();
    const detail = searchParams.get('detail');
    const mode = searchParams.get('mode');

    const onToggleDetail = () => {
        setSearchParams({mode, detail:detail === 'true' ? false:true});
    };

    const onIncreaseMode = () => {
        const nextMode = mode === null ? 1 : parseInt(mode) +1;
        setSearchParams({mode: nextMode, detail});
    };

    return (
        <div>
            <h1>소개</h1>
            <p>리액트 라우터를 사용해 보는 프로젝트입니다. </p>
            <p>detail: {detail} </p>
            <p>mode: {mode}</p>
            <buton onClikc={onToggleDetail}>Toggle detail</buton>
            <button onClick={onIncreaseMode}>mode + 1</button>
        </div>
    );
};

export default About;

 

 

 

13.5 중첩된 라우트 

 

pages/Articles.js

- 게시글 목록을 보여주는 페이지

import { Link } from 'react-router-dom';

const Articles = () => {
    return (
        <ul>
            <li>
                <Link to="/articles/1">게시글 1</Link>
            </li>
            <li>
                <Link to="/articles/2">게시글 2</Link>
            </li>
            <li>
                <Link to="/articles/3">게시글 3</Link>
            </li>
        </ul>
    );
};

export default Articles;

 

 

pages/Article.js

- 게시글을 읽는 페이지 

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

const Article = () => {
    const { id } = useParams();
    return (
        <div>
            <h2>게시글 {id}</h2>
        </div>
    );
};

export default Article;

 

 

App.js

- Article, Articles 페이지들의 라우트 설정

...
import Article from './pages/Article';
import Articles from './pages/Articles';

const App = () => {
  return (
    <Routes>
      ...
      <Route path="/articles" element={<Articles />} />
      <Route path="/articles/:id" element={<Article />} />
    </Routes>
  );
};

export default App;

 

 

pages/Home.js

- 게시글 목록 페이지로 가는 링크 추가 

...
           <li>
                <Link to="/articles">게시글 목록</Link>
            </li>
...

- 게시글 목록페이지에서 게시글을 열었을 때, 게시글 하단에 목록을 보여 주기 위해 중첩된 라우트 형태로 라우트 설정

 

App.js

...
import Article from './pages/Article';
import Articles from './pages/Articles';

const App = () => {
  return (
    <Routes>
     ...
      <Route  path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />}/>
      </Route>
    </Routes>
  );
};

export default App;

 

- Articles 컴포넌트에 리액트 라우터에서 제공하는 Outlet 컴포넌트를 사용해줘야 함 

- Outlet 컴포넌트Route의 children으로 들어가는 JSX 엘리먼트를 보여주는 역할 

 

pages/Articles.js

- Outlet 컴포넌트를 통해 <Route path=":id" element={<Article />} /> 내용이 보여짐

import { Link, Outlet } from 'react-router-dom';

const Articles = () => {
    return (
        <div>
            <Outlet />
        <ul>
            <li>
                <Link to="/articles/1">게시글 1</Link>
            </li>
            <li>
                <Link to="/articles/2">게시글 2</Link>
            </li>
            <li>
                <Link to="/articles/3">게시글 3</Link>
            </li>
        </ul>
     </div>
    );
};

export default Articles;

 

 

13.5.1 공통 레이아웃 컴포넌트 

 

- 중첩된 라우트와 Outlet은 페이지끼리 공통적으로 보여줘야 하는 레이아웃이 있을 때도 유용하게 사용 가능

- Home, About, Profile 페이지 상단에 헤더 보여줘야 하는 상황

 

src/Layout.js

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

const Layout = () => {
    return (
        <div>
            <header style={{background:'lightgray', padding:16, fontSize:24}}>
                Header
            </header>
            <main>
                <Outlet />
            </main>
        </div>
    );
};

export default Layout;

 

App.js

- 각 페이지 컴포너트가 보여져야 하는 부분에 Outlet 컴포넌트를 사용 

import { Route, Routes } from 'react-router-dom';
import Layout from './pages/Layout';
import About from './pages/About';
import Home from './pages/Home';
import Profile from './pages/Profile';
import Article from './pages/Article';
import Articles from './pages/Articles';

const App = () => {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route path="/" element={<Home />}/>
        <Route path="/about" element={<About />}/>
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route  path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />}/>
      </Route>
    </Routes>
  );
};

export default App;

 

 

13.5.2 index props

 

- Route 컴포넌트에는 index라는 props가 있음

- path="/"와 동일한 의미를 가짐 

- index prop를 사용하면 상위 라우트의 경로와 일치하지만, 그 이후에 경로가 주어지지 않았을 때 보여지는 라우트를 설정할 수 있음 

 

App.js

...
const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />}/>
        <Route path="/about" element={<About />}/>
        <Route path="/profiles/:username" element={<Profile />} />
      </Route>
      <Route  path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />}/>
      </Route>
    </Routes>
  );
};

 

 

13.6 리액트 라우터 부가 기능 

 

13.6.1 useNavigate

 

- Link 컴포넌트를 사용하지 않고 다른 페이지로 이동해야 하는 상황에 사용하는 Hook 

 

Layout.js

import { Outlet, useNavigate } from "react-router-dom";

const Layout = () => {
    const navigate = useNavigate();

    const goBack = () => {
        //이전 페이지로 이동
        navigate(-1);
    };

    const goArticles = () => {
        //articles 경로로 이동
        navigate('/articles');
    };

    return (
        <div>
            <header style={{background:'lightgray', padding:16, fontSize:24}}>
                <button onClick={goBack}>뒤로 가기</button>
                <button onClick={goArticles}>게시글 목록</button>
            </header>
            <main>
                <Outlet />
            </main>
        </div>
    );
};

export default Layout;

 

 

- navigate 함수를 사용할 때 파라미터가 숫자 타입이라면 앞으로 가거나, 뒤로 감

- 다른 페이지로 이동을 할 때 replace라는 옵션 있는데, 이 옵션을 사용하면 페이지를 이동할 때 현재 페이지를 페이지 기록에 남기지 않음 

 

 const goArticles = () => {
        //articles 경로로 이동
        navigate('/articles', {replace:true});
    };

 

 

13.6.2 NavLink

 

- 링크에서 사용하는 경로가 현재 라우트의 경로와 일치하는 경우 특정 스타일 또는 CSS 클래스를 적용하는 컴포넌트 

- style과 className은 { isActive: boolean} 을 파라미터로 전달받는 함수 타입의 값을 전달

 

Articles.js

import { NavLink, Outlet } from 'react-router-dom';

const Articles = () => {
  return (
    <div>
      <Outlet />
      <ul>
        <ArticleItem id={1} />
        <ArticleItem id={2} />
        <ArticleItem id={3} />
      </ul>
    </div>
  );
};

const ArticleItem = ({ id }) => {
  const activeStyle = {
    color: 'green',
    fontSize: 21,
  };
  return (
    <li>
      <NavLink
        to={`/articles/${id}`}
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      >
        게시글 {id}
      </NavLink>
    </li>
  );
};

export default Articles;

 

13.6.3 NotFound 페이지 만들기 

 

- 사전에 정의되지 않는 경로에 사용자가 진입했을 때 보여주는 페이지 

- 페이지를 찾을 수 없을 때 나타나는 페이지 

 

NotFound.js

const NotFound = () => {
    return (
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontSize: 64,
          position: 'absolute',
          width: '100%',
          height: '100%',
        }}
      >
        404
      </div>
    );
  };
  
  export default NotFound;

 

 

App.js

- * 라우트 엘리먼트의 상단에 위치하는 라우트들의 규칙을 모두 확인하고, 일치하는 라우트가 없다면 화면에 나타나게 됨 

import { Route, Routes } from 'react-router-dom';
...
const App = () => {
  return (
    <Routes>
      ...
      <Route path="*" element={<NotFound />}/>
    </Routes>
  );
};

export default App;

 

 

13.6.4 Navigate 컴포넌트 

 

- 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트 

- 페이지를 리다이렉트하고 싶을 때 사용

- 사용자의 로그인이 필요한 페이지인데 로그인을 안 했다면 로그인 페이지를 보여줘야 하는 상황에 사용

 

pages/Login.js

const Login = () =>{
    return <div>로그인 페이지</div>;
};

export default Login;

 

pages/MyPage.js

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

const MyPage = () => {
    const isLoggedIn = false;

    if(!isLoggedIn) {
        return <Navigate to="/login" replace={true} />;
    }

    return <div>마이 페이지</div>;
};

export default MyPage;

 

- isLoggedIn 값이 false라면 Navigate 컴포넌트를 통해 /login 경로로 이동

- repalce={true}는 페이지를 이동할 때 현재 페이지를 기록에 남기지 않기 때문에 이동 후 뒤로가기를 눌렀을 때 두 페이지 전의 페이지로 이동

 

App.js

const App = () => {
  return (
    <Routes>
      ...
      <Route path="/login" element={<Login />} />
      <Route path="/mypage" element={<MyPage />} />
      <Route path="*" element={<NotFound />}/>
    </Routes>
  );
};

export default App;