Gatsby 동적 라우팅(Dynamic Routing) 처리하기

출처: https://unsplash.com/photos/36b7JBzhfF4
출처: https://unsplash.com/photos/36b7JBzhfF4

웹사이트에서 라우팅(routing)은 URL 경로와 그에 대응하는 페이지 간을 연결하는 것입니다.

Gatsby는 정적 웹사이트 생성기(SSG; Static Site Generator)이기 때문에 모든 라우팅은 기본적으로 빌드 시점에 이루어지게 됩니다. 즉 빌드 시점에 이미 웹사이트의 모든 페이지들이 HTML로 렌더링되어 저장되는 방식으로 작동합니다.

Gatsby의 라우팅 시스템은 매우 간단합니다. 특히 정적인 페이지만 갖춘 간단한 웹사이트를 하나 구성하는 거라면 딱히 처리해 줄 게 없을 정도로 간단합니다. 하지만 사이트 규모가 커져 페이지 수가 많아지고 담을 콘텐츠가 늘어나면 라우팅도 복잡해 질 수 밖에 없겠죠.

이 글에서는 Gatsby의 라우팅 처리 방식에 대해 알아보고 특히 동적 라우팅(dynamic routing)을 처리하는 몇 가지 방법을 소개합니다.

Gatsby 라우팅: 3가지 방법

Gatsby에서 라우팅을 처리하는 데는 크게 다음 3가지 방법이 있습니다.

  1. src/pages 에 페이지 컴포넌트 추가
  2. gatsby-node.js 파일에서 createPages 구현
  3. File System Route API 활용

사실 3가지라고 하였지만 이게 엄밀하게 구분되는 건 아니고, 서로 섞여서 라우팅을 처리하게 된다고 보는 게 더 정확한 표현일 것 같습니다.

한편 Gatsby에서는 정적인(static) 라우팅 뿐 아니라 동적 라우팅(dynamic routing)도 지원합니다. 또한 통상적인 리액트 SPA 앱에서처럼 클라이언트측 라우팅(client-only routing)도 물론 가능하죠.

그럼 하나씩 알아 볼까요?

정적 라우팅(static routing)

우선 정적(static) 라우팅입니다. ‘정적(static)’ 이라는 표현이 정확한 표현인지는 모르겠지만, 아무튼 이 방법은 이어 소개할 동적 라우팅(dynamic routing)에 대비되는 개념으로 페이지의 URL 정보와 페이지 간에 1:1 맵핑이 되는 라우팅 방식입니다.

Gatsby는 페이지(page)가 네비게이션의 기본 단위가 되기 때문에, 라우팅 역시 페이지 단위로 처리하게 되는데요. Gatsby에서는 프로젝트의 /src/pages 디렉터리 아래에 페이지 컴포넌트를 두면 그 페이지가 자동으로 URL과 맵핑됩니다.

예를 들어, /src/pages/about.js 파일을 만들었다면, 이 파일이 URL /about/ 과 맵핑되는 식입니다.

// /src/pages/about.js

import * as React from "react"
import { Link } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"

const AboutPage = () => (
  <Layout>
    <Seo title="About" />
    <h1>Hi people</h1>
    <p>Welcome to your new Gatsby site.</p>
    <p>
      <Link to="/page-2/">Go to page 2</Link>
    </p>
  </Layout>
)

export default AboutPage

이 때 페이지 파일(/src/pages/about.js)은 빌드 시점에 자동으로 컴파일되어 HTML 파일로 저장되고 나중에 배포 후에는 지정된 URL(/about/)을 통해 접근 가능하게 됩니다. 이런 정적 라우팅은 Gatsby가 자동으로 처리해 주기 때문에 개발자가 딱히 할 일은 없습니다.

동적 라우팅(dynamic routing)

그런데 모든 라우팅을 위와 같이 직접 페이지 파일을 만드는 방식으로 처리할 수는 없습니다. 예를 들어 Gatsby로 블로그 사이트를 만든다고 해 봅시다. 블로그에는 여러 개의 포스트가 있을 건데요. 이들 포스트 하나하나를 일일이 페이지 컴포넌트로 만들어 하나씩 대응시키는 방식은 어딘가 좀 어색해 보입니다. 불편하기도 할 뿐더러 포스트 양이 늘어나면 관리하기도 어렵겠죠.

무언가 다른 방법이 있으면 좋겠습니다. 얼핏 생각하기에, 포스트는 모두 동일한 유형일 것이기에 어딘가에 템플릿(template) 같은 것을 하나 만들어 두고, 포스트 내용을 그 템플릿에 채우는 식으로 처리하면 좋지 않을까요?

하지만, 앞서 잠깐 언급한 것처럼, Gatsby에서 모든 파일은 빌드 시점에 컴파일되어 저장됩니다. 따라서 각각의 블로그 포스트 URL을 하나의 페이지 컴포넌트와 맵핑시키는 라우팅 방식이 필요해 보이는데요. 이걸 ‘동적 라우팅(dynamic routing)’ 이라 부르겠습니다.

예를 들어 블로그 상세 페이지의 URL을 /posts/:id 로 했을 때, 이 URL과 페이지 컴포넌트를 맵핑시키려면 어떻게 하면 될까요? 혹은 식별자로 id 값 대신 slug 값을 사용해도 마찬가지입니다.

gstsby-node.js 사용하기

Gatsby에서 동적 라우팅을 처리하는 전통적인 방법은 gatsby-node.js 파일에서 createPages 함수를 구현하여 필요한 페이지들을 직접 렌더링하는 방법입니다.

아래는 /posts/:slug 경로를 /src/templates/post.js 에 담긴 페이지 컴포넌트 템플릿을 사용해 렌더링하는 방식으로 페이지를 생성하도록 해주는 코드입니다.

// gatsby-node.js

const data = [
  {id: 1, slug: "first", title: "First Item" },
  {id: 2, slug: "second", title: "Second Item" },
  {id: 3, slug: "third", title: "Third Item" },
]

exports.createPages = ({ actions }) => {
  const { createPage } = actions
  data.forEach(node => {
    createPage({
      path: `/posts/${node.slug}`,
      component: require.resolve(`./src/templates/post.js`),
      context: { node: node }
    })
  })
}

여기서는 설명을 간단히 하기 위해 편의상 데모 데이터를 사용했지만, 실제로라면 GraphQL로 리모트 API를 호출하거나 로컬 파일 시스템에 있는 마크다운(markdown) 파일을 불러와서 처리하게 될 것입니다. 어쨌거나 createPage 함수로 페이지 경로(path)와 대응하는 페이지 컴포넌트를 직접 생성하는 점에서, 원리는 똑같습니다.

File System Route API 사용하기

한편 Gatsby에서 제공하는 File System Route API를 이용하면 위 작업을 좀더 간단하게 처리할 수도 있습니다. 이번엔, 앞서처럼 gatsby-node.js 파일에서 createPages 함수를 구현하는 대신, 파일시스템을 관례에 맞춰 구성해 주면 됩니다.

예를 들어 앞의 예제를 이 방식으로 구현하려면, /src/pages/ 디렉터리 아래에 중괄호를 사용하여 {Post.slug}.js 파일을 하나 만들어 주기만 하면 됩니다. 그리고 이 파일 속에는 앞서 그랬던 것처럼 페이지 컴포넌트를 구현해 주면 되겠죠.

다만 이 경우, Gatsby가 빌드 시점에 파일명을 해석하여 중괄호 속의 값을 파싱하여 그에 맞는 GraphQL을 자동으로 호출하기 때문에, 미리 GraphQL을 준비해 두어야 합니다.

예를 들어 페이지 파일을 {Post.slug}.js 로 주었다면, Gatsby는 빌드 시점에 자동으로 allPost 라는 GraphQL 쿼리를 호출하여 그 결과값 속에 들어 있는 slug 필드값을 취하게 될 것입니다. 때문에 그에 맞춰 미리 GraphQL 쿼리를 설계할 필요가 있겠죠?

이런 라우터를 Gatsby에서는 ‘콜렉션 라우터(Collection routes)’ 라고 부르는데요.

예를 들어, GraphQL로 allItem 데이터 쿼리를 호출하는 경우라면, {Item.slug}.js 파일의 내용을 다음과 같이 만들면 될 것입니다.

import React from "react"
import { graphql } from "gatsby"

const Item = ({ data }) => {
  const item = data.item
  return (
    <h1>Item: {item.title}</h1>
  )
}

export default Item

export const query = graphql`
  query($slug: String) {
    item(slug: { eq: $slug }) {
      title
    }
  }
`

이렇게 File System Route API를 이용하면 별도로 gatsby-node.js 파일에서 createPages를 구현할 필요도 없고 페이지 템플릿 파일도 따로 만들 필요가 없기 때문에 편리합니다. 둘다 가능한 방법이라면 좀더 간단한 방법이 낫겠죠!

클라이언트측 라우팅

지금까지 소개한 방식은 SSG 라우팅이었습니다. 즉, 빌드 시점에 서버에서 렌더링이 이루어지기 때문에 브라우저에 자바스크립트가 꺼져 있어도 페이지가 렌더링되죠.

한편 Gatsby는 리액트 기반이기 때문에 리액트의 라우팅 방식도 그대로 활용할 수 있습니다. 통상적인 SPA(Single Page App)에서의 라우팅이죠. Gastby에서는 내부적으로 React Router 코어 컴포넌트인 Reach/Router를 사용합니다.

출처: https://www.gatsbyjs.com/docs/how-to/routing/client-only-routes-and-user-authentication/
출처: https://www.gatsbyjs.com/docs/how-to/routing/client-only-routes-and-user-authentication/

위 그림을 보면, Home과 App 페이지는 정적 라우팅 방식으로 처리하고 있지만, App 아래의 Profile과 Detail 페이지는 클라이언트측 라우팅을 사용하고 있습니다.

예를 들어, 라우팅을 다음과 같이 처리하려 한다고 해 보죠.

  1. /app → App 페이지 컴포넌트 렌더링 (Static 페이지 방식)
  2. /app/prifle → 사용자 프로필 페이지 (클라이언트측 라우팅 방식)
  3. /app/details → 상세 페이지 (클라이언트측 라우팅 방식)

어떻게 처리할까요?

우선, /src/pages/ 디렉터리 아래에 [...].js 파일을 하나 생성합니다. 앞서 파일명에 중괄호를 둔 것과 달리 이번에는 대괄호([])를 사용한 점에 유의하세요.

그런 다음 [...].js 파일을 열어 Gatsby @reach/router 라이브러리에 포함된 <Router> 컴포넌트를 사용하여 다음과 같은 식으로 라우팅을 처리하면 됩니다.

import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/Layout"
import Profile from "../components/Profile"
import Details from "../components/Details"
import Default from "../components/Default"

const App = () => {
  return (
    <Layout>
      <Router basepath="/app">
        <Profile path="/profile" />
        <Details path="/details" />
        <Default path="/" />
      </Router>
    </Layout>
  )
}

export default App

이 방법 역시 앞서 소개한 동적 라우팅 방법과 같습니다. 다만, 앞서의 동적 라우팅은 빌드 타임에서 페이지가 자동 생성되는 SSG 방식인 반면, 이 방법은 브라우저에서만 작동하는 클라이언트측(client-only) 라우팅이라는 점만 다를 뿐이죠.

또한 파일명을 이용하는 방식 대신, gatsby-node.js 파일에서 Gatsby Node API에서 제공하는 createPage 이벤트를 구현하는 방식으로도 똑같은 처리가 가능합니다. (이 때 <Router> 컴포넌트는 /src/pages/app.js 파일 내에서 처리해 주면 되구요!)

exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  // page.matchPath is a special key that's used for matching pages only on the client.
  if (page.path.match(/^\/app/)) {
    page.matchPath = `/app/*`

    // Update the page.
    createPage(page)
  }
}

클라이언트측 라우팅 처리는 통상적인 리액트 라우팅 방식을 따르기 때문에 자세한 설명은 여기서 생략합니다.

지금까지 Gatsby에서 사용할 수 있는 다양한 라우팅 처리 방법들을 알아 보았는데요. 정적 라우팅과 동적 라우팅, 그리고 클라이언트측 라우팅을 적절하게 혼합하여 사용하면 좀더 효과적으로 웹사이트나 웹앱을 구축할 수 있을 것입니다.

참고자료

Contact

유스풀패러다임
03159 서울특별시 종로구 종로 33
그랑서울타워1, 7층

+82 02 720 5059
Contact Us

Connect

Links

유스풀패러다임의 다른 사이트들도 만나 보세요.

Usefulparadigm blog
WordPress 가이드
Landing Jekyll
Hello Gatsby