[React] 리액트 라우팅 - 벨로그 라우팅 클론

초코침·2023년 3월 24일
0

React

목록 보기
2/14

라우팅

웹 애플리케이션에서 사용자가 요청한 URL에 해당되는 페이지를 보여주는 것라우팅이라 한다.

여러 페이지로 구성된 웹 애플리케이션을 만들 때, 페이지 별로 컴포넌트를 분리하여 프로젝트를 관리하기 위해 라우팅 시스템이 필요하다.

MPA와 SPA

MPA

MPA(Multi Page Application)여러 페이지로 이루어진 애플리케이션을 말한다.

다른 페이지로 이동할 때마다 서버에서 해당 페이지의 html을 받아오고, 로딩할 때마다 서버에서 CSS, JS, 이미지 등의 리소스 파일을 전달받아 화면에 나타냈다.

사용자와의 인터랙션이 많지 않은 페이지의 경우 MPA 방식도 문제가 되지 않았으나, 모던 웹 애플리케이션은 사용자와의 인터랙션이 많고 보다 많은 정보를 제공하기 때문에 페이지를 로드할 때마다 항상 서버로부터 받아오는 작업이 필요하다면 사용자 경험이 저하될 것이다.


사용자 경험을 향상시키기 위해 뷰 렌더링은 사용자의 브라우저에서 담당하도록 하고, 사용자와의 인터랙션이 발생하면 그때 그때 필요한 부분만 자바스크립트를 사용하여 업데이트하는 방식을 사용하게 되었는데 그 방식이 바로 SPA다.

SPA

SPA(Single Page Application)하나의 페이지로 이루어진 애플리케이션을 말한다.

SPA는 MPA와 비교했을 때 보다 더 나은 사용자 경험을 제공한다. html은 한 번만 받아와서 웹 애플리케이션을 실행한 후, 이후에는 필요한 데이터만 받아와 업데이트하기 때문에 기술적으로는 한 페이지이지만, 사용자에게는 여러 페이지가 존재하는 것처럼 보일 수 있다.

하지만 MPA와 달리 대부분의 코드가 html이 아닌 JavaScript 파일에 들어있기 때문에 보다 무거운 JS 파일을 로드해야 하므로 첫 화면의 로딩 시간이 많이 소요된다는 단점이 있다. 그리고 검색 엔진 최적화(SEO) 관점에서 좋지 않다는 단점도 있는데 이 부분은 점차 개선중이라고 한다.

리액트 라우터 라이브러리

리액트 라우터 라이브러리는 리액트의 라우팅 라이브러리들 중에서 가장 생태계가 좋은 라이브러리다. 컴포넌트 기반으로 라우팅 시스템을 설정할 수 있다.

사용자의 브라우저 주소창의 경로에 따라 알맞은 페이지를 보여주게 되는데, 다른 페이지로 이동할 때 서버에 다른 페이지의 html을 새로 요청하는 것이 아니라 브라우저의 History API를 사용해 브라우저의 주소창 값만 변경하고 기존 페이지에 띄웠던 웹 애플리케이션을 그대로 유지하면서 또 다른 페이지를 보여준다.

설치하기

  • yarn
    yarn add react-router-dom
  • npm
    npm install 'react-router-dom'

벨로그의 라우팅 클론하기

자신의 벨로그 메인에 들어가면 나오는 라우팅 시스템을 비슷하게 구현해보려 한다.

(css는 최소한으로만 작성하고 라우팅에 집중해보려 합니다!)

1. 라우터 적용하기 - BrowserRouter

프로젝트에 react-router-dom 패키지를 설치한 다음, index.js 파일의 App 컴포넌트를 BrowserRouter로 감싸준다.

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

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

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

2. 컴포넌트 만들기

헤더 컴포넌트

벨로그 이름과 메뉴, 사용자 정보가 나와있는 부분을 모두 헤더로 묶었다. 이 부분은 라우팅되어도 항상 동일하게 보여줘야하는 부분이다.

라우팅 부분에 집중하기 위해 해당 부분을 직접 구현하지 않고 캡쳐해서 배치만 하였다.

import React from 'react';

import './Header.css';
import velogName from './img/velog-name.png';
import velogMenuBar from './img/velog-menu-bar.png';
import velogUserView from './img/velog-user-view.png';

const Header = () => {
  return (
    <header className="header">
      <div className="header__bar--wrapper">
        <VelogName />
        <VelogMenuBar />
      </div>
      <VelogUserView />
    </header>
  );
};

const VelogName = () => {
  return <img className="velog-name--img" src={velogName} alt="velogName" />;
};

const VelogMenuBar = () => {
  return <img className="velog-menu-bar--img" src={velogMenuBar} alt="velogMenuBar" />;
};

const VelogUserView = () => {
  return <img className="velog-user-view--img" src={velogUserView} alt="velogMenuBar" />;
};

export default Header;

페이지 컴포넌트

보여줄 페이지가 글, 시리즈, 소개 이렇게 3가지이므로 각 Articles.js, Series.js, About.js 이름으로 아주 간단한 컴포넌트를 만들었다.

/* 동일한 부분 */
import React from 'react';

const styles = {
  display: 'flex',
  height: '100vh',
  justifyContent: 'center',
  fontSize: '20px',
  fontWeight: '700',
  padding: '50px',
};

/* --------------- Articles.js --------------- */
const Articles = () => {
  return <div style={styles}>글 목록이 나타나는 공간입니다.</div>;
};

export default Articles;

/* --------------- Series.js --------------- */
const Series = () => {
  return <div style={styles}>시리즈가 나타나는 공간입니다.</div>;
};

export default Series;

/* --------------- About.js --------------- */
const About = () => {
  return <div style={styles}>소개가 나타나는 공간입니다.</div>;
};

export default About;

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

사용자의 브라우저 주소창에 있는 경로에 따라 원하는 컴포넌트를 보여주기 위해서 Route 컴포넌트를 사용할 수 있다.

Route는 해당 경로에 어떤 컴포넌트를 보여줄지 설정하는 역할이다.

Route 컴포넌트의 props로 pathelement를 작성해야 한다. 작성할 내용은 다음과 같다.

  • path: 경로
  • element: 해당 경로에서 보여줄 컴포넌트
<Route path='/path/name' element={<ComponentToShow />}

이때 Route 컴포넌트는 반드시 Routes 컴포넌트 내부에서 사용해야 한다.

<Routes>
	<Route ... />
<Routes>

위에서 작성한 세가지 페이지에 대한 주소는 다음과 같이 작성할 것이고, Articles 페이지를 기본값으로 사용할 것이다.

  • Articles: / (index 페이지)
  • Series: /series
  • About: /about

위에서 작성한 Header 컴포넌트와 Route 컴포넌트들을 App.js에 작성한다. Route 컴포넌트들은 Routes 컴포넌트 내부에 작성해야 한다.

import './App.css';
import Header from './Header.js';
import { Routes, Route } from 'react-router-dom';
import Articles from './pages/Articles';
import Series from './pages/Series';
import About from './pages/About';
import Tabs from './Tabs';

function App() {
  return (
    <div className="App">
      <Header />
      <Routes>
        <Route index element={<Articles />} />
        <Route path="/series" element={<Series />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
}

export default App;

이때 Articles 컴포넌트의 path를 작성하지 않고 index라는 props를 줬는데 이는 path=’/’와 동일하다.

path=’/’ 대신 index props를 사용할 수 있다.

클릭 시 다른 페이지로 이동할 링크는 Link 컴포넌트를 사용해 보여줘야 한다.

Link 컴포넌트는 다른 페이지로 이동하는 링크를 보여주는 역할을 한다.


Link 컴포넌트는 props로 to를 작성해야 한다.

  • to: 이동할 경로

[참고] Link와 a 태그의 차이?

Link 컴포넌트는 a 태그와 비슷한 역할을 하는데, 페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾼다는 점에서 다르다. SPA 구현 시 페이지를 새로 불러올 필요가 없기 때문에 리액트 라우터를 사용할 때 Link 대신 a를 사용하면 안 된다.


페이지를 변경하기 위해 클릭할 버튼들을 모아둔 Tabs 컴포넌트를 만들어 다음처럼 작성했다.

글, 시리즈, 소개를 클릭하면 해당 경로로 이동해 Route에서 설정한 컴포넌트들을 보여준다.

// Tabs.js
import React from 'react';
import { Link } from 'react-router-dom';

import './Tabs.css';

const Tabs = () => {
  return (
    <div className="Link--container">
      <Link className="link" to="/"></Link>
      <Link className="link" to="/series">
        시리즈
      </Link>
      <Link className="link" to="/about">
        소개
      </Link>
    </div>
  );
};

export default Tabs;
// App.js
function App() {
  return (
    <div className="App">
      <Header />
      <Tabs /> {/* 컴포넌트 추가*/}
      <Routes>
        <Route index element={<Articles />} />
        <Route path="/series" element={<Series />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
}


여기까지 작성하면 간단한 라우팅 시스템을 구현할 수 있다.

클릭할 때마다 주소창의 경로가 변경되고, 하단에 나타나는 컴포넌트도 달라진다.

리액트 라우터 부가 기능

공통 레이아웃 컴포넌트 - Outlet

작성한 코드에서 변경되는 부분은 Tabs 아래 부분이고, Header와 Tabs 컴포넌트는 어느 경로로 이동하더라도 동일하게 나타난다.

이러한 부분을 리액트 라우터에서 제공하는 Outlet 컴포넌트를 이용해 구현할 수 있다.

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


공통 레아이웃을 위해 Layout 컴포넌트를 만들었다.

import React from 'react';
import { Outlet } from 'react-router-dom';

import Header from './Header';
import Tabs from './Tabs';

const Layout = () => {
  return (
    <div>
      <Header />
      <Tabs />
      <main>
        <Outlet />
      </main>
    </div>
  );
};

export default Layout;

Layout 컴포넌트를 App에 가져와 중첩된 라우트를 만든다.

import './App.css';
import { Routes, Route } from 'react-router-dom';

import Layout from './Layout';
import Articles from './pages/Articles';
import Series from './pages/Series';
import About from './pages/About';

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Articles />} />
          <Route path="/series" element={<Series />} />
          <Route path="/about" element={<About />} />
        </Route>
      </Routes>
    </div>
  );
}

export default App;

벨로그에는 선택된 탭의 글자의 컬러가 바뀌는 기능이 있다. 이러한 기능을 NavLink로 구현할 수 있다.

→ 주소에 맞는 탭의 글자 색깔이 변경된다.


NavLink 컴포넌트는 주소창의 경로와 현재 라우트 경로가 일치할 때 특정 스타일(CSS)을 적용할 수 있는 컴포넌트다. 이 컴포넌트의 style과 className은 { isActive : boolean }을 인수로 갖는 함수를 받는다.

/* style */
<NavLink style={({ isActive }) => (isActive ? activeStyle : undefined)}></NavLink>;
/* className */
<NavLink className={({ isActive }) => (isActive ? activeStyle : undefined)}></NavLink>;

사용할 스타일을 정의하고 기존의 Link 컴포넌트를 NavLink로 바꾼 다음 isActive이 true일 때 activeStyle을 적용하도록 했다.

const Tabs = () => {
  const activeStyle = {
    color: '#62e6be',
  };

  return (
    <div className="Link--container">
      <NavLink
        className="link"
        to="/"
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      ></NavLink>
      <NavLink
        className="link"
        to="/series"
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      >
        시리즈
      </NavLink>
      <NavLink
        className="link"
        to="/about"
        style={({ isActive }) => (isActive ? activeStyle : undefined)}
      >
        소개
      </NavLink>
    </div>
  );
};

위 코드는 비슷한 내용이 반복되기 때문에 Tab이라는 컴포넌트를 하나 더 만들어 간결하게 리팩터링해 봤다.

const Tabs = () => {
  return (
    <div className="Link--container">
      <Tab to="/" content="" />
      <Tab to="/series" content="시리즈" />
      <Tab to="/about" content="소개" />
    </div>
  );
};

const Tab = ({ to, content }) => {
  const activeStyle = {
    color: '#62e6be',
  };

  return (
    <NavLink
      className="link"
      to={to}
      style={({ isActive }) => (isActive ? activeStyle : undefined)}
    >
      {content}
    </NavLink>
  );
};

NotFound 페이지 만들기

NotFound 페이지는 경로를 *로 표시하고, NotFound 페이지에서 보여줄 컴포넌트를 만들어 연결하면 된다.

<Route path='*' element={<NotFound />} />

단, NotFound 페이지의 경우 Layout 컴포넌트는 같이 표시되지 않아도 되므로 중첩된 라우팅 내부가 아닌 바깥에 Route를 넣어준다.

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Articles />} />
          <Route path="/series" element={<Series />} />
          <Route path="/about" element={<About />} />
        </Route>
				<Route path='*' element={<NotFound />} />
      </Routes>
    </div>
  );
}

전체 코드 Github




Reference

https://www.outsystems.com/glossary/what-is-single-page-application/

리액트를 다루는 기술 (김민준)

profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글