리액트 라우터로 SPA 개발하기

nasagong·2023년 2월 23일
0

React

목록 보기
13/15
post-thumbnail

📚 들어가며

리액트의 꽃, SPA를 리액트 라우터를 사용해 구현해보자.


라우팅?

웹 애플리케이션에서 라우팅은 사용자가 요청한 URL에 따라 알맞은 페이지를 보여주는 것을 의미한다. 이 때 페이지는 하나가 될 수도, 여러개가 될 수도 있을 것이다.

가령 글쓰기, 글 목록 보기, 글 읽기 기능이 필요한 블로그를 구현하려면 여러 페이지가 필요할 것이다. 이러한 프로젝트를 관리하기 위해 라우팅 개념이 사용된다.

라우팅을 사용해 시스템을 구축하기 위해 리액트라우터Next.js를 사용할 수 있는데, 이번 장에서는 리액트 라우터를 사용한다.

SPA (Single Page Application)

SPA는 문자대로 하나의 페이지로 이루어진 어플리케이션을 말한다. 리액트 라우터를 사용하면 앞서 예시로 든 블로그 같은 어플리케이션을 개발할 때도 멀티 페이지가 아닌 싱글페이지로 개발할 수 있다.

이미지의 첫번째 예시 같은 경우는 사용자가 다른 페이지로 이동할 때마다 html을 받아와 새로고침이 발생하지만, SPA예시는 첫 요청에서만 html을 받아오고 그 후는 새로고침이 발생하지 않는다.

라우터를 이용한 컴포넌트 관리

우선 프로젝트에 라우터를 적용해줘야 한다. src/index.js 파일을 아래처럼 수정해주자.

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

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

src에 pages디렉토리를 만들어 그 안에 Home과 About컴포넌트를 만들어 연결해 볼 것이다.

//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/>} />
      // path ="/" <- Root URL === homepage
      <Route path="/about" element={<About/>}/>
    </Routes>
  );
};
export default App;

브라우저 주소 경로에 따라 원하는 컴포넌트를 보여주기 위해선 Route 컴포넌트를 통해 라우트 설정을 해줘야 한다. 단, Route컴포넌트는 Routes 컴포넌트 내부에서 사용돼야 한다.

이제 Home, About 컴포넌트를 확인해보자.

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

const Home = () =>{
    return (
        <div>
            <h1></h1>
            <p>가장 먼저 보여지는 페이지입니다.</p>
            <Link to="/about">소개</Link>
        </div>
    )
};
export default Home;
//About.js
const About = () =>{
    return(
        <div>
            <h1>소개</h1>
            <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
        </div>
    );
};
export default About;

App을 렌더링해보면 아래처럼 나온다.


처음에 Home컴포넌트가 표시되고, Link된 소개를 누르면 About컴포넌트가 새로고침 없이 표시된다.

URL 파라미터와 쿼리스트링

페이지 주소를 유동적인 값으로 정해야 될 때가 있다. 두 가지 예시를 보자.

  • URL 파라미터 : /profile/nasagong
  • 쿼리스트링 : /articles?page=1&keyword=react

URL 파라미터는 주소의 경로에 유동적인 값을 넣는 형태,
쿼리스트링은 주소 뒷부분에 “?” 뒤로 key=value 형태로 값을 정의하며 &으로 구분한다.

URL파라미터는 id 또는 이름을 사용하여 특정 데이터를 조회할 때, 쿼리스트링은 키워드 검색 둥 데이터 조회에 필요한 옵션을 전달할 때 사용된다고 한다.

URL파라미터를 사용하는 법을 먼저 알아보자.

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/nasagong">nasagong의 프로필</Link>
                </li>
                <li>
                    <Link to="/profiles/gildong">gildong의 프로필</Link>
                </li>
                <li>
                    <Link to="/profiles/void">존재하지 않는 프로필</Link>
                </li>
            </ul>
        </div>
    )
};
export default Home;

profiles/어쩌구 형태로 링크가 여러개 걸려있다. 각각 컴포넌트를 하나씩 만들어서 연결시키는 걸까?

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;

그렇지 않은 것 같다. Route컴포넌트는 하나만 있긴 한데.. path로 사용된 profiles/:username 이 심상치 않다. 이제 Profiles 컴포넌트를 확인해 진상을 알아보자.

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

const data ={
    nasagong : {
        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 파라미터는 /profiles/:username 처럼 경로에 :를 사용하여 설정한다. 처음엔 좀 이해하는 데 시간이 걸렸는데..

  1. Home에서 Link를 사용해 “/profiles/이름” 을 path로 둔 컴포넌트에 연결한다
  2. useParams를 통해 Profile컴포넌트는 유동적으로 주어진 username을 읽어낸다.

가령 Link로 /profiles/nasagong을 연결하려고 한다면 profiles/:username이 path인 Profile컴포넌트와 일단 연결되고, Profile은 useParams를 통해 {username : nasagong} 같은 객체 형태로 params를 저장한다.

그 후 data값에 params를 사용해 접근한다.
data[params.username]은 data["nasagong"]과 같은 표현이다. nasagong을 key로 갖는 value값을 profile에 할당해주고,

profile을 통해 유저의 데이터에 접근해 적절한 페이지를 렌더링해낸다. 페이지 주소를 유동적으로 정한다는 건 이런 뜻이었다.

쿼리스트링

About 컴포넌트를 아래처럼 바꿔보자.

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;

위 코드에 사용된 hook인 useLocation은 객체를 반환하는데, 객체 안에는 아래와 같은 값들이 있다.

  • pathname : 현재 주소의 경로 (쿼리스트링 제외)
  • search : 물음표를 포함한 쿼리스트링 값
  • hash : 주소의 # 문자열 뒤의 값
  • state : 페이지로 이동할 때 임의로 넣을 수 있는 값
  • key : location 객체의 고유값. 초기에는 default지만 페이지가 변경될 떄마다 고유의 값이 생성됨.

예시로 적은 코드는 반환된 객체의 search값을 조회하여 보여주고 있다. 주소창에 http://localhost:3000/about?detail=true&mode=1을 입력하면 사진과 같이 쿼리스트링 값이 표시되는 페이지가 나온다.

쿼리스트링 값은 &으로 구분지어져 있는데, detali,mode값을 따로 파싱하려면 어떡해야 할까? 리액트 라우터의 useSearchParams 훅을 사용하면 쉽게 파싱이 가능하다.

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:mode,detail:detail === 'true'?false:true});
    };
    const onIncreaseMode = () =>{
        const nextMode = mode === null? 1:parseInt(mode)+1;
        setSearchParams({mode:nextMode,detail:detail});
    };
    return(
        <div>
            <h1>소개</h1>
            <p>리액트 라우터를 사용해 보는 프로젝트입니다.</p>
            <p>detail : {detail}</p>
            <p>mode : {mode}</p>
            <button onClick={onToggleDetail}>Toggle detail</button>
            <button onClick={onIncreaseMode}>mode+1</button>
        </div>
    );
};
export default About;

useSearchParams는 배열을 반환하는데, 첫 번째 원소는 쿼리 파라미터를 조회/수정 가능한 메서드들이 담긴 객체다. get 메서드는 조회하는 역할, set메서드는 수정하는 역할을한다. 예시코드에선 get을 통해 파라미터를 파싱한 값을 변수에 할당했다.

두 번째 원소는 쿼리파라미터를 객체형태로 업데이트 할 수 있는 함수다. 예시코드의 setSearchParams에 해당된다.

쿼리파라미터 사용에 있어 주의할 점은 조회한 파라미터 값은 무조건 문자열 형태라는 것이다. onIncreaseMode함수에서 parseInt를 쓰지 않았다면 mode=1 상태에서 버튼을 누르면 mode=11 이 됐을 것이다..

지금까지 웹 브라우저가 서버에 요청을 보낼 때 URL에 추가되는 정보인 쿼리스트링에 대해 알아봤다!

중첩된 Route에 Outlet 사용하기

게시글 목록을 조회하는 컴포넌트를 추가했다. (구현은 직접 해보자) 목록 중 하나를 누르면 useParams를 통해 유동적으로 만들어진 페이지가 표시된다.

모든 게시글 페이지마다 밑에 글 목록이 나오게 하려면 어떻게 해야될까?

<div>
  <h2>게시글 {id}</h2>
  <ArticleList/>
</div>

이렇게 글 목록 컴포넌트를 따로 만드어서 모든 게시글마다 추가해주는 것도 방법이 될 수 있겠지만 이번엔 중첩된 라우트를 사용해보자.

<Routes>
  (...)
  //글목록
  <Route path="/articles" element={<Articles/>}>
    //각각의 글 
    <Route path=":id" element={<Article/>}/>
  </Route>
</Routes>

App 컴포넌트를 위 코드처럼 수정해줬다. Route안에 children 포지션 Route가 추가됐다.

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

  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;

Outlet을 사용해 게시글을 누르면 아래에 목록이 표시되도록 만들어줬다. 처음엔 직관적으로 보이지 않을 수 있는데, Articles 컴포넌트 내에서 Route의 children으로 들어간 JSX엘리먼트를 Outlet에서 ‘보여지는’ 것이다. 그래서 Outlet이 상단, 목록이 아래에 작성된 것이다. 직접 한번 작성해보면 금방 이해된다.

Outlet은 여러 페이지에서 공통적으로 보여져야 하는 요소를 보여주기에도 굉장히 유용한데, 어떤 페이지를 가도 보여주고 싶은 네비게이션 바 등을 구현하고 싶다면 Outlet을 사용해봐도 좋을 것이다.

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;

회색 바탕에 Header라고 적힌 상단바(?)를 추가해주고 Outlet으로 Route에 children으로 들어간 값들을 보여주고 있다. Outlet에 적절한 값들이 보여지려면 Route를 미리 중첩시켜놔야될 것이다.

<Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        // index === "/"
        <Route path="/about" element={<About />} />
        (...)
      </Route>    
</Routes>

이런 식으로 말이다.

Layout으로 모든 라우트 컴포넌트를 감쌌기에 어떤 페이지를 들어가도 Header가 표시된다.

useNavigate

useNavigate는 Link를 사용하지 않고 다른 페이지로 이동할 수 있게 해준다. Layout 컴포넌트를 수정한 후 결과값을 확인해보자.

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

const Layout = () =>{
    const navigate= useNavigate();
    const goBack = () =>{
        navigate(-1);
    };
    const goArticles = () =>{
        navigate('/articles');
    };
    const goHome = () =>{
        navigate('/');
    }
    return(
        <div>
            <header style={{background:'lightgray',padding:16,fontSize:24}}>
                <button onClick={goBack}>뒤로가기</button>
                <button onClick={goArticles}>게시글목록</button>
                <button onClick={goHome}>홈으로</button>
            </header>
            <main>
                <Outlet/>
            </main>
        </div>
    );
};
export default Layout;

useNavigate가 반환한 함수에 -1을 넣으면 이전 페이지로, 1을 넣으면 다음 페이지로, path값을 넣으면 해당 페이지로 이동한다.


버튼이 추가됐고, 잘 작동한다.
여기서 알아두면 좋은 옵션이 이는데, 바로 replace다. useNavigate가 반환한 함수에 {replace:boolean}형태의 객체를 추가해보자. true면 해당 페이지로 이동해도 기록에 남지 않게 된다. 오류 페이지 등에 사용하면 좋을 것 같다.

NavLink는 링크에서 사용하는 경로가 현재 라우투의 경로와 일치하는 경우 특정 스타일 또는 CSS클래스를 적용하는 컴포넌트다. NavLink의 스타일과 className은 {isActive:boolean}을 파라미터로 전달받는 함수 타입의 값을 전달한다고 하는데, 말로만 들으면 좀 어려우니 코드를 보자.

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

const Articles = () => {
  const activeStyle = {
    color:'green',
    fontSize:21,
  };
  return (
    <div>
      <Outlet />
      <ul>
        <li>
          <NavLink 
          to="/articles/1"
          style={({isActive})=>isActive?activeStyle:undefined}>
            게시글1
          </NavLink>
        </li>
        <li>
        <NavLink 
          to="/articles/2"
          style={({isActive})=>isActive?activeStyle:undefined}>
            게시글2
          </NavLink>
        </li>
        <li>
        <NavLink 
          to="/articles/3"
          style={({isActive})=>isActive?activeStyle:undefined}>
            게시글3
          </NavLink>
        </li>
      </ul>
    </div>
  );
};
export default Articles;

NavLink의 경로와 현재 라우트의 경로가 같다면 각 스타일 혹은 클래스이름에 {isActive : true}가 전달된다. 보통 삼항연산으로 원하는 값이 지정되게 만드는 것 같다.

NavLink가 articles1을 연결하고 있고, 게시글1을 눌러 현재 라우트가 articles1이 됐을 경우 isActive는 true가 된다. 미리 지정해둔 activeStyle이 적용되어 글목록의 특정 부분 색과 크기가 변했다.

NotFound 페이지 만들기

존재하지 않는 path값이 url에 입력됐을 때 NotFound페이지를 보여주도록 만들어보자. 방법은 간단하다.

<Route path="*" element={<NotFound/>/>

와일드카드 값을 path로 갖는 Route를 추가해주면, 이상한 path값이 들어왔을 때 지정해둔 컴포넌트로 연결해준다.

로컬호스트값 뒤에 아무 한글이나 막 입력해보니 잘 나온다.

어떤 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동시키고 싶을 때 Navigate를 사용한다. 가령 아직 로그인이 되지 않은 상태에서 내 정보 페이지를 들어가려 한다면 로그인 창으로 먼저 보내야 할 것이다. 빠르게 사용법만 보자..

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;

미리 로그인 값을 false로 지정해줬다. return 밖에서 미리 조건을 판단한 후 충족하지 않을 경우 Navigate를 통해 다른 페이지로 보낸다. 그냥 Link처럼 쓰면 되는 것 같다. replace를 true로 설정해둬 뒤로가기를 눌러도 돌아오지 않도록 만들어줬다.


💁 마치며

생각보다 오래 걸렸다. 한 3일 붙잡았나....
조금 낯선 개념들이 자주 나오고 하다보니 흡수하는 데 좀 걸렸지만 이제 좀 페이지다운 페이지를 만들 수 있게 된 것 같다.
교재 예시만 따라치지 말고 헷갈리는 건 항상 밑바닥부터 기억을 더듬어가며 직접 만들어보자~!

profile
잘쫌해

0개의 댓글