NextJS를 이용하는 경우 프로젝트 내 pages라는 폴더를 통해서 추가적인 코드나 패키지 설치 없이도 Routing 기능을 사용할 수 있는 것을 앞에서 살펴보았습니다.
pages 폴더 내 파일명이나 폴더명으로 HTML 문서 이름이 pre-rendering되어 생성됩니다. 즉, 파일명이나 폴더명이 URL의 경로의 의미를 갖게 됩니다. 생성된 HTML 문서는 빌드시 서버측 루트 경로에 존재하게 됩니다.
참고로 index라는 이름으로 페이지 컴포넌트 파일명을 지정하게 된다면 이는 해당 파일이 속한 폴더의 루트 경로가 됩니다.
pages 폴더 내 "서브 폴더"로 페이지 컴포넌트 파일을 중첩하여 저장하는 방식으로 "중첩 경로"을 구현할 수 있습니다.
page 폴더 내 서브 폴더명은 "/"로 구분되어 하나의 경로값으로 사용됩니다.
> pages 폴더
// index.html 문서 생성
// "/" 요청받으면 index.html 문서를 응답으로 전달
index.js
> news 폴더
// news.html 문서 생성
// "/news" 요청받으면 news.html 문서를 응답으로 전달
index.js
// news/abc.html 문서 생성
// "/news/abc" 요청받으면 news/abc.html 문서를 응답으로 전달
abc.js
> article 폴더
// news/article.html 문서 생성
// "/news/article" 요청받으면 news/article.html 문서를 응답으로 전달
index.js
// news/article/qwe.html 문서 생성
// "/news/article/qwe"요청받으면 news/article/qwe.html 문서를 응답으로 전달
qwe.js
,,,
pages 폴더 내 index.js로 생성한 index.html 문서는 요청한 URL의 경로가 "/"일 때 응답으로 전달
pages/news 폴더 내 index.js로 생성한 news.html문서는 요청한 URL의 경로가 "/news"일 때 전달
pages/news/article/qwe.js 파일로 생성된 news/article/qwe.html 문서는 요청한 URL의 경로가 "/news/article/qwe"일 때 응답으로 전달합니다.
동적 라우팅이란 URL 경로값으로 페이지 컴포넌트 내 렌더링될 정보를 동적으로 결정하여 페이지를 렌더링합니다. 이는 하나의 페이지를 재사용하여 여러 페이지로 동작하기 위해서 사용합니다.
NextJS에서 동적 라우팅하는 페이지를 구현하기 위해서 페이지 컴포넌트 파일명을 "[파일명].js" 형식으로 작성해주어야 합니다.
즉, "[,,,]" 안에 작성된 것이 "경로 파라미터"가 되고, 요청한 URL 경로에 의해 동적으로 결정된 경로값이 할당됩니다.
> pages 폴더
// "/어떤값" 요청시
[userId].js
> news 폴더
// "/news/어떤값" 요청시
[newsId].js
위 pages 구조를 갖는 NextJS 프로젝트의 경우 pages 폴더 내 "[,,,].js" 형식으로 작성된 파일들은 모두 동적 라우팅으로 동작하는 페이지 컴포넌트 파일을 의미합니다.
"[,,,]" 안에 작성한 것은 경로 파라미터로 동작하며, 요청한 URL 경로값으로 결정된 경로값이 경로 파라미터에 할당됩니다.
"/a"를 요청시 userId 경로 파라미터에는 a가 할당됩니다.
"/qqq"를 요청시 userId 경로 파라미터에는 qqq가 할당됩니다.
"/news/abc"를 요청시 newsId 경로 파라미터에는 abc가 할당됩니다.
"/news/pop"를 요청시 newsId 경로 파라미터에는 pop가 할당됩니다.
pages 폴더내 파일 뿐만 아니라 서브 폴더명을 "[폴더명]"으로 작성할 수 있습니다. 그리고 폴더 내 중첩 경로로 동적 라우팅하는 페이지 컴포넌트도 작성할 수 있습니다.
폴더명과 폴더 내 파일명을 모두 "[경로 파라미터]" 형식으로 생성하는 경우 여러 개의 경로 파라미터를 사용할 수 있습니다.
> pages 폴더
index.js
about.js
> client 폴더
index.js
> [clientId] 폴더
// "/client/어떤것" 요청시
// 경로파라미터 clientId에 경로값 할당
index.js
// "/client/어떤것/어떤것" 요청시
// 경로 파라미터 clientId, clientProjectId 각각 경로값 할당
[clientProjectId].js
위 pages 폴더 구조처럼 "[clientId]"라는 이름으로 서브 폴더를 생성하고, 내부에 "[clientProjectId].js"를 생성할 수도 있습니다. 이렇게 여러 개의 경로 파라미터를 사용하는 것도 가능합니다.
만약 "/client/user123/project987"을 요청한 경우 clientId 경로 파라미터에는 "user123"이 할당되고, clientProjectId 경로 파라미터에는 project987이 할당됩니다.
앞선 방법처럼 서브 폴더를 이용하여 폴더명을 "[폴더명]" 형식으로 중첩하여 페이지 컴포넌트를 작성할 필요 없이 "[...경로 파라미터].js"으로 작성하여 한 번에 여러 개의 경로 파라미터를 갖도록 작성할 수 있습니다.
> pages 폴더
index.js
> blog 폴더
// "/blog/어떤것/어떤것,,," 요청시
[...slug].js
위 pages/blog 폴더의 "[...slug].js" 파일 이름처럼 "[...파일명].js"형태로 생성했습니다. 동적 라우팅을 기반으로 동작하기 때문에 "[,,,]"안에 파일명을 작성하고 파일명 앞에 "..."을 작성합니다. 작성한 파일명은 경로 파라미터로 사용됩니다.
이러한 형태로 생성하게 된다면 만약 "/blog/2022/02/02" 요청시 페이지 컴포넌트 내 useRouter
훅 호출시 반환된 객체의 query 프로퍼티에는 { slug: ['2022', '02', '02] }
객체가 존재합니다.
즉, 단일 경로 파라미터 값을 사용하는 것이 아니라 경로를 구분하는 "/"으로 각 경로값을 요소로 갖는 "배열"을 경로 파라미터 값으로 갖도록 합니다.
주의할 점으로 경로 파라미터 값의 개수가 가장 근접한 동적 페이지와 매칭되는 것에 주의해야 합니다.
우리는 NextJS가 자체적으로 갖고 있는 "next/router" 이라는 패키지에서 "useRouter
"라는 커스텀 훅을 사용하여 경로 파라미터 값을 추출할 수 있습니다.
useRouter
훅을 호출하여 반환된 "객체"를 통해서 경로 파라미터 값을 취득할 수 있습니다. 반환되는 객체의 "query"라는 프로퍼티에 객체가 바인딩되어 있습니다.
query 객체의 프로퍼티 키로 대괄호안에 작성한 파일명, 즉 경로 파라미터가 존재하고, 프로퍼티 값으로는 동적으로 결정된 경로값이 존재합니다.
예를 들어, "/a" 요청시 "pages/[userId].js" 파일에 작성된 페이지 컴포넌트 내에서 아래와 같은 코드로 경로 파라미터 값을 취득하여 렌더링될 데이터를 결정할 수 있습니다.
// page/[userId].js
import { useRouter } from 'next/router';
const User = () => {
// useRouter 훅을 호출하여 router 객체 생성
const router = useRouter();
// 반환된 router 객체의 query 프로퍼티에 바인딩된 객체를 통해서 취득
// query 객체에는 경로 파라미터와 동일한 이름으로 프로퍼티가 존재
// router.query -> Object { userId: "a" }
const userId = router.query.userId;
// userId를 통해 렌더링할 데이터를 결정,,,
return (
<>
<h1>The User Page</h1>
<p>{userId}</P>
</>
);
}
export default User;
즉, router.query 객체에 파일명, 즉 경로 파라미터인 userId 프로퍼티 키로 동적으로 결정된 경로값인 a를 취득할 수 있습니다.
router 객체는 추가적으로 아래와 같은 프로퍼티를 갖고 있습니다.
// apple/[name]?key=123 -> apple/banana?key=123
{
// 현재 페이지 경로(page 디렉토리 경로)
pathname: '/apple/[name]',
// 동적 경로 파라미터, 쿼리 파라미터 값
query: {
name: 'banana',
key: '123'
},
// 브라우저 주소 표시줄에 표시되는 경로
asPath: '/apple/banana',
// 현재 페이지의 라우트 패턴
route: "/apple/[name]",
,,,
}
기본적으로 동적 라우팅하는 페이지의 경우 "빌드시"에 pre-rendering되어 생성된 HTML 문서는 "Fallback 버전의 페이지"입니다(pre-rendering 방식이 Static인 경우).
Static 방식으로 빌드시 pre-rendering을 진행하게 되는 경우 서버측에서는 구체적인 경로 파라미터 값을 알 수 없기 때문에 Fallback 버전 HTML 문서를 생성하게 됩니다.
아래 그림에서 "[파일명].html"이 Fallback 버전의 페이지를 의미합니다.
Fallback 버전이란 아래와 같은 특징을 갖는 페이지입니다.
페이지 컴포넌트 함수에게 "props로 빈 객체"를 전달하여 생성된 결과를 콘텐츠로 갖는 HTML 문서를 의미합니다.
router 객체의 query가 "빈 query 객체"인 상태로 실행하여 생성된 결과를 콘텐츠로 갖는 HTML 문서를 의미합니다.
동적 라우팅의 경우 요청을 받아야만 동적 경로값을 알 수 있기 때문에 빌드시에는 어떤 경로값을 가지는지 NextJS가 알지 못하기 때문입니다.
같은 폴더 내 동적 라우팅하는 페이지 컴포넌트는 같은 폴더 내 이미 존재하는 페이지 컴포넌트 이름을 제외한 나머지 경로와 매칭됩니다.
즉, 미리 정의된 정적 라우트가 동적 라우트보다 우선되며, 동적 라우트는 미리 정의된 정적 라우트를 제외한 나머지 모든 경로와 연결됩니다.
또한 같은 폴더 내 경로 파라미터 개수가 동일한 동적 페이지 컴포넌트를 중복하여 생성할 수 없습니다.
> pages 폴더
index.js
> news 폴더
index.js -> "/news/" 요청시
about.js -> "/news/about" 요청시
[newsId].js -> "/news/"와 "/news/about"을 제외한 "/news/어떤것" 요청시
,,,
pages 폴더 내 news 서브 폴더에 "[newsId].js"라는 동적 라우팅하는 페이지 컴포넌트가 존재하며 이 외에도 index.js, about.js 페이지 컴포넌트 파일이 존재합니다.
"our-domain.com/news" 요청시 "news/index.html"이 응답되고, "our-domain.com/news/about" 요청시 "news/about.html"이 응답됩니다.
즉, 요청된 경로값이 "/news"와 "/news/about"를 제외한 나머지 다른 모든 경로 요청시에 동적 라우팅하는 페이지가 응답으로 전달됩니다.
동적 라우팅하는 페이지 컴포넌트 내에서 useRouter
훅을 호출하여 반환되는 객체의 "query" 프로퍼티를 통해 경로 파라미터 값을 추출할 수 있습니다.
이때 서버에게 요청하여 전달받은 HTML 문서가 Fallback 버전 HTML 문서인 경우에는 Hydrate 과정에서 실행되는 페이지 컴포넌트 함수는 query 객체가 빈 객체인 상태로 실행된다는 점에 주의해야 합니다.
NextJS 공식 문서를 보면 hydrate가 완료되기 전에는 query 객체가 빈 객체로 존재하게 되며, hydrate가 완료된 이후 NextJS가 qeury 객체에 경로 파라미터 값을 갖도록 하기 위해서 애플리케이션 업데이트를 트리거한다고 작성되어 있습니다.
// our-domain.com/blog/[userId]
,,,
const router = useRouter();
console.log(router.query);
,,,
위와 같은 코드가 존재하는 경우 만약 "/blog/user123"라는 특정 경로로 직접 접근하는 경우 콘솔창은 다음과 같은 결과를 출력합니다.
Hydrate시 실행되는 페이지 컴포넌트 함수의 query에는 빈 객체가 존재하는 상태에서 리렌더링
Hydrate가 끝난 후 qeury에 경로 파라미터에 대한 정보를 갖는 객체가 바인딩되고 NextJS가 페이지 컴포넌트 함수를 재실행하여 리렌더링
NextJS는 경로를 변경하면 그에 맞는 HTML 문서를 새롭게 요청하여 렌더링하지 않고 경로가 변경될 때마다 클라이언트측에서 해당되는 페이지 컴포넌트 함수를 실행하여 화면을 리렌더링합니다.
즉, 경로를 변경하여 페이지에 접근하는 경우 해당 페이지에 맞는 "페이지 컴포넌트 함수를 실행"하여 화면을 리렌더링합니다.
NextJS는 각 페이지마다 코드 스플리팅을 하기 때문에 페이지마다 해당 페이지에서 필요한 것만(페이지 컴포넌트 파일, JSON 파일, 이미지 등)로드하게 됩니다. 따라서 어떤 페이지가 렌더될 때 모든 페이지를 위한 코드가 초기에 전달되지 않습니다.
만약 어떤 페이지가 다른 페이지로 이동하는 Link
컴포넌트나 router 객체의 push
, replace
, back
메서드를 포함하고 있는 경우에는 이동될 페이지와 관련된 것들도 미리 로드하여 가져옵니다.
다른 페이지로 이동하는 링크 클릭한 경우 미리 로드한 페이지 컴포넌트를 실행함으로써 화면을 동적으로 업데이트합니다.
NextJS 또한 SPA로서 동작해야 하기 위해서 새로운 페이지에 대한 요청을 보내지 않고 경로만 변경하여 기존 상태를 보존하면서 사용자에게도 더 나은 UX를 제공해주어야 합니다.
이를 위해 NextJS가 자체적으로 갖고 있는 "next/link" 패키지에 존재하는 Link
컴포넌트를 사용합니다.
import Link from 'next/link';
<Link href="/경로값"></Link>
Link
컴포넌트는 a 엘리먼트를 반환하는 컴포넌트입니다. 내부에는 새로운 HTML 파일의 요청을 보내지 않는 로직이 포함되어 있습니다.
Link
컴포넌트에 "href 어트리뷰트" 값으로 변경할 URL 경로값을 작성해줍니다.
이때 NextJS가 갖고 있는 페이지로 이동하고자 한다면 서버측 루트 경로로 시작하는 해당 페이지 경로를 작성해줍니다.
import Link from 'next/link';
<Link href="/페이지이름"></Link>
href 어트리뷰트에 이동할 경로값을 문자열로 작성하는 대신에 객체를 전달할 수도 있습니다. 이때 객체의 구조는 아래와 같습니다.
import Link from 'next/link';
<Link
href={
{
pathname: '/경로/[경로파라미터]',
query: { 경로파라미터: '경로값' }
}
}></Link>
주의할 점으로 동적 라우팅되는 경로를 작성할 때 pathname 프로퍼티 값에는 구체적인 경로 파라미터 값을 작성하지 않고 경로 파라미터 부분에는 그대로 "[경로파라미터]"형식으로 작성하고, 구체적인 경로 파라미터 값은 query 객체의 프로퍼티로 작성해줍니다.
"replace 어트리뷰트"을 작성하는 경우 History stack에 push하지 않고 replace를 하게 됩니다. 불린 속성으로 따로 값을 지정하지 않아도 됩니다.
import Link from 'next/link';
<Link href="/경로값" replace></Link>
만약 Link
컴포넌트에게 className 어트리뷰트와 같은 html 어트리뷰트를 작성하기 위해서는 Link
컴포넌트의 Content 영역에 a 엘리먼트를 포함시켜 작성해줍니다. 그리고 a 엘리먼트에 html 어트리뷰트를 작성하여 설정할 수 있습니다.
Link
컴포넌트는 자체적으로 a 태그로 돔에 렌더링됩니다. 하지만 Content 영역에 명시적으로 a 엘리먼트가 작성된 경우 Link
컴포넌트가 Content 영역에 작성된 a 엘리먼트를 인식하여 자체적으로 a 엘리먼트를 렌더링하는 대신에 Content 영역에 작성된 a 엘리먼트를 렌더링합니다.
주의할 점으로 a 엘리먼트에게 href를 작성하는 것이 아니라 반드시 Link
컴포넌트에게 href를 작성해주어야 합니다. 그러면 Link 컴포넌트가 기존처럼 새로운 요청을 보내지 않도록 기본적인 동작을 막아줍니다.
import Link from 'next/link';
<Link href="/경로">
<a className="클래스"></a>
</Link>
next/link의 Link
컴포넌트를 통한 경로를 변경하는 방법 말고 직접 코드를 작성하여 경로를 변경할 수 있습니다.
NextJS에서는 자체적으로 갖고 있는 "next/router" 패키지의 useRouter
커스텀 훅을 사용합니다.
경로를 변경하기 위해서는 useRouter
훅이 반환한 객체를 통해 push
, replace
, back
메서드를 호출하는 방식으로 가능합니다.
메서드 호출시 인수로 이동할 경로를 문자열로 작성하는대신 객체를 전달할 수도 있습니다. 객체의 구조는 Link
컴포넌트의 href 어트리뷰트에 작성된 객체와 동일합니다.
이동할 경로를 작성할 때 NextJS가 갖고 있는 페이지로 이동하고자 한다면 서버측 루트 경로를 시작으로 해당 페이지 경로를 작성해주어야 합니다.
router.push("/이동할 경로" | 객체)
: 인수로 전달한 경로로 이동하며 URL 정보가 History 스택에 쌓이게 됩니다.
router.replace("/이동할 경로" | 객체)
: 인수로 전달한 경로로 이동하며 URL 정보가 History 스택에 쌓이지 않게 됩니다.
router.back()
: 직전 페이지로 이동합니다.
push와 replace 메서드 둘 다 인수로 전달한 경로로 변경해줍니다. 이때 두 메서드의 차이점은 History stack의 동작에 있습니다.
push 메서드의 경우 인수로 전달한 경로로 변경을 하고 변경한 경로의 정보를 History stack에 push 합니다. 이 경우 뒤로가기 버튼을 눌렀을 때 직전 경로로 이동이 가능합니다.
정리하자면 push와 replace 둘 다 경로를 변경하는 동작을 하지만, push의 경우 변경할 URL 정보가 History stack에 쌓이게되고, replace의 경우 변경할 URL 정보가 History Stack에 쌓이지 않고 교체되는는 차이점이 존재합니다.
Nextjs는 기본적으로 존재하지 않는 경로 요청시 404 에러 페이지를 요청에 대한 응답으로 전달해줍니다. 이는 빌드시 NextJS가 자동으로 생성해주는 페이지입니다.
만약 404 에러 페이지를 커스텀하고 싶다면 pages 폴더에 명시적으로 "404.js"라는 이름으로 페이지 컴포넌트를 생성해야 합니다.
404.js 파일이 존재한다면 해당 페이지를 화면에 렌더링하고 404.js가 없다면 nextjs가 제공하는 404 페이지를 화면에 렌더링합니다.