본문 바로가기
Programming/Web

[Next.js] Next.js Routing 이모저모

 

 

 

지난 시간 동안 React를 다뤄봤다. 그동안 vite를 사용해서 개발했는데, 이제는 SSR도 같이 제공해주는 Next.js로의 프로젝트 이관을 앞두고 있다. 수업 내용도 복기할 겸, 처음부터 다시 코드를 작성해본다.

 

 

React의 한계

Routing

기존 React Router는 일일히 어떤 페이지에 어떤 JSX 페이지를 노출할 지 설정해야줘야 했다. (라이브러리인 리액트는 Router 또한 설치해줘야 한다.) 

const Router = () => {
  return (
    <BrowserRouter>
        <Routes>
          <Route path="/" element={<GalleryPage />} />
          <Route path="/gallery" element={<DetailCardPage />}>
            <Route path=":cardId" element={<DetailCard />} />
          </Route>
        </Routes>
    </BrowserRouter>);
};

 

BrowserRouter를 통해 History 객체를 생성해준다. 페이지 이동 시 사용하는 Web API 중 History API를 SPA 라우팅으로 확장한다. 다른 페이지로의 이동은 <Link /> 컴포넌트로 통해 이동한다. 

 

하지만 Next.js에서는 React-Router-Dom을 사용하지 않고 파일 디렉토리 구조만으로 단순히 라우팅을 지원한다. 

(13버전 미만은 페이지 단위 라우팅, 13버전 이상은 컴포넌트 단위 라우팅) 

 

컴포넌트 단위 라우팅 

라우팅이란,
웹 애플리케이션에서 페이지 간의 전환, URL 경로에 다른 컴포넌트 렌더링 등을 처리할 수 있다.
쉽게 말해서 데이터가 전달되는 과정에 데이터의 목적지가 어디인지 확인해서 길을 찾아 전달해주는 역할을 한다. 

 

1. File System Routing 

개별 코드 설정 없이 파일 디렉토리 구조로 웹 페이지 간 Route를 정의한다. 즉 웹 페이지도 반환하고 API가 되어 데이터를 반환할 수도 있다.

 

2. Special Files 특수 파일 

웹 페이지 내 세부 컴포넌트 구성을 Route 파일로 정의 가능하다. 이를 통해 App Router는 전체 페이지의 전환이 아니라 부분 페이지의 전환을 활용하게 된다. 

 

Layout

불변이자 공통 영역이다. 리렌더가 발생되지 않는 부분이며 기본적으로 서버 컴포넌트이다. 메타 데이터나 헤더 푸터와 같은 모든 페이지에 필요한 공통 컴포넌트를 포함할 수 있다. 

 

Page

가변이자 비공통 영역. 리렌더가 발생하며 실제 페이지에 해당하는 부분이다. 

(그 외 Error, Template, Not Found 등 존재함)

 

Layout은 고정, Page는 가변

이것에 대한 예시를 알아보기 위해 src/app 아래 dashboard 디렉토리를 만들고 page.js와 layout.js를 생성한다. 

red 블럭이 존재하는 부분이 layout, 그 밑에 텍스트들은 모두 page.js에서 정의했다. layout은 이를 {chlidren}으로 받아 처리하기 때문에 아래와 같은 구조로 구성된다. 

Layout의 큰 틀 안에 Page.jsx가 위치한다.

 

 

이때, 나는 별도의 설정을 하지 않았는데도 아래와 같은 파일 구조 덕분에 /dashboard에 쉽게 접근할 수 있었다.

dashboard와 같은 각 폴더들은 Route Segment라고 불린다. 각 Route Segment는 URL Segment에 자동으로 매핑된다.

각 Route Segment 디렉토리에 정의한 Layout은 그 이후의 path 디렉토리에 모두 적용된다. (예컨대 dashboard 아래 setting라는 폴더를 하나 더 생성하면 /dashboard/settings로 접근할 수 있는 것이다.) 

위와 같이 파일구조를 만들면,

이렇게 접근이 가능하다. 

 

파일 구조를 보면, dashboard 아래 layout이 위치하고 있는 것을 알 수 있다. 정리하자면, app 아래 layout이 가장 첫번째, dashboard의 layout이 두번째 layout이 된다. 따라서 dashboard 아래 위치한 customer와 invoices는 app과 dashboard의 layout을 둘 다 계승받게 된다. 

 

페이지 렌더링에 사용되는 Special Files 특수 파일 중 가장 많이 사용되는 것은 Layout과 Page이다. 

 

 

에러 발생하기 

일부러 에러를 발생하기 위해 customer아래 page.js에 throw error를 작성한다. 그럼 아래와 같이 보여진다. 

에러를 적절하게 처리하기 위해 앞서 언급했던 erorr.js를 customers 아래 만들어준다. 

 

 

이때 error.js의 최상단에 'use client'를 추가한다. 그러면, /dashboard/customers 내 Error 파일이 생성되고 /dashboard/customers의 Page 내 에러를 발생하면 해당 페이지가 보여진다. 

페이지 렌더링에 사용되는 예약어 파일들을 제외하고 어떤 js 파일이든지 넣어도 상관 없다. 

 

 

컴포넌트 기반 SPA Routing을 위한 <Link /> 

위와 같이 /dashboard아래 layout.js에 버튼 3개를 생성한다. 해당 버튼들은 invoices, dashboard, customers로 이동하는 역할을 한다. 이 때, <a> 태그와 <Link /> 태그에 차이가 있다. 

 

하드 navigation, <a> 

<a> 태그는 클릭하면 브라우저가 현재 페이지를 완전히 새로고침하고, 새로운 URL을 요청한다. 이 과정에서 페이지의 모든 상태와 JS 컨텍스트가 초기화 된다. 

브라우저가 서버에 새로운 요청을 보내고, 서버는 HTML, CSS, JS 등 필요한 모든 리소스를 다시 응답하게 되며, DOM을 처음부터 다시 구성한다. 

 

SPA navigation, <Link /> 

React와 같은 SPA 프레임워크에서는 페이지를 이동할 때 JS를 통해 URL을 변경하고 브라우저 히스토리를 업데이트한다. 이 과정에서 전체 페이지가 새로고침 되지 않는다. 클라이언트 사이드 라우팅을 통해 JS가 URL 변경을 감지하고 필요한 컴포넌트만 교체하거나 업데이트한다. 실제로 어떤 링크를 클릭했을 때 전환되는 '효과'만 적용할 뿐 실제 페이지의 새로운 렌더링이 일어나지 않는다. <a>태그와는 다르게 js 컨텍스트가 유지될 수 있다. 게다가, 좀 더 빠를 수도 있음.

 

useRouter()

SPA Routing을 위한 또 한 가지 라우팅 방법이 있다. 바로 useRouter 훅을 사용하는 것. 

'use client'

import {useRouter} from 'next/navigation'

export default function ClientMenuButton({href, children}){
    const router = useRouter()

    return (
        <div
            onClick={()=> router.push(href)}
            className='border-2 border-solid p-[10px] border-white block'
        >
            {children}
        </div>
    )
}

 

위와 같이 useRouter를 import하고, ClientMenuButton을 클릭했을 때 .push(href)를 통해 해당 페이지로 이동한다.

useRouter와 Link?

궁금한 점이 생겼다. 이 두가지는 같은 역할을 하는 것 같은데, 왜 나뉘는 걸까? 
LinkSEO에 최적화 되어 있다. Next.js는 SSR을 지원하므로, <Link> 컴포넌트를 포함한 페이지도 SSR을 통해 초기 HTML을 생성할 수 있다. 또한 프리페칭이 가능하다. <Link> 컴포넌트는 클라이언트 사이드 네비게이션을 위해 기본적으로 링크된 페이지를 미리 가져올 수 있다. 때문에 페이지 전환이 빠르다
하지만 단순히 페이지 이동을 위한 컴포넌트로, 라우터 상태 관리나 복잡한 네비게이션은 지원되지 않는다. 

useRouter는 라우터 상태 관리를 위해 사용한다. 현재 경로, 쿼리 매개변수, 라우터 이벤트 등에 접근하고 이를 활용할 수 있다. 또한, 사용자의 상호작용이나 비즈니스 로직에 따라 페이지 이동이 가능하며 라우터의 상태를 세밀하게 제어하거나 페이지 이동 시 추가적인 로직 실행도 가능하다. 즉 유연성상태 접근, 이벤트 처리에 장점이있다.
다만 미리 가져오기 기능은 없다. 

 

 

Image를 위한 커스텀 <Image /> 태그

        <ClientMenuButton href='/'>
          <Image
            src='/vercel.svg'
            alt='Next.js 에서 이미지를 넣을때는 다음과 같이 최적화된 커스텀 컴포넌트 <Image> 추가'
            width={100}
            height={24}
            priority
          />
        </ClientMenuButton>

 

이미지는 일반적인 웹 사이트에서 페이지의 무게에 큰 부분을 차지한다. 이는 성능에 상당한 영향이 있을지도.

Next.js 이미지 구성 요소는 <img> 자동 이미지 최적화 기능으로 HTML 요소를 확장한다. 공식문서

 

Optimizing: Images | Next.js

Optimize your images with the built-in `next/image` component.

nextjs.org

 

Layout에 meta data가 있을 경우 use client를 사용할 수 없다. 

다시 말하자면 `use client`는 특정 파일이 클라이언트에서만 렌더링 되도록 지정한다. 해당 컴포넌트는 서버에서 렌더링 되지 않고 클라이언트 측에서만 렌더링 된다. 

Metadata란, <head> 태그 내 삽입 되어 SEO 및 페이지 정보를 제공한다. 메타 데이터는 SSR 중에 설정되어야 검색 엔진이 올바르게 인덱싱 할 수 있다. 

export const metadata: Metadata = {
  title: "Next를 위한 연습",
  description: "Generated by create next app",
};

 

그렇다면, 왜 레이아웃에 use client를 사용할 수 없는가? 

레이아웃에서 메타 데이터를 설정할 때 서버 사이드에서 설정하는 것이 중요하다. use client를 사용하면 해당 컴포넌트는 클라이언트 측에서만 렌더링되므로 서버에서 HTML을 생성할 때 메타 데이터가 포함되지 않는다. 이는 SEO 및 초기 페이지 로딩에 문제가 생길 수 있음.

 

 

Layout에서 컴포넌트로 분리하자

앞서 일어났던 문제는 사실 Button 컴포넌트 하나를 만들어 layout에서 사용하려 했기 때문이다. 그리고 그 버튼은 router를 사용한다. useRouter는 오직 클라이언트 컴포넌트에서만 사용가능하다. 이런 문제로 앞선 문제가 발생함.

따라서 app 아래 button-group.js를 만들어 해당 버튼들을 재정의했다. use client를 삽입했고, 각각 '앞으로 가기, 뒤로 가기, 새로고침' 기능을 useRouter를 통해 실행하도록 작성한다. 이를 app의 layout.tsx에 작성해주면, 

이렇게 된다. 어느 위치에 나타나도록 작성했는지 잘 기억해본다.

 

 

clsx를 사용해서 현재 위치 다르게 표기해보기 

clsx 
clsx는 CSS 클래스 이름을 조건부로 조합하기 위한 유틸리티 라이브러리다. 

 

    return (
        <div
            onClick={()=> router.push(href)}
            className={clsx(
                'border-2 border-solid p-[10px] block',
                isCurrentPage ? 'border-red-500': 'border-white'
            )}
        >
            {children}
        </div>
    )

menu-client.js에 위와 같이 clsx를 사용해서 조건을 결합했다. 

usePathname을 사용해 현재 props로 가져온 href가 usePathname과 일치하는지 확인하고 그렇다면 빨간색 border를 준다. 

 

 

Dynamic Routing 페이지 만들기 [slug] 

[slug]와 같이 디렉토리 명을 지으면 Single Segment로 라우팅이 가능하다. 

customers 아래 만들어준다.

 

page.js에 아래와 같이 작성해준다. 

export default function CustomersNickname({params, searchParams}){
    const {nickname } = params
    return(
        <main
            className='flex min-h-screen flex-col items-center p-24'
        >
            어떤 유저의 정보 표시할까요? <b>{nickname}</b>
        </main>
    )
}

 

`dashboard/customers/쓰고 싶은 닉네임`과 같이 URL을 입력하면 

이렇게 nickname이 받아와진다. nickname을 params로 받아와, 해당 정보를 뿌린 것이다. 이때 searchParam은 쿼리 스트링에 해당함.

 

 

다수 Segment [...slug]

우선, dashboard/invoices/[...information]/page.js로 페이지를 만들어준다. 

그러면 dashboard/invoices/type/detail과 같이 URL 요청을 보냈을 때 type과 detail을 받을 수가 있다. 그런데...!

치명적인 실수를 저질렀다. 폴더 이름을

이렇게 적어버린 것... information을 아무리 비구조화 하려 해도 iterable할 수 없다며 거부 당했다. 디렉토리의 이름으로 해당 세그먼트들을 받는구나... 깨달았다. 아무튼, 



export default function InvoicesInformation({ params, searchParams }) {
    
    const { information } = params
    console.log(information)
    const [type, detail] = information
    return (
      <main className='flex min-h-screen flex-col items-center p-24'>
        어떤 계약에 대한 내용을 볼까요?
       <div>타입은 {type}과 같고,</div>
        <div>상세내용은 {detail}과 같습니다</div> 
      </main>
    )
  }

 

이렇게 비구조화를 통해 여러 세그먼트들을 가져올 수 있다. 

 

 

다수 Segment [[...slug]] = Optional Catch-all Segments

우선, 디렉토리 구조를 아래와 같이 만들어준다. 단순히 dashboard 아래 users 폴더를 추가한 것.

 

[[]]의 특징은 `빈 값도 수용할 수 있다`라는 것이다. 앞서 알아봤던 []은 빈 값이 들어오면 매칭되지 않는다. 그런데 여기서 또 하나의 얕은 깨달음이 있었다. 위와 같이 users 아래에 page.js를 정의하면 dashboard/users로 요청해도 [[...information]]으로 매칭되지 않는다. 만약 그렇게 만들고 싶다면 users의 page.js를 삭제하면 된다. 

export default function CustomersNickname({params, searchParams}){
    console.log("매칭 ㅇㅇ")
    const information = params.information || []
    
    return(
        <main
            className='flex min-h-screen flex-col items-center p-24'
        >
            {information.length === 0 ?
        <p> 환영합니다.</p> : 
        <>
        <p>nickname들을 보여주겠습니다.</p>    
        <ul>
            {information.map((segment, index) => (
                <li key={index}>{segment}</li>
            ))} 
        </ul>
        </> 
        }
        </main>
    )
}

따라서 params의 배열이 빈 값이라면 달리 처리할 수 있다. 

 

 

'Programming > Web' 카테고리의 다른 글

[AWS] AWS VPC와 Subnet 설정  (2) 2024.06.09
[Servlet & Jsp &DB] 2초마다 변하는 배너 만들기  (0) 2023.06.16
[Servlet & JSP] get, post 만들기  (0) 2023.06.16
JavaScript Tutorial  (0) 2023.06.16
Web 시작  (0) 2023.06.16