[리액트공부] 13. SPA

kkado·2022년 8월 10일
0

리다기

목록 보기
14/16
post-thumbnail

SPA : Single Page Application

한 개의 페이지로 이루어진 애플리케이션이라는 의미이다.

기존의 웹 페이지는 여러 페이지로 구성되어 있었고, 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고, 받아온 리소스를 해석하여 화면에 보여주는 과정을 거쳤다. 즉, 사용자에게 보여지는 화면을 서버 측에서 준비하였다.

그러나 요즘에는 웹이 담고 있는 정보들이 방대하여 서버 측에서 모든 뷰를 준비한다면 성능적으로 문제가 생길 수 있다. 속도와 트래픽 측면에서 일정 수준 최적화는 가능하겠지만 유저와 상호작용하는 애플리케이션의 경우에는 부적절하다.

따라서 리액트와 같은 라이브러리 혹은 프레임워크가 도입되어 뷰 렌더링을 사용자의 브라우저가 담당하며 사용자와 상호작용하면서 필요한 부분만 업데이트해주는 것이 가능하게 되었다.

싱글 페이지 애플리케이션이라고 해서 한 화면만 있는 것은 아니다. 그 안에서 자바스크립트 코드로써 컴포넌트들을 렌더링하기를 반복하여 여러 화면을 보여줄 수 있다.

라우팅

다른 주소마다 다른 화면을 보여주는 것을 라우팅이라고 한다.
리액트 자체적으로 라우팅을 해 주는 라이브러리는 없어서, 다른 라이브러리를 사용하는데 많이들 사용하는 것이 react-router, reach-router, next.js 등이 있다. 여기서는 react-router를 다룬다.

SPA의 단점은 앱의 규모가 커지면 자바스크립트 코드가 너무 방대해진다는 것이다. 나중에 배울 코드 스플리팅(code spliting)을 통해 해결할 수 있다. 또다른 문제는 자바스크립트를 이용해 라우팅하는 방식은 일반 웹 크롤러로 정보를 수집할 수 없다는 단점이 있다. 이것 역시 나중에 배우는 서버 사이드 렌더링(server-side rendering)을 통해 해결 가능하다.

프로젝트에 라우터 적용

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

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

export default About;

먼저 간단한 페이지 두개를 Home.js와 About.js라는 이름으로 만들고 App.js 파일로 와서 react-router-dom에 내장되어 있는 BrowserRouter 컴포넌트를 이용해서 감싸면 된다. (편의상 'Router' 이라는 이름으로 import 한다.) 그리고 <Routes> 태그 안에 각각의 라우팅할 컴포넌트들을 <Route> 컴포넌트로 감싸면 된다.

책에서는 component 속성을 사용한다던지 exact 속성을 사용하는데, 출판 후에 업데이트된 react-router-dom v6 버전에서는 component 대신 element 속성을 사용하고 exact 속성은 사용하지 않는 등의 조금의 차이가 있다.

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;

그리고 path 속성에다가 주소를 넣어주고, element 속성에다가 해당 주소로 이동했을 때 렌더링할 컴포넌트 정보를 넣어준다. 이제 애플리케이션을 실행해 보면

주소에 따라 각기 다른 컴포넌트들이 렌더링되는 것을 볼 수 있다.

Link

클릭하면 다른 주소로 이동시켜 주는 컴포넌트이다. 일반 웹 애플리케이션의 경우 <a> 태그를 이용해 페이지 전환을 하는데, a 태그는 페이지 전환 과정에서 페이지를 새로 불러오기 때문에 기존에 갖고 있던 상태들을 모두 초기화한다. 즉 렌더링된 컴포넌트들이 모두 사라지고 처음부터 렌더링하게 된다.

Link 컴포넌트를 사용하면 페이지를 새롭게 불러오지 않고 HTML5 History API를 이용해서 애플리케이션을 유지한 채 페이지의 이동을 가능하게 해 준다.

        <ul>
          <li>
            <Link to="/"></Link>
          </li>
          <li>
            <Link to="/about">소개</Link>
          </li>
        </ul>

사용하는 문법은 간단하다. to 속성으로 어디로 갈 것인지 주소를 작성해 주면 된다.

이런 식으로 렌더링되었고, 위의 홈이나 소개를 누르면 Home, About 컴포넌트를 렌더링한다.

<Route path={["/about", "/info"]} component={About} />

이렇게 작성하면 여러 path가 하나의 Route를 가리키게끔 할 수 있었지만 v6으로 업그레이드되고 나서는 마땅한 방법이 없는 듯 하다.

<Route path="/about" element={<About />} />
<Route path="/info" element={<About />} />

이런 식으로, 다소 불편하지만 두 번 사용해야 할 것 같다.

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

그리고 마지막 부분에 모든(*) 주소를 렌더링하는 NotFound 컴포넌트를 만들어 주면 마치 switch문의 default 처럼 예외처리가 가능하다.


URL 파라미터, 쿼리

https://www.google.com/search?q=%EC%9D%B4%EC%8A%B9%EA%B8%B0
https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%8A%B9%EA%B8%B0

웹 검색을 할 때 다음과 같은 주소를 본 적이 있을 것이다.

위의 URL은 물음표 ? 뒤에 사용자가 전달해준 값의 정보가 들어가고, 아래의 URL은 슬래시 / 뒤에 어떤 값이 들어간다. 위의 방식을 쿼리, 아래를 파라미터 라고 한다. 둘 다 URL에 어떤 유동적인 값을 전달해 주는 역할을 하지만 그 쓰임새가 조금 다르다.

일반적으로 파라미터는 특정 id나 이름을 가지고 조회하는 등의 작업을 할 때 사용하고 쿼리는 키워드나 필요 옵션 등을 전달해 줄 때 사용한다.

URL 파라미터

<Route path="/about/:userInfo" element={<About />} />

위와 같이 path 뒤에 /:파라미터명 을 넣어주면 된다.

const userInfo ="이승기";
<Link to={`/about/${userInfo}`}>소개</Link>
// 또는 그냥 이렇게
<Link to="/profiles/gildong">gildong의 프로필</Link>

그리고 위와 같은 링크를 만들어 주면 About 컴포넌트로 연결이 된다.

파라미터 가져와서 사용

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

const { userInfo } = useParams();
console.log(userInfo);

About.js 파일에서 위와 같이 useParams 을 import 하고, useParams(); 를 사용해 주면 파라미터를 사용할 수 있다. 여기서 파라미터 이름은 위의 path에 지정해준 이름과 동일해야 한다.

URL 쿼리

쿼리는 location 객체에 있는 search 값에서 조회할 수 있다.
URL 쿼리 값으로 detail=true 라는 정보를 전해주었다고 가정하자.

location 객체를 살펴보면

우리가 입력해준 쿼리 스트링이 search 에 저장되어 있는 것을 볼 수 있다.
이것을 객체로 변환하기 위해서는 qs 라는 라이브러리를 사용한다.

qs 라이브러리를 설치 후 다음과 같이 parse 함수와 ignoreQueryPrefix 기능을 사용해서 쿼리스트링을 객체로 변환할 수 있다.

import QueryString from "qs";

  const location = useLocation();
  const queryData = QueryString.parse(location.search, {
    ignoreQueryPrefix: true,
  });

그리고 queryData를 console.log 해보면

위와 같이 객체로 만들어진 것을 볼 수 있다.

또는 리액트 라우터 v6부터 사용가능해진 useSearchParams 라는 Hook을 이용해서 쿼리스트링을 쉽게 다룰 수 있다.

const [searchParams, setSearchParams] = useSearchParams();
const detail = searchParams.get("detail");
const mode = searchParams.get("mode");

마치 useState를 사용하듯이 선언해주고, 이후 get 함수를 이용해서 쿼리스트링에서 값을 쉽게 추출해낼 수 있다. 만약 해당 이름의 쿼리스트링이 존재하지 않는다면 null이 조회된다.

여기서 주의할 점이 있는데 두 가지 방법 모두 쿼리스트링을 객체로 파싱하는 과정에서 결과 값이 모두 문자열이 된다는 것이다. 만약 우리가 쿼리를 ?number=1&query=true 등의 숫자나 불리언 값을 전달해주고 싶어도, "1", "true" 의 문자열이 되기 때문에 === "true" 등의 비교를 통해서 값을 사용해야 하고, 숫자를 사용하기 위해서는 parseInt를 통해 숫자로 변환하는 과정을 거쳐야 한다.

setSearchParams({ mode, detail: detail === "true" ? false : true });

그리고 쿼리스트링을 변경하는 것도 가능하다. 위와 같이 setSearchParams 함수를 사용해주고 안에 바꿀 searchParams의 정보를 넣어주면 된다. 위의 코드는 detail이 true면 false로, false면 true로 toggle 해주는 기능이고, 나머지 스트링인 mode는 그대로 값을 사용한다.

중첩 라우팅

라우팅을 중첩해서 사용할 수도 있다. 먼저 다음과 같은 컴포넌트들을 만든다.
src/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;

src/pages/Article.js

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

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

export default Article;

그리고 App.jsHome.js를 다음과 같이 수정한다.

import { Route, Routes } from 'react-router-dom';
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 path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/profiles/:username" element={<Profile />} />
      <Route path="/articles" element={<Articles />} />
      <Route path="/articles/:id" element={<Article />} />
    </Routes>
  );
};

export default App;
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>
        <li>
          <Link to="/articles">게시글 목록</Link>
        </li>
      </ul>
    </div>
  );
};

export default Home;

각 게시글 링크를 클릭했을 때 위와 같이 잘 나타나게 된다.

여기서, 만약 각 게시글의 하단에 또다시 게시글 목록을 표시하고 싶다면, 다음과 같이 ArticleList 컴포넌트를 따로 만들어서 각 페이지 컴포넌트에서 사용을 해야할 것이다.

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

만약 중첩 라우트를 사용한다면 보다 나은 방식으로 구현할 수 있다.
App.js를 다음과 같이 수정하자.

import { Route, Routes } from 'react-router-dom';
import About from './pages/About';
import Article from './pages/Article';
import Articles from './pages/Articles';
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 />} />
      <Route path="/articles" element={<Articles />}>
        <Route path=":id" element={<Article />} />
      </Route>
    </Routes>
  );
};

export default App;

보면 Article 컴포넌트가 Articles 컴포넌트 안에 감싸져 있는 것을 볼 수 있다.

그리고 리액트 라우터에서 제공하는 Outlet 이라는 컴포넌트를 사용한다. 이 컴포넌트는 Route의 children으로 들어가는 엘리먼트들을 그대로 보여준다. 위와 같은 경우에는 다음과 내용이 Outlet 컴포넌트 위치에 렌더링된다.

<Route path=":id" element={<Article />} />

그리고 Articles 컴포넌트를 수정해 주면 :

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;


Outlet의 위치, 즉 리스트의 위쪽에 Article이 보여지고 있다.

공통 레이아웃 컴포넌트

이 방법은 페이지들 사이에서 공통적으로 보여져야 하는 레이아웃이 있을 때 유용하게 사용 가능하다.
예를 들어 여러 페이지들 상단에서 헤더를 보여주어야 하는 상황에서, Header 컴포넌트를 따로 만들어 두고 모든 컴포넌트들에서 Header 컴포넌트를 재사용하는 방법이 있겠지만 비효율적이다.

이럴 때 Outlet과 중첩 라우트를 이용하면 쉽게 구현할 수 있는데, 먼저 공통 레이아웃을 위한 Layout 컴포넌트를 만든다.
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;

헤더를 간단히 구현하고 나머지 main에 들어가는 부분을 Outlet으로 처리하였으므로, 전체 컴포넌트를 Layout으로 감싸면 될 것이라고 유추가 가능하다.

App.js도 다음과 같이 수정하자.

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

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;

이렇게 구현하면 Home, About, Profile 컴포넌트 위에 헤더가 표시될 것이다.


위에 헤더가 잘 나타나고 있는 것을 볼 수 있다.

index props

Route 컴포넌트에는 index라는 props가 있다. path="/"와 동일한 의미를 가진다. path="/"로 사용해도 되지만, 이 페이지가 인덱스 페이지라고 명시적으로 나타낼 수 있다.

<Route index element={<Home />} />

리액트 라우터 부가기능

리액트 라우터는 라우팅 작업을 할 때 사용하면 유용한 API들을 여럿 제공한다.

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;

헤더에 위와 같은 버튼 2개가 나타났고, 각각 버튼을 클릭하면 페이지가 이동된다.

navigate 함수를 사용할 때 파라미터가 숫자라면 앞으로 가거나 뒤로 간다. 현재 페이지를 기준으로 -1, -2를 하면 한 번, 두 번 뒤로 가고, +1을 하면 한 번 앞으로 간다. (물론 뒤로가기 한 상태여야 함)

파라미터가 문자라면 해당 페이지로 이동한다.

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

즉 예를 들어 게시글 목록 버튼을 클릭하여 /articles 로 이동하고 나서 뒤로가기 버튼을 눌러 이전 페이지로 이동하면 원래 있던 페이지가 기록에 남지 않기 때문에 그 이전 페이지로 이동하게 된다.

NavLink는 Link에서 사용하는 경로가 현재 경로와 일치하는 경우 특정 스타일, CSS 클래스를 적용하는 컴포넌트이다.

이 컴포넌트를 사용할 때 style 또는 className을 설정할 때 { isActive: boolean} 을 파라미터로 전달 받는 함수를 전달한다.

예시를 확인해보자.

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

<NavLink 
  className={({isActive}) => isActive ? 'active' : undefined} 
/>

isActive와 화살표 함수를 사용하고 있다.

이를 활용하여 Articles 컴포넌트를 수정하면 :

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;

현재 게시글이 위치하는 '게시글 1' 의 isActive가 true가 되면서 스타일이 변경된 것을 알 수 있다. 여기서 반복되는 NavLink 부분이 번거롭기 때문에 따로 빼내어 리팩토링 해 줄수도 있다.

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;

코드의 중복이 많이 사라진 모습을 확인할 수 있다.

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의 Routes 맨 하단에 다음을 추가한다.

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

*은 와일드카드 문자로, 모든 텍스트에 매칭된다. 마치 if문의 else문 처럼 상단의 라우트들의 규칙을 확인했을 때 일치하는 라우터가 없다면 이 라우트가 표시되게 된다.

Navigate 컴포넌트는 컴포넌트를 화면에 보여주는 순간 다른 페이지로 이동을 하고 싶을 때 사용하는 컴포넌트이다. 즉 리다이렉션 하고 싶을 때 사용한다.

예를 들어 사용자가 로그인하지 않은 채로 로그인이 필요한 화면에 접속하려 한다면 로그인 페이지로 이동시켜주면 좋을 것이다.

두 가지 컴포넌트를 만든다.
Login.js

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

export default Login;

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 변수를 수정하는 부분을 구현하지 않았지만 실제로는 로그인 여부에 따라 true/false가 바뀐다고 가정한다.

만약 이 값이 false라면 Navigate 컴포넌트를 통해 /login 으로 이동한다.

그리고 마지막으로 App.js를 수정한다.

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

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>
      <Route path="/login" element={<Login />} />
      <Route path="/mypage" element={<MyPage />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
};

export default App;

정리

리액트 라우터를 사용하여 주소 경로에 따라 다른 페이지를 보여주고, 페이지에 파라미터를 전달하고, 페이지를 이동하는 방법 등을 배웠는데, 책에는 버전5 기준으로 설명하고 있는데 내가 사용하는 버전은 v6이라서 문법이 조금 바뀐 점 때문에 고생을 좀 했던 것 같다.

구글링 하다가 이 책을 작성하신 김민준님 velog에서 리액트라우터 버전6으로 설명해주신 글이 있길래 참고하였다. (링크 : https://velog.io/@velopert/react-router-v6-tutorial)

profile
베이비 게임 개발자

0개의 댓글