[Next.js] Routing 1부

용어

용어 개념 기타
Tree Next.js의 하이라키 폴더 구조를 뜻함. 폴더가 기본적으로 트리구조를 가짐.

/dashboard/settings

  • / (Root segment)
  • dashboard (Segment)
  • settings (Leaf segment)
SubTree 폴더를 구성하는 트리의 일부분.
Root 트리나 서브트리의 최상위 부모 폴더.
Leaf 트리나 서브트리에서 자식 폴더가 없는 폴더.
URL Segment URL에서 / 사이에 구분되어져 있는 값
URL Path Domain 이후의 경로
Route Segment 라우트 세그먼트는 폴더 구조에 따라 URL Segment를 구성

 

App Router 와 Pages Router
두 라우터는 하나의 프로젝트에서 동시에 사용이 가능하지만, 겹치는 라우터가 존재하는 경우 App Router에 있는 경로가 우선적으로 반환되는 로직으로 구성되어져 있습니다.

 

❓폴더와 파일
폴더는 기본적으로 라우터를 정의 하기 위해 사용되는 방식입니다. 파일은 해당 라우터를 어떻게 구성하는지 보여주는 UI를 보여주는데 사용됩니다.

 

파일 컨벤션

Next.js에서는 지정된 파일명을 사용시, 라우트 세그먼트에서 상호작용이 일어나도록 만들어졌습니다.

라우터 구조에서는 하위 컴포넌트에서 사용되는 특별 페이지는 하위 컴포넌트에서 작성이 되어져야 합니다.

 

파일 구조

지정된 파일을 작성할 시 빌드 단계에서 아래 순서내로 파일을 구성하게 됩니다.

  • layout.js
  • template.js
  • error.js (React error boundary)
  • loading.js (React suspense boundary)
  • not-found.js (React error boundary)
  • page.js or nested layout.js

라우터로 폴더를 작성 하였을 때, page.jsroute.js 인 파일만 인식을 합니다.

 

레이아웃

레이아웃은 여러 라우터에 UI를 공유하기 위해서 사용되는 기술입니다. 레이아웃의 경우, 상태를 유지하고 유저와의 상호작용을 기억하고 있으며, 다시 렌더링이 되지 않습니다. 이러한 레이아웃은 여러 개로 중첩이 되어서 사용이 가능합니다.

 

Root Layout (Required)

app 폴더 아래 만들어지는 최상단 레이아웃으로 생성하는 것을 요구하고 있습니다.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Layout UI */}
        <main>{children}</main>
      </body>
    </html>
  )
}

 

Nested Layout

레이아웃이 겹쳐지는 구조로 하위 페이지에 필요한 레이아웃을 구성할 때 사용 됩니다.

 

알아두면 좋은 것들

  • 레이아웃은 오직 html 과 body 태그만 포함 시킬 수 있습니다.
  • layout.js 와 page.js가 같은 경로에 있다면 자동적으로 페이지는 해당 레이아웃을 포함하게 됩니다.
  • 레이아웃은 기본적으로 서버 컴포넌트이지만 클라이언트 컴포넌트로도 변경이 가능합니다.
  • 레이아웃에서도 데이터 요청이 가능합니다.
  • 부모 레이아웃에서 받아온 데이터를 자식 레이아웃에 넘기는 것은 불가능 합니다. 그러므로 한번 더 자식 레이아웃에서 데이터를 불러오는 작업이 필요로 합니다.
  • 레이아웃은 기본적으로 자신이 포함하고 있는 세그먼트들에 접근 할 수 없습니다. 접근하고자 하는 경우, useSelectedLayoutSegment or useSelectedLayoutSegments 를 사용하세요.
  • 라우팅 그룹을 사용하면 여러 레이아웃을 같은 계층에서 만들어 선택적으로 사용 가능하게 됩니다.

 

템플릿

페이지에서 각 컴포넌트마다 사용되는 훅 또는 상태관리를 위해서 사용이 됩니다.

import React from 'react';
import ReactDOM from 'react-dom';
import {
  BrowserRouter as Router,
  Route,
  Switch
} from 'react-router-dom';
import PageTemplate from './PageTemplate';
import HomePage from './HomePage';
import AboutPage from './AboutPage';

const App = () => {
  return (
    <Router>
      <Switch>
        <Route path="/home">
          <PageTemplate>
            <HomePage />
          </PageTemplate>
        </Route>
        <Route path="/about">
          <PageTemplate>
            <AboutPage />
          </PageTemplate>
        </Route>
      </Switch>
    </Router>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

템플릿은 호출 할 때마다 새로운 인스턴스를 생성하므로 중복 걱정 없이 사용이 가능합니다.

 

메타데이터

헤더 태그에 존재하는 메타데이터를 수정 할 수 있습니다. layout.jspage.js에서 아래와 같은 코드를 사용하면 됩니다.

import { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'Next.js',
}
 
export default function Page() {
  return '...'
}

 

Linking & Navigating

<Link> 컴포넌트의 경우, a태그를 확장한 개념으로 prefetching 기능을 제공하기 위해 사용됩니다. Next.js에서는 Link 컴포넌트를 사용하는 것을 우선적으로 권장하고 있습니다.

 

Link 컴포넌트는 href props를 받아서 사용할 수 있습니다.

import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

 

Link 컴포넌트를 통해 동적인 href 경로를 제공할 수 있습니다.

import Link from 'next/link'
 
export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

 

Link 컴포넌트의 활성화 상태를 동적으로 제공하고자 하면 usePathName() 과 함께 사용하면 됩니다.

'use client'
 
import { usePathname } from 'next/navigation'
import Link from 'next/link'
 
export function Links() {
  const pathname = usePathname()
 
  return (
    <nav>
      <ul>
        <li>
          <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
            Home
          </Link>
        </li>
        <li>
          <Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href="/about"
          >
            About
          </Link>
        </li>
      </ul>
    </nav>
  )
}

 

Scroll

Link 컴포넌트는 스크롤 위치를 원복 하는 것이 기본 설정입니다. 하지만 개발자가 임의로 위치를 원하는 곳으로 이동하고자 하면 #을 추가하여 원하는 스크롤 위치로 이동이 가능합니다.

<Link href="/dashboard#settings">Settings</Link>
 
// Output
<a href="/dashboard#settings">Settings</a>

 

기본 스크롤 위치로 이동하는 것을 사용하기 싫다면 옵션을 통해 돌아가지 못하게 할 수 있습니다.

// next/link
<Link href="/dashboard" scroll={false}>
  Dashboard
</Link>

// useRouter
import { useRouter } from 'next/navigation'
 
const router = useRouter()
 
router.push('/dashboard', { scroll: false })

 

useRoute()

useRouter훅은 클라이언트 컴포넌트에서 라우터를 변경 할 때 사용됩니다.

'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

 

redirect

redirect는 서버 컴포넌트에서 라우터를 변경하고자 할 때, 사용됩니다.

import { redirect } from 'next/navigation'
 
async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}
 
export default async function Profile({ params }: { params: { id: string } }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }
 
  // ...
}

 

알아두면 좋은 내용

  • redirect를 사용 할 시에 server action이 일어나고 기본적으로 307 코드를 반환합니다.
  • post 요청 성공 이후에 일어나는 redirect는 303 코드를 반환합니다.
  • redirect를 사용할 시 에는 내부에 error를 던지고 있어, try/catch문을 사용하여야 합니다.
  • 클라이언트 컴포넌트에서도 redirect를 사용 할 수 있으나, 렌더링 시에 실행되는 기능으로 클라이언트 컴포넌트에서는 useRouter훅을 사용 하는 것을 권장합니다.
  • redirect로 외부 URL로 이동하는 것이 가능합니다.
  • 페이지 렌더링 전에 다른 페이지로 이동하는 기능을 원하면 미들웨어를 사용하면 됩니다.

 

native History API

window 객체에서 제공하는 일부 기능과 함께 Next.js에서 제공하는 기능을 사용하는 방법을 제공하고 있습니다.

 

  • window.history.pushState : 기존 페이지 URL에서 추가적으로 URL Params를 추가할 시에 사용 되어 집니다.
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SortProducts() {
  const searchParams = useSearchParams()
 
  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }
 
  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}
  • window.history.replaceState : 경로를 완전 다른 곳으로 이동 할 때에 사용이 됩니다.
'use client'
 
import { usePathname } from 'next/navigation'
 
export function LocaleSwitcher() {
  const pathname = usePathname()
 
  function switchLocale(locale: string) {
    // e.g. '/en/about' or '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }
 
  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}

 

Code Splitting

서버가 작동이 되면 라우터 세그먼트를 / 기반으로 세그먼트를 구분하여, 미리 해당 페이지에 필요한 내용들을 prefetchscaches 처리를 합니다.

 

Prefetching

Next.js에서 <Link> 컴포넌트의 기본 prefetching(사전 로드) 동작은 loading.js 파일의 사용 여부에 따라 다르게 작동합니다.

기본적으로 Next.js는 사용자가 페이지를 탐색할 때 더 나은 사용자 경험을 제공하기 위해 해당 페이지의 데이터를 미리 가져옵니다. 그러나 loading.js 파일을 사용할 경우, 사전 로드 동작이 최적화되어 페이지 트리를 따라 첫 번째 loading.js 파일이 있는 부분까지만 사전 로드되고, 그 이후의 컴포넌트들은 사전 로드되지 않습니다. 이는 동적 경로 전체를 가져오는 비용을 줄여주고, 더 빠르게 로딩 상태를 표시할 수 있게 해줍니다.

 

loading.js 파일이 있는 경우, Next.js는 페이지 트리의 첫 번째 loading.js 파일이 있는 부분까지만 사전 로드합니다. 

/pages 
  /about 
    index.js 
    loading.js 
  _app.js 
  index.js

위와 같은 디렉토리 구조에서 about 페이지에는 loading.js 파일이 있습니다.

  • Next.js는 about 페이지를 사전 로드할 때 loading.js 파일까지만 로드합니다.
  • loading.js 파일이 있는 지점까지만 사전 로드하고, 그 이후의 컴포넌트는 사용자가 실제로 페이지를 탐색할 때 로드됩니다.

이로 인해 동적 경로 전체를 미리 로드하는 비용을 줄일 수 있고, 로딩 상태를 더 빠르게 표시할 수 있어 사용자에게 더 나은 시각적 피드백을 제공합니다.

 

알아두면 좋은 내용

production 환경에서만 prefetching이 수행됩니다. 개발 모드에서는 해당 기능을 사용 할 수 없습니다.

 

Router Cache

라우터 캐시의 경우 클라이언트 사이드 메모리 캐싱 기능으로 유저가 app내에서 이동을 할 때, React Server Components Payload를 미리 prefetching을 해두고 유저 메모리에 저장 해둡니다. 결론은 유저에게 저장된 캐시 기능을 최대한 사용하여 최적화된 환경을 제공해줍니다.

 

Partial Rendering

라우터 세그먼트에 따라 별로의 청크 파일들이 서버에 저장되어져 있고, 경로를 이동하게 될 경우, 상단 레이아웃을 제외한 부분만 재 렌더링이 일어나게 됩니다.

  • /dashboard/settings
  • /dashboard/analytics

 

Soft Navigation

라우터 세그먼트 기반으로 부분적 렌더링이 일어나기 때문에, Soft Navigation 이 가능합니다.

 

Back and Forward Navigation

기본적으로 Next.js는 스크롤의 위치를 Router Cache에 기억하고 있어 필요 시에 재 사용이 가능합니다

 

Routing between pages and app

  • 자동 라우팅 처리
    • Next.js는 pages/와 app/ 디렉토리 간의 페이지 전환을 처리할 때 "하드 네비게이션"을 자동으로 수행합니다. 즉, 이 두 디렉토리 간의 이동은 전체 페이지 새로고침처럼 동작합니다.
    • 이를 통해, pages/에서 app/으로, 또는 그 반대로 이동할 때 자동으로 적절한 라우팅을 처리합니다.

 

  • 클라이언트 라우터 필터
    • Next.js는 pages/와 app/ 간의 전환을 감지하기 위해 클라이언트 라우터 필터를 사용합니다.
    • 이 필터는 확률적(Probabilistic) 체크를 활용하여 app/ 경로로의 전환을 감지합니다.
    • 필터는 때때로 잘못된 긍정(False Positive)을 발생시킬 수 있지만, 기본적으로 이러한 발생률은 0.01%로 매우 낮게 설정되어 있습니다.
    • 이 발생률은 next.config.js 파일에서 experimental.clientRouterFilterAllowedRate 옵션을 통해 조정할 수 있습니다. 그러나 잘못된 긍정률을 낮추면 클라이언트 번들의 필터 크기가 커집니다.

 

  • 클라이언트 라우터 필터 비활성화
    • 이 기능을 완전히 비활성화하고 수동으로 라우팅을 관리하고자 하는 경우, next.config.js 파일에서 experimental.clientRouterFilter 옵션을 false로 설정할 수 있습니다.
    • 이 기능을 비활성화하면 pages/의 동적 경로가 app/ 경로와 겹치는 경우 기본적으로 올바르게 탐색되지 않습니다.

 

Loading UI And Streaming

Next.js에서 제공하는 loading.js는 리액트의 Suspense Boundary와 함께 유용한 기능을 제공하고 있습니다. 유저가 라우트 세그먼트를 변경 시, 유저에게 로딩 컴포넌트를 보여주게 함으로써 유저의 경험을 좀 더 유익하게 해주고 있습니다.

 

Instant Loading States

즉시 로딩 상태는 우리가 흔히 알고 있는 스켈레톤과 스피너 등을 의미 하고 있습니다. 해당 로딩을 통해 유저에게 현재 페이지가 생성되고 있음을 알려줍니다.

 

Streaming with Suspense

Suspense Boundary 를 통해 loading.js를 적용하는 부분을 직접적으로 지정을 할 수 있습니다.

 

기본적으로 SSR에서의 스트리밍을 아래와 같이 진행됩니다.

  • 페이지를 구성하는데 필요한 모든 데이터를 불러옵니다.
  • 서버에서는 데이터를 기반으로 HTML을 작성합니다.
  • HTML, CSS, JS를 유저에게 전송합니다.
  • 유저는 비반응형 페이지를 생성합니다.
  • hydrate 과정을 통해 반응형 페이지로 변환합니다.

 

위의 로직은 각 과정을 순차적으로 진행해야 합니다. 그러다 보니 렌더링에 대한 속도 최적화에 대한 문제가 이슈가 될 수 밖에 없습니다.

스트리밍

스트리밍은 기존 렌더링이 가지고 있는 속도 문제를 해결하는데 사용되어지는 기술입니다. 각 컴포넌트가 완전히 생성이 되면 보여지던 것을 좀 더 세분화 하여 완성이 되는 세부 컴포넌트부터 유저에게 보여주도록 하고 있습니다.

이러한 기능이 가능한 이유는 각 컴포넌트 간에 우선순위가 존재하고 그리고 데이터와 상관없이 렌더링이 될 수 있는 컴포넌트가 존재 하기 때문입니다.

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

일차적으로 부분 스트리밍을 수행하고 이후 선택적 Hydration이 수행이 되는 것을 볼 수 있습니다.

 

SEO

Next.js는 head에 있는 메타데이터를 받기 전까지 유저에게 스트리밍 UI를 제공하지 않고 있습니다. 즉 모든 데이터는 메타데이터가 존재 하게 됨으로써 검색엔진 최적화가 일어나고 있다고 볼 수 있습니다.

 

Status Codes

스트리밍이 완료가 되면 200 번 상태 코드를 반환하도록 되어져 있습니다. 이러한 방식이 redirect나 notFound 상황에서도 유저에게 제대로 진행되고 있는지 여부를 알려주는데 사용되고 있습니다.

 

Error Handling

error.js는 런타임 환경에서도 예상치 못한 에러를 처리하는데 사용됩니다. 특징은 아래와 같습니다.

  • 상위 세그먼트에서 설정을 하면 자동적으로 하위 라우트 세그먼트의 에러를 잡을 수 있게 됩니다.
  • 부분 세그먼트 마다 별도의 에러 UI를 제공 할 수 있습니다.
  • 부분 세그먼트 마다 별도의 에러 처리 함수를 제공 할 수 있습니다.
  • 에러가 발생 시, 전체 페이지를 재 렌더링을 하는 것이 아닌 일부 렌더링 처리를 할 수 있습니다.
'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

 

라우터 폴더 내에 error.js를 작성하게 되면 자동으로 Error Boundary가 page 컴포넌트를 감싸도록 되어져 있습니다.

 

Recovering From Errors

여러가지 에러 케이스가 존재 할 수 있지만 일부 에러의 경우 다시 불러오게 되면 해결 될 가능성이 큽니다. 그렇기 때문에 사용자에서 reset 시킬 수 있는 버튼을 제공함으로써 문제를 해결토록 할 수 있습니다.

'use client'
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

 

파일 구조에 따른 문제점

같은 폴더 내에 layout.js, template.js, error.js가 존재 하게 되면 error.js는 두 개의 파일을 감싸지 못하도록 되어져 있습니다. 이러한 경우, global-error.js 를 작성하여 전체 에러를 잡도록 해야 합니다.

 

서버 에러

서버 컴포넌트에서 발생되는 에러의 경우, 자동적으로 error.js 에 에러를 보내도록 되어져 있습니다. 실제 서비스 단계에서 민감한 정보가 에러에 반영이 되어질 수 있으므로 민감한 정보를 제공하도록 되어 있지 않습니다. 대신 에러를 추적할 수 있는 digest 해쉬 값을 같이 제공하고 있습니다.

'Frontend > Next.js' 카테고리의 다른 글

[Next.js] Routing 2부  (0) 2024.06.22
[Next.js] Server and Client Composition Pattern  (0) 2024.06.22
[Next.js] Client Components  (0) 2024.06.22
[Next.js] Server Components  (0) 2024.06.22
[Next.js] 왜 두 번 렌더링 되는 것일까?  (1) 2024.02.28