Next.js - Pages와 라우팅

이효진·2023년 3월 5일
3

Next

목록 보기
2/2
post-thumbnail
Next.js 13버전에서는 베타로 app이라는 폴더를 안내한다.
layouts, 중첩 경로, Server Components가 기본으로
이 폴더 안에 깔려있다고 하는데...
내가 설치한 두 프로젝트는 package.json을 확인했을 때
13.2.1버전임에도 없다. 왜지?
일단 이번 주제에 집중하기 위해 먼저 pages부터 뜯어 봐야겠다.

굉장히 스타워즈 같은 시작

1. 준비물:pages 폴더

왼쪽은 가장 기본적인 폴더 구조, pages가 있다. 오른쪽은 src와 app을 설정한 폴더 구조, pages가 없고, src 폴더 하위에 app 폴더가 있다. 왼쪽은 가장 기본적인 폴더 구조, pages가 있다.
오른쪽은 src와 app을 설정한 폴더 구조, pages가 없다.

만일 app 폴더와 src 폴더를 설정하지 않으면, Next.js에 들어오자마자 pages라는 폴더가 눈에 띈다. 설정하지 않으면 src 폴더와 그 하위에 설정된 app 폴더가 있다. pages.tsx 파일도 함께.

Next.js에서 page는 하나의 React 컴포넌트로, .js, .jsx, .ts, .tsx 파일이 이 pages라는 폴더에 들어간다고 한다. 각 페이지는 그 파일 이름을 통해 라우팅이 지원된다고도.


2. Index routes

그러니까, pages 폴더 하위에 about.js라는 컴포넌트를 만들었을 때, 링크로는 localhost:3000/about으로 접근하면 된다는 말!

파일 경로라우팅 링크
pages/index.js/
pages/blog/index.js/blog

3. Nested routes

맨 마지막 행에서 눈치 챈 사람도 있을 것이다. Next.js는 중첩 라우팅을 제공한다. 중첩 폴더 구조를 생성했을 때, 파일은 자동으로, 위와 같은 방식대로 라우팅이 될 것이다.

파일 경로라우팅 링크
pages/blog/first-posts/blog/first-post
pages/dashboard/settings/username.js/dashboard/settings/username

4. Dynamic routes

항상 미리 정의된 경로를 사용할 수는 없을 것이다. 이런 상황에서 주로 쓰는 것이 동적 라우팅인데, Next.js는 이 또한 제공한다. 만일 블로그를 만든다고 생각해보자. 블로그 포스트는 동적 라우팅이 적용되어야 할 것이다. id로 각 포스트 링크를 구분한다 가정했을 때, 파일 이름을 이렇게 설정하면 된다.

pages/posts/[id].js

그러면 각 포스트는 다음과 같은 방식으로 접근할 수 있을 것이다.

posts/1
posts/2

Next.js에서는 [params]를 이용해서 동적 경로를 생성할 수 있다.

만약에 pages/post에 [pid].js라는 파일이 있다. 여기서 [pid]는 동적 라우팅에서 값이 들어갈 자리다. Next에서는 url에서 해당 위치에 값을 매칭한다.

파일에는 다음과 같은 코드가 저장되어 있다고 하자.

// next/router : 현재 경로의 정보를 들고 올 수 있는 모듈
import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  // 동적 라우팅에서 경로 이름
  const { pid } = router.query

  return <p>Post: {pid}</p>
}

export default Post

사용자가 /post/1, /post/abc 등의 경로를 입력했을 때, 파일은 pages/post/[pid].js에 매칭될 것이다. Next는 자동으로 url에서 매개변수의 값을 추출한다. 그 후 경로를 정의할 때 지정된 이름(여기서는 pid)과 매칭하여 router.query 객체를 생성할 것이다. 예시를 들어보자.

여기에 /post/abc라는 경로가 있다. 쿼리 객체로는 이렇게 나타날 것이다.

{ "pid": "abc" }

그러면, /post/abc?foo=bar라는 경로는 다음과 같이 쿼리 객체가 나타나지 않을까? pid는 abc와 매칭, foo는 bar와 매칭돼서 말이다.

{ "foo": "bar", "pid": "abc" }

하지만, 경로 매개변수들은 이름이 같은 쿼리 파라미터가 있다면 다시 재정의(override)된다. /post/abc?pid=123이라는 경로가 있다고 해보자.

pid의 '123'이라는 값은 경로 매개변수인 'abc'에 재정의되진 않는다. 두 파라미터 모두 'router.query' 객체에 있고 이름이 같다면, 경로 매개변수인 'abc'가 쿼리 파라미터인 '123'보다 우선 순위를 가진다. 이렇게 하면, 경로 매개변수 값을 기반으로 페이지를 동적으로 렌더링 할 수 있다고 한다. 그래서 결론은 다음과 같다.

{ "pid": "abc" }

중첩 동적 라우팅도 비슷한 방식으로 작동한다. pages/post/[pid]/[comment].js/post/abc/a-comment와 매칭될 것이고, 쿼리는 다음과 같다.

{ "pid": "abc", "comment": "a-comment" }

Client-side Navigation에서 동적 라우팅을 쓰려면 next/link를 사용하면 된다. 다음의 예시를 참고하자.

import Link from 'next/link'

function Home() {
  return (
    <ul>
      <li>
        <Link href="/post/abc">Go to pages/post/[pid].js</Link>
      </li>
      <li>
        <Link href="/post/abc?foo=bar">Also goes to pages/post/[pid].js</Link>
      </li>
      <li>
        <Link href="/post/abc/a-comment">
          Go to pages/post/[pid]/[comment].js
        </Link>
      </li>
    </ul>
  )
}

export default Home

4-1. Catch all routes

동적 라우팅은 모든 경로를 확장할 수 있다.
여기에 pages/post/[...slug].js라는 경로가 있다. 이 경로는 다음 경로들과 매칭된다.

/post/a
/post/a/b
/post/a/b/c
...

slug에 매칭되는 변수는 쿼리 파라미터로 전달 될 것이다. 그럼 위의 경로를 차례대로 입력한다고 해보자.

첫번째 경로 /post/a은 다음 쿼리 객체로 표현될 것이다.

{ "slug": ["a"] }

두번째 경로 /post/a/b가 들어오면, 새로운 파라미터 'b'가 배열에 추가될 것이다.

{ "slug": ["a", "b"] }

4-2. Optional catch all routes

대괄호 두번에 매개변수를 포함하여 Catch all route 경로를 선택적으로 만들 수 있다.

위와 다른 점은, optional을 이용하면 매개변수가 없는 경로도 매칭이 된다는 것이다.

{ } // GET `/post` (empty object)
{ "slug": ["a"] } // `GET /post/a` (single-element array)
{ "slug": ["a", "b"] } // `GET /post/a/b` (multi-element array)

4-3. 주의사항

먼저 정의된 경로는 동적 경로보다 우선시된다. 그리고 동적 경로는 catch all routes보다 우선된다. 예를 보자.

pages/post/create.js - Will match /post/create

// 동적 경로는 먼저 정의된 post/create와는 매칭되지 않음
pages/post/[pid].js - Will match /post/1, /post/abc, etc. But not /post/create

//Catch all routes는 동적 경로와는 매칭되지 않음
pages/post/[...slug].js - Will match /post/1/2, /post/a/b/c, etc. But not /post/create, /post/abc

5. Imperatively

Next에게는 대부분의 수요를 충족하는 모듈이 있다. 바로 next/link다. 하지만 client-side에서도 next/link를 제하고 사용할 수 있다. 바로 next/router를 이용하는 방법이다.

import { useRouter } from 'next/router'

export default function ReadMore() {
  const router = useRouter()

  return (
    <button onClick={() => router.push('/about')}>
      Click here to read more
    </button>
  )
}

6. Shallow Routing

얕은 라우팅은 getSurverSideProps, getStaticProps와 getInitialProps를 포함, 데이터를 가져오는 메소드를 다시 실행하지 않고 우리가 url을 바꿀 수 있게 해준다.

Next는 실제 페이지를 다시 로드하지 않고, url과 브라우저 기록을 업데이트 한다. 다시 말해 재렌더링되지 않고 데이터를 다시 가져오지 않아도 됨을 뜻한다.

이걸 쓰려면 shallow 옵션을 true로 세팅하면 된다.

// Current URL is '/'
function Page() {
  const router = useRouter()

  useEffect(() => {
    // Always do navigations after the first render
    router.push('/?counter=10', undefined, { shallow: true })
  }, [])

  useEffect(() => {
    // The counter changed!
  }, [router.query.counter])
}

export default Page

이 코드에서는 url을 '/?counter=10'으로 업데이트하고, 페이지의 변환 없이 경로의 state만 변환될 것이다.


6-1. 주의사항

얕은 라우팅은 오로지 현재 페이지에서 URL을 바꿀 때 작동한다. 만일 우리가 pages/about.js라는 페이지도 있다고 해보자, 그리고 다음 코드를 실행시킨다면?

router.push('/?counter=10', '/about?counter=10', { shallow: true })

새 페이지에서 얕은 라우팅을 요청하더라도 현재 페이지를 언로드하고->새 페이지를 로드한 후->데이터를 가져올 때까지 기다려야 한다.

미들웨어에서 얕은 라우팅을 사용할 경우, data fetch가 이루어지지 않으면 새로운 페이지가 현재 페이지와 일치하는 지 알 수 없다. 미들웨어에서 url을 동적으로 재작성하고 새 데이터를 가져오지 않는 이상, 클라이언트 측에서는 확인할 수 없다.

그래서 미들웨어에서 얕은 라우팅을 사용할 때, 모든 얕은 경로를 처리하고 사용자 경험과 데이터를 가져올 때 등을 고려해서 설계하는 게 중요하다.




참고

Next공식문서 - pages
Next공식문서 - Routing
Next공식문서 - Dynamic Routes
chatGPT

profile
코딩나라 감자서민

0개의 댓글