Dev/React

Next.js 라우팅

rryu09 2024. 5. 8. 13:26

잘 되어있는 React를 사용하고 있는데, 왜 Next.js가 필요할까요??

오늘 다루어 볼 주제는 Next.js의 라우팅인데, 먼저 Next.js가 무엇인지 알아봅시다.

Next.js 란?

Next.js는 Vercel에서 리액트를 위해 만든 오픈소스 자바스크립트 웹 프레임워크로, 

서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG), 증분 정적 재생성(ISR)과 같은 기능들을 제공합니다.

 

리액트는 클라이언트 사이드에서만 작동하기 때문에, 

첫 화면을 제대로 표시하기 위해 실행 초기에 성능 부담이 생기기도 하고 SEO 효과를 거의 볼 수 없었습니다.

이 문제를 해결하기 위해 웹 어플리케이션을 서버에서 미리 렌더링해두는 방법을 연구하기 시작했고,

서버 사이드 렌더링을 통해 리액트 앱을 HTML 페이지로 미리 렌더링해둔 후 브라우저가 이를 다운로드해

즉각적으로 화면에 표시한 후, 클라이언트에서 자바스크립트 번들을 다 다운 받으면 유저가 웹과 상호작용할 수 있도록 했습니다.

Next.js 왜 쓸까??

Next.js 를 사용하는 가장 큰 이유로는 SEO를 위한 SSR이 가능하기 때문이라는 의견이 많습니다.

이외에도 Next.js는

  • 직관적인 페이지 기반 라우팅 시스템
  • optimal prefetching을 사용한 client-side navigation
  • 빠른 로딩을 위한 code splitting
  • built-in-CSS, Image Optimization, fast refresh, API routes ...

등의 기능을 지원해 개발자가 서버 사이드와 클라이언트 사이드에서 고려해야 할 사항들을 줄여주어 더욱 어플리케이션 개발에 집중할 수 있도록 돕습니다.

 


 

기존 react-router-dom을 이용한 라우팅

먼저 기존 react-router-dom을 이용한 라우팅 방식을 살펴볼까요?

import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import Home from "./pages/Home";
import Blogs from "./pages/Blogs";
import Contact from "./pages/Contact";
import NoPage from "./pages/NoPage";


export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="blogs" element={<Blogs />} />
          <Route path="contact" element={<Contact />} />
          <Route path="*" element={<NoPage />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

 

 

react-router-dom을 이용해 라우팅을 적용해 온 분들이라면 익숙한 형식일텐데요,

page 폴더 안에 페이지들을 넣어 두고, Routes 안에 path 와 element 를 props 로 가진 Route 컴포넌트를 두는 형식입니다.

 


 

그렇다면 Next.js의 라우팅은 어떤 형식일까요?

 

next.js 에서는 현재 Page 라우터와 App 라우터 두가지를 제공하고 있습니다.

무엇이 다른지 알아보도록 할까요?

Page 라우터

Pages and Layouts

페이지 라우터는 파일 시스템 기반으로 작동합니다!

pages 폴더에 .js, .jsx, .ts, .tsx 형식의 파일이 추가되면 자동으로 Route로 사용 가능합니다.

  • pages/index.js  /
  • pages/blog/index.js  /blog

과 같은 형식으로 접근할 수 있어요.

 

왼쪽이 기본적인 폴더 구조고,오른쪽은 pages 폴더 하위에 blog 폴더를 만들고 Index.js 를 추가한 모습입니다.

 

 

다음과 같이 /blog 로 접근하면 자동으로 blog 폴더 내의 Index.js 의 내용이 나타나는 걸 확인할 수 있어요!

react-router-dom 을 이용할 때처럼 직접 라우트를 지정해 줄 필요 없이, 폴더 구조에 따라 주소가 지정되니 더 직관적으로 확인할 수 있겠네요.

 

이런 폴더 구조가 있다면,

  • /blog
  • /board
  • /board/chart

주소에 페이지들이 존재한다는 걸 쉽게 파악할 수 있겠죠!

여기에서 board 폴더 안에 chart 폴더가 들어있는 걸 확인할 수 있는데, 이런 방식으로 nested routes 를 구현할 수 있습니다.

 

 


Dynamic Routes

⬇️ react-router-dom 의 dynamic routes ⬇️

더보기

react-router-dom 에서 dynamic routes 를 아래 방식으로 작성하셨던 것 기억이 나시나요?

<Route path="/users/:id" component={User} />

 

:id 를 이용해 값을 넘겨주고,

const { id } = useParams();

 

useParams 를 이용해 받아오는 방법이었죠!

폴더 구조를 이용하는 page 라우터를 이용할 때는 어떻게 dynamic routes를 구현할 수 있을까요?

 

// pages/blog/[id].js

import { useRouter } from "next/router";
import React from "react";

const index = () => {
  const router = useRouter();
  return <p>{router.query.id}번 블로그입니다!</p>;
};

export default index;

 

위와 같이 [] 를 이용해 Dynamic Segment를 만들 수 있고,

next/router 의 useRouter 로 세그먼트의 값을 받아 올 수 있습니다.

 

  • http://localhost:3000/blog/1
  • http://localhost:3000/blog/2
  • http://localhost:3000/blog/3

으로 접근하면 다음과 같은 화면을 볼 수 있습니다.

 

 

[...segmentName]

과 같이 Catch-all Segments는 /blog/1 뿐만 아니라

/blog/1/section , /blog/1/section/part 과 같이 이후의 segments도 모두 받습니다.

 

[[...segmentName]]

Optional Catch-all Segments 도 있는데,

Catch-all Segments 와 거의 유사하지만 파라미터가 없는 route 도 매치될 수 있다는 차이가 있습니다.

(e.g pages/blog/[id].js 가 있다면 /blog 도 매치됩니다. { id: undefined })

 

 


 

Layout

모든 페이지에 아래와 같은 Navbar와 Footer가 들어가야 한다면 어떻게 할 수 있을까요?

모든 페이지에 해당 컴포넌트를 불러와 쓰기에는 효율적이지 않다는 생각이 들었다면, Layout을 사용해보면 좋을 것 같네요.

 

 

왼: components/Layout.js / 오: pages/_app.js

위와 같이 components 폴더 내에서 Layout을 정의해주고,

_app.js 에서 Layout 컴포넌트로 감싸주면 모든 페이지에서 Layout 의 UI를  사용할 수 있어요.

 

모든 페이지에 Layout이 잘 적용됐네요!

 

 

Per-Page Layouts

 

모든 페이지가 아니라 특정한 페이지에만 레이아웃을 적용하고 싶다면 per-page Layouts 를 사용하면 됩니다.

id값을 가진 페이지에만 레이아웃을 적용해볼게요.

 

// pages/blog/[id].js

const index = () => {
  const router = useRouter();
  return (
      <p>{router.query.id}번 블로그입니다!</p>
  );
};

index.getLayout = function getLayout(page) {
  return <Layout>{page}</Layout>;
};

export default index;
// pages/_app.js

export default function App({ Component, pageProps }) {
  const getLayout = Component.getLayout ?? ((page) => page);
  return getLayout(<Component {...pageProps} />);
}

위의 pages/blog/[id].js 처럼, 각 페이지에서 `getLayout` 을 따로 정의할 수 있습니다.

 

app.js 에서 `Component.getLayout` 이 있다면 그 값을 `getLayout`에 할당하고,

없다면 `((page) => page)` 와 같이 페이지를 그대로 반환하는 함수를 할당합니다.

 

`getLayout`을 정의했다면 그 레이아웃을, 아니라면 그 페이지를 그대로 보여줍니다.

 

 

/blog 페이지에는 레이아웃이 적용되지 않았지만, /blog/[id] 페이지에는 레이아웃이 적용되었습니다.

 

우리는 페이지를 이동하더라도 인풋 값이나 스크롤 위치와 같은 상태들이 유지되기를 원하는데요,

이런 레이아웃 패턴을 사용하면 상태 유지에 도움을 줄 수 있습니다!

리액트 컴포넌트 트리는 페이지가 전환되어도 유지되기 때문에 상태 유지가 가능합니다.

컴포넌트 트리를 이용해 리액트가 상태를 유지하기 위해 어떤 요소가 변경되었는지 파악할 수 있다고 해요.

 

 

 


Link

그렇다면 페이지 간 이동은 어떻게 구현해야 할까요?

<Link /> 컴포넌트에 대해 알아봅시다.

 

<Link href="/about">About Us</Link>

 

  • / -> pages/index.js
  • /about -> pages/about.js
  • /blog/hello-world -> pages/blog/[slug].js

href 속성에 주소를 적어주면 됩니다.

 

<Link href={`/blog/${post.slug}`}>{post.title}</Link>

 

dynamic route 를 위해서는 위와 같이 작성하면 됩니다.

 

 <Link
     href={{
       pathname: '/blog/[slug]',
       query: { slug: post.slug },
     }}
 >
     {post.title}
</Link>

 

URL Object를 사용하는 방법도 있는데요, 

pathname 에 pages 폴더에 있는 페이지의 이름을 적어주고,

query 에 다이나믹 세그먼트를 담은 객체를 넣어주면 됩니다.

 

Static Generation 을 사용하는 페이지 뷰포트에 있는 모든 <Link /> 는 기본적으로 prefetch 됩니다!

더보기

`getStaticProps` 라는 함수를 페이지에서 export 하면 `getStaticProps` 함수에서 리턴된 props를 이용해 build time에서 페이지를 pre-render 하는 방식인데요,

유저 요청과 상관 없이 빌드 타임에 페이지에 필요한 데이터, headless CMS에서 오는 데이터,

SEO 최적화를 위해서 pre-rendering 이 필수적인 경우, 아주 빨라야 하는 경우 좋다고 합니다. 

 

`getStaticProps` 는 HTML과 JSON 파일을 생성하고, 성능을 위해서 CDN에 캐싱될 수 있습니다.

 

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props

 

Data Fetching: getStaticProps | Next.js

Fetch data and generate static pages with `getStaticProps`. Learn more about this API for data fetching in Next.js.

nextjs.org

 

 

 

useRouter 를 이용한 라우팅

`next/router` 를 이용하면 Link 를 사용하지 않고 client-side 라우팅을 할 수 있습니다.

useRouter 와 withRouter 는 router object 를 반환하는데, 

더보기

router object

pathname: String - The path for current route file that comes after /pages. Therefore, basePath, locale and trailing slash (trailingSlash: true) are not included.
query: Object - The query string parsed to an object, including dynamic route parameters. It will be an empty object during prerendering if the page doesn't use Server-side Rendering. Defaults to {}
asPath: String - The path as shown in the browser including the search params and respecting the trailingSlash configuration. basePath and locale are not included.
isFallback: boolean - Whether the current page is in fallback mode.
basePath: String - The active basePath (if enabled).
locale: String - The active locale (if enabled).
locales: String[] - All supported locales (if enabled).
defaultLocale: String - The current default locale (if enabled).
domainLocales: Array<{domain, defaultLocale, locales}> - Any configured domain locales.
isReady: boolean - Whether the router fields are updated client-side and ready for use. Should only be used inside of useEffect methods and not for conditionally rendering on the server. See related docs for use case with automatically statically optimized pages
isPreview: boolean - Whether the application is currently in preview mode.

router.push(pathname)를 이용해 라우팅을 할 수 있어요.

 


 

Link  vs  router.push()  vs <a> tag

Next.js에서 라우팅을 할 수 있는 방법이 이렇게 있는데, 과연 어떤 것을 사용해야 할까요??

 

router.push()

window.location과 비슷하게 동작합니다.<a> 태그를 만들지 않아서 크롤러에게 감지되지 않기에 SEO에 좋지 않습니다.

 

<Link />

<a> 태그를 생성합니다.

크롤러에게 감지될 수 있고, 페이지 새로고침 없이 끝까지 navigate 할 수 있어서 Single Page App으로 동작할 수 있습니다.

 

<a>

next/link 의 <Link> 를 사용하지 않고 a 태그를 사용하면
사용자가 새로운 페이지의 URL로 이동하는 표준 하이퍼링크가 생성되며,
full reload가 일어납니다.

 

Next.js는 SSR을 통한 SEO가 가능한 것이 특징인 만큼, 웹사이트 전반에서 <Link> 를 사용한 라우팅을 진행하고

유저를 직접 이동시켜야 하는 경우에만 router.push() 를 사용하는 것이 좋겠네요.

 


 

App 라우터

Next.js 버전 13에서 새로 소개되었습니다.

이전 pages 에서 작동했던 방식처럼 app 폴더에서 작동하고, page 폴더와 함께 사용될 수 있습니다.

 

!!App router는 pages router보다 우선됩니다!!

app 과 pages 디렉토리 라우팅이 섞였을 때 같은 URL path로 라우팅해선 안 되고, 
이는 충돌 방지를 위해 빌드 타임 에러를 발생시킵니다.

 

 

app 폴더 안에 있는 컴포넌트들은 기본적으로 React Server Components 입니다.

"use client" 를 통해서 client component 로 사용할 수 있습니다.

File Conventions

layout 세그먼트와 children 위한 공유 UI
page routed의 unique한 UI
loading 세그먼트와 children 위한 로딩 UI
not-found 세그먼트와 children 위한 not-found UI
error 세그먼트와 children 위한 에러 UI
global-error 전역 에러 UI
route Server-side API 엔드포인트
template Specialized re-rendered Layout UI
default Fallback UI for Parallel Routes

Component  Hierarchy

컴포넌트들은 위와 같은 위계를 가집니다.

 

nested route 의 경우, 위 이미지처럼 위계가 부모 segment 안에 중첩되는 방식으로 표현됩니다.

 


 

Layouts

// app/layout.js

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <main>{children}</main>
      </body>
    </html>
  )
}

위 코드는 루트 레이아웃입니다!

필수적인 파일이고, 서버에서 오는 초기 HTML 파일을 수정할 수 있도록 html 과 body 태그를 꼭 포함해야 해요. 

page route 에서의 _app.js, _document.js 파일을 대체한 파일입니다.

 

 

레이아웃은 state 를 보존하고 interactive하며 재렌더링되지 않습니다.

 

page router 에서 만들어본 페이지와 같이 레이아웃을 적용하려면 다음과 같이 할 수 있습니다.

Page router에서는 레이아웃 컴포넌트를 따로 만들어 _App.js 에 적용해줬던 반면, App router 를 사용하면

page.js 가 위치한 경로에 layout.js를 위치시키면 레이아웃이 페이지를 wrap 합니다.

 


 

useRouter

Pages Router에서는 `next/router`로부터 useRouter를 가져와서 사용했지만,

App Router에서는 `next/navigation`으로부터 useRouter를 가져와야 합니다.

그리고 기존의 useRouter에서 제공하는 기능들 중 라우팅과 관련된 기능들만 담당합니다.

pathname, query와 관련된 기능은 next/navigation의 `usePathname`, `useSearchParams`로 분리되었습니다.

 


Templates

child layout이나 페이지를 감싼다는 점에서 레이아웃과 비슷하지만,

템플릿은 레이아웃과 달리 각각의 children 에 대해 새로운 Instance를 만듭니다.

 

따라서... 템플릿을 공유하는 라우트들을 탐색할 경우 child의 새로운 인스턴스가 mount되고, DOM 요소가 새로 만들어지며, 클라이언트 컴포넌트에 state가 보존되지 않습니다. effect도 re-synchronized 됩니다.

 

언제 쓰면 좋을까요???

  • navigation 시 useEffect를 resynchronize 해야할 때
  • navigation 시 child Client Components의 state를 리셋해야 할 때

이런 경우에 효과적인 방법입니다.

 

// app/template.tsx

export default function Template({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}
<Layout>
  {/* 템플릿에는 고유한 키가 있어야 함 */}
  <Template key={routeParam}>{children}</Template>
</Layout>

 

템플릿은 레이아웃과 children 사이에서 렌더링됩니다!

 

 

더보기

Colocation

App 폴더 구조에 따라서 라우팅이 되는데 컴포넌트, 스타일, 테스트 등 파일들을 app 디렉토리에 넣을 수 있는 이유는 무엇일까요?

 

이는 page.js 또는 route.js 가 반환한 컨텐츠만이 공개적으로 주소로 지정할 수 있기 때문입니다.

(pages 폴더에 있는 모든 파일은 route로 고려됩니다.!!)

 


 

Loading UI 

loading.js 파일은 React Suspense와 함께 사용되어 컨텐츠가 로딩될 동안 로딩 UI를 보여주는 데에 사용됩니다.

 

컴포넌트 위계를 다시 살펴보면, Suspense 컴포넌트의 fallback 으로 Loading 컴포넌트가 들어가 있네요!

loading.js는 layout 안에 nesting되고, Suspense 아래에 있는 children들을 자동적으로 감싸게 됩니다.

 

Streaming

먼저 SSR 순서에 대해 간단히 알아보자면,

  1. 클라이언트가 페이지에 접속, 브라우저는 해당 페이지의 URL을 서버에 전송
  2. 서버는 이 요청을 받고, 페이지를 렌더링하기 위해 필요한 모든 데이터를 수집
    데이터는 주로 데이터베이스에서 가져오거나 외부 API를 호출하여 얻음, 페이지에 표시될 내용, 상태 및 기타 필요한 정보 포함
  3. 서버가 페이지를 위한 HTML 렌더
  4. 페이지의 HTML, CSS, JS 가 클라이언트에게 전송됨
  5. 상호작용 가능하게 하기 위한 hydration

서버는 2번 작업이 끝나야 HTML 렌더링을 할 수 있고,

클라이언트는 4번 작업 이후 다운로드가 완료되어야 hydration을 진행할 수 있습니다.

 

SSR with React 와 Next.js 는 non-interactive 한 페이지를 유저에게 먼저 보여주지만,

서버에서 필요한 데이터를 다 fetching하는 건 시간이 오래 걸릴 수 있습니다.

 

그래서 streaming은 페이지의 HTML을 더 작은 청크로 나누어 서버에서 클라이언트로 보내줍니다!

따라서 UI 렌더 전에 모든 데이터를 기다릴 필요 없이, 페이지의 일부가 더 빠르게 보여질 수 있습니다.

 

Streaming은 리액트 컴포넌트 모델과 함께 잘 작동하는데, 각 컴포넌트가 chunk가 될 수 있기 때문입니다.

높은 우선순위를 가지거나 데이터에 의존하지 않는 컴포넌트가 먼저 전송되고, 그러면 리액트는 hydration을 더 일찍 시작할 수 있습니다.

낮은 우선순위를 가진 컴포넌트들은 데이터 페칭이 완료된 이후 전송됩니다.

 

 

<Suspense>를 사용하면 

  1. Streaming Server Rendering: 서버-> 클라로 렌더된 HTML를 절차적으로 보내줌
  2. Selective Hydration: 리액트는 어떤 컴포넌트를 먼저 interactive하게 만들지 우선순위를 매김
    React는 컴포넌트 트리를 순회하면서 각 컴포넌트의 우선순위 결정
    <Suspense> 경계에 의해 감싸인 부분은 React에게 hydration 우선순위를 나타내는 신호를 제공

 

SEO 는 어떻게 될까요??

  • Next.js 는 클라에 UI를 스트리밍하기 전에 `generateMetadata` 안에서 데이터 페칭이 완료될 때까지 기다림
    • 첫 streamed response가 <head> 태그 포함하도록 보장
  • streaming이 서버에서 렌더되기 때문에, 초기 HTML Crawling 가능 -> SEO에는 영향을 미치지 않음.

 


 

How Routing and Navigation Works

전체적으로 라우팅과 네비게이션이 어떻게 일어나는지 정리해봅시다!

  1. Code Splitting
    1. application code를 작은 번들로 나누어 각 요청마다 전송되는 데이터의 양을 줄이고, 실행 시간을 줄여 성능을 개선합니다.
    2. 서버 컴포넌트는 route segments 로 application code가 자동적으로 split 되도록 합니다.
  2. Prefetching
    1. <Link>
      유저의 뷰포트에 보이게 되면 route 들은 자동으로 prefetch 됩니다. 처음 로드되거나, 스크롤을 통해 보이게 될 때 prefetching이 일어납니다.
    2. router.prefetch()
      useRouter 훅을 통해 route가 prefetch 되도록 만들 수 있습니다.
  3. Caching
    1. Next.js 는 Router Cache 라고 하는 in-memory client-side cache 가 있습니다. 사용자가 페이지를 돌아다니는 동안, prefetched route segment 의 리액트 서버 컴포넌트 페이로드와 방문한 route들이 캐시에 저장됩니다.

      네비게이션에 있어서 캐시가 가능한한 재사용되어, 서버에 요청을 보내는 횟수를 줄일 수 있어 성능이 개선됩니다.
  4. Partial Rendering
    1. 페이지를 이동했을 때 공유되는 segment들은 유지되고, 바뀌는 route만 효율적으로 재렌더링합니다.
  5. Soft Navigation
    1. 브라우저들은 페이지 이동시 hard navigation을 하는 반면,
      App router는 Partial rendering 을 하는 Soft Naviagation을 수행합니다.
  6. Back and Forward Navigation
    1. 뒤로가기나 앞으로 가기 시 기본적으로 스크롤 위치를 유지하고, Router Cache에 있는 route segments 를 재사용합니다.

 

 


Page router vs App router

Page router

  • pages 폴더 하위의 모든 폴더/파일명을 기반으로 경로로 사용 가능
  • components, lib 등은 pages 폴더 외부에 작성

App router

  • 서버 컴포넌트 지원
  • app 폴더 하위에 모든 파일 추가 가능
  • 폴더 안의 page.js 또는 router.js 로 작성된 파일만 경로로 사용
  • components, lib 등도 app 폴더 하위에 포함 가능

 

실습

>> https://citrine-tractor-afe.notion.site/Next-js-Routing-98e7acbfee784ee5bb59260579bf31c7?pvs=4

 

후기..

next.js는 서버 컴포넌트가 있고 폴더 구조에 따라 라우팅을 해주는구나~ 정도만 알고 있었던 것 같습니다.

반도 모르고 사용하고 있었던 것 같네요,, 공식 문서를 열심히 읽어야겠다는 생각이 듭니다.

양이 굉장히 많고 여러가지가 얽혀 있어 다 소개할 수가 없어서 넘어간 중요한 부분들도 많은 것 같습니다.

뭔가 ... 명확하게 잘 정리하고 싶었는데 부족한 것 같아 부끄럽네요 🥲

공부를 열심히 해야겠습니다 ...

긴 글 읽어주셔서 감사합니다!

 

 

 

참고한 문서

https://www.w3schools.com/react/react_router.asp

https://nextjs.org

 

 

'Dev > React' 카테고리의 다른 글

공통 컴포넌트 컨트롤버튼 개발일지  (1) 2024.11.20
useTheme vs theme import  (1) 2024.11.18
공통 컴포넌트 아이콘버튼 만들기  (0) 2024.11.17