페이지 생성
app 폴더에 page.tsx를 배치해 About과 대시보드 페이지를 만들고 중첩 경로와 Link 이동을 확인합니다.
App Router의 기본 구조를 이해했으니, 이제 실제로 페이지를 만들어 브라우저에서 확인해 봅니다. 이 글에서는 새 폴더와 page.tsx가 URL을 만드는 규칙, 중첩 라우트(Nested Routes), 그리고 <Link>로 이동하는 방법을 차례로 안내합니다.
앞 장에서 고른 src/app 구조와 루트 레이아웃을 씁니다. 앞 글의 API용 dashboard/page.tsx를 이미 작성했다면 이 글의 DashboardHomePage 코드로 그 파일을 바꾸며, 한 파일에 default export를 두 개 붙이지 않습니다. 아래의 처음 세 페이지는 별도의 목 API 없이 확인할 수 있습니다.
첫 번째 새 페이지 만들기: about 페이지
가장 기본적인 형태인 About 페이지를 만들어 봅시다. 먼저 프로젝트 루트의 src/app 폴더로 이동합니다. 이곳이 모든 라우트와 페이지가 시작되는 곳입니다. src/app 안에 about이라는 새 폴더를 만들고, 그 안에 page.tsx 파일을 만듭니다.
src/app/about/page.tsx를 열고 다음 코드를 작성합니다.
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>
이 페이지는 '나 혼자 Next.js' 교재의 예시 프로젝트입니다.
</p>
<p>Next.js 16과 함께 웹 개발을 학습하고 있습니다.</p>
</div>
);
}이 파일은 기본적인 React 컴포넌트이며, 특별한 지시어가 없으므로 기본으로 서버 컴포넌트로 동작합니다. 개발 서버가 실행 중이 아니라면 프로젝트 루트에서 npm run dev(또는 yarn dev)를 실행하고, 터미널에 나온 개발 서버의 실제 주소 뒤에 /about을 붙여 들어가세요. 이 글의 localhost:3000은 기본 포트를 쓰는 경우의 예입니다. 방금 만든 About Us 페이지가 렌더링되면 성공입니다. App Router에서는 폴더 이름이 URL 경로가 되고, 그 폴더 안의 page.tsx가 그 경로의 실제 UI를 맡는다는 점이 핵심입니다.
중첩 라우트(Nested Routes) 만들기
이제 조금 더 복잡한 페이지 구조로, 대시보드(Dashboard) 안에 설정(Settings) 페이지가 있는 형태를 만들어 봅니다. 먼저 src/app 안에 dashboard 폴더를 만들고, 그 안에 page.tsx를 만들어 다음 내용을 작성합니다. 이 파일은 /dashboard 경로의 페이지가 됩니다.
export default function DashboardHomePage() {
return (
<div>
<h1>Welcome to Dashboard!</h1>
<p>여기는 대시보드의 홈 페이지입니다.</p>
</div>
);
}http://localhost:3000/dashboard로 들어가 확인해 보세요. 이어서 dashboard 폴더 안에 다시 settings 폴더를 만들고, 그 안에 page.tsx를 만들어 다음 내용을 작성합니다. 이 파일은 /dashboard/settings 경로의 페이지가 됩니다.
export default function DashboardSettingsPage() {
return (
<div>
<h2>Dashboard Settings</h2>
<p>대시보드 설정을 관리하는 페이지입니다.</p>
</div>
);
}여기까지 만들면 폴더 구조는 다음과 같습니다.
http://localhost:3000/dashboard/settings에 들어가 방금 만든 중첩 페이지가 잘 렌더링되는지 확인합니다. 폴더 안에 폴더를 만들고 그 안에 page.tsx를 두는 방식으로 URL 경로가 계속 중첩되며, 파일 시스템의 구조와 URL 경로가 일치하는 직관적인 방식입니다.
src/app/dashboard/settings/page.tsx가 /dashboard/settings와 이어지는 과정을 하나씩 보면, src/app은 앱의 기준 디렉터리라 URL에 들어가지 않고, 일반 폴더인 dashboard와 settings가 경로의 두 부분을 만들며, 마지막의 page.tsx가 그 위치의 페이지 UI를 제공합니다. 부모의 dashboard/page.tsx는 /dashboard의 페이지일 뿐 settings 페이지를 감싸는 역할이 아니므로, 부모의 페이지 파일이 없어도 settings의 페이지 파일과 필요한 루트 레이아웃만 있으면 /dashboard/settings는 따로 존재할 수 있습니다.
NEXT · CH3-2
page.tsx의 위치가 페이지 URL을 정합니다
아래 경로는 src/app 기준입니다. 일반 폴더는 URL 세그먼트가 되고 page.tsx가 페이지 UI를 공개합니다. 마지막 행은 다음 장의 동적 경로 예고입니다.
폴더는 세그먼트가 되고, page.tsx가 해당 위치의 페이지 UI를 공개합니다.
| 페이지 | src/app 아래 파일 | 이동할 URL |
|---|---|---|
| 소개 | about/page.tsx | /about |
| 대시보드 홈 | dashboard/page.tsx | /dashboard |
| 대시보드 설정 | dashboard/settings/page.tsx | /dashboard/settings |
| 게시물 상세 예고 | blog/[slug]/page.tsx | /blog/my-first-post 등 실제 slug를 넣은 경로 |
- 소개
- src/app 아래 파일: about/page.tsx이동할 URL: /about
- 대시보드 홈
- src/app 아래 파일: dashboard/page.tsx이동할 URL: /dashboard
- 대시보드 설정
- src/app 아래 파일: dashboard/settings/page.tsx이동할 URL: /dashboard/settings
- 게시물 상세 예고
- src/app 아래 파일: blog/[slug]/page.tsx이동할 URL: /blog/my-first-post 등 실제 slug를 넣은 경로
앞 세 행은 이 절에서 작성한 파일입니다. 마지막 행은 동적 경로의 예고이며, 링크만 추가해도 없는 page.tsx가 생성되는 것은 아닙니다.
위 그림은 이 글에서 만든 페이지 파일과 URL의 대응을 보여 줍니다. 그림의 요약처럼 경로는 src/app을 기준으로 하며, 일반 폴더는 URL의 세그먼트가 되고 page.tsx가 페이지의 UI를 공개합니다. about/page.tsx는 /about, dashboard/page.tsx는 /dashboard, dashboard/settings/page.tsx는 /dashboard/settings가 되며, 세 대응은 서로 독립되어 있고 URL에서 src/app과 page.tsx는 빠집니다. 그림 아래 표의 마지막 행은 다음 장에서 다룰 동적 경로의 예고로, blog/[slug]/page.tsx가 /blog/my-first-post처럼 실제 slug를 넣은 경로를 처리한다는 뜻입니다. 그림의 마지막 설명처럼 앞의 세 행은 이 글에서 작성한 파일이고, 링크만 더한다고 없는 page.tsx가 생기지는 않습니다.
페이지 사이의 이동: <Link> 컴포넌트
웹 애플리케이션에서는 페이지 사이를 이동하는 기능이 꼭 필요합니다. Next.js는 페이지 사이의 클라이언트 측 이동을 위해 <Link> 컴포넌트를 제공하며, 일반 HTML의 <a> 태그보다 몇 가지 장점이 있습니다. <Link>는 페이지 전체를 새로고침하지 않고 JavaScript로 페이지를 부드럽게 바꿉니다. 또 프로덕션에서는 뷰포트에 들어온 <Link>의 대상을 정책에 따라 미리 가져올(prefetch) 수 있지만, 자동 프리페치는 개발 서버에서는 실행되지 않으므로 이 글의 npm run dev 실습에서는 클릭했을 때의 이동과 URL의 대응을 확인합니다.
이제 src/app/page.tsx(루트 페이지)에 다른 페이지로 이동하는 링크를 더해 봅니다. 파일을 열고 다음과 같이 고칩니다.
import Link from 'next/link'; // Link 컴포넌트 임포트
export default function HomePage() {
return (
<div>
<h1>Welcome to '나 혼자 Next.js'!</h1>
<p>Next.js 16 App Router와 함께 웹 개발을 시작합니다.</p>
<nav style={{ marginTop: '20px' }}>
<ul>
<li>
<Link href="/about">
About 페이지로 이동
</Link>
</li>
<li>
<Link href="/dashboard">
Dashboard 홈으로 이동
</Link>
</li>
<li>
<Link href="/dashboard/settings">
Dashboard 설정 페이지로 이동
</Link>
</li>
</ul>
</nav>
</div>
);
}Next.js 13부터 <Link>가 스스로 <a> 요소를 렌더링하므로, <Link><a>...</a></Link>처럼 <a>를 중첩하면 안 됩니다. 텍스트나 React 요소를 바로 자식으로 넘기고, 스타일 클래스와 링크 속성은 <Link>에 지정합니다.
http://localhost:3000에 들어가 링크가 잘 보이는지 확인하고, 각 링크를 눌러 페이지가 부드럽게 바뀌는지 시험해 보세요. 브라우저가 새로고침되지 않고 내용만 바뀌는 것을 볼 수 있습니다.
동적 라우트 미리 보기
지금까지는 정적인 경로의 페이지만 만들었습니다. 그렇다면 게시물 상세 페이지(/posts/1)나 사용자 프로필 페이지(/users/john-doe)처럼 URL의 일부가 바뀌는 페이지는 어떻게 만들까요? Next.js의 App Router는 대괄호([])로 동적 라우트 세그먼트를 정의합니다. 예를 들어 src/app/blog/[slug]/page.tsx를 만들면 /blog/my-first-post, /blog/nextjs-basics 같은 경로를 처리할 수 있습니다.
[slug]의 값은 페이지 컴포넌트의 params prop으로 전달되며, Next.js 16의 params는 Promise이므로 서버 컴포넌트에서는 await로 값을 읽습니다. 동적 라우트의 매개변수를 읽는 코드는 다음 장에서 자세히 다룹니다.
정리
src/app안에 폴더를 만들고 그 안에page.tsx를 두면 그 폴더 이름의 URL이 페이지가 됩니다.- 폴더 안에 폴더를 만들면 URL도
/dashboard/settings처럼 중첩되며, 부모 폴더에page.tsx가 없어도 하위 페이지는 존재할 수 있습니다. <Link>는 새로고침 없이 클라이언트 측에서 이동하고, 프로덕션에서는 뷰포트에 들어온 대상을 미리 가져올 수 있으며, 자식으로<a>를 중첩하지 않습니다.- 대괄호 폴더(
[slug])는 동적 세그먼트를 만들고, 그 값은 Promise인params로 전달됩니다.
연습 문제
1. 폴더, page, layout을 구별하기
지금 /dashboard/settings가 잘 보입니다. 루트 레이아웃과 settings/page.tsx는 그대로 두고 dashboard/page.tsx만 지운 뒤, 빈 dashboard/profile 폴더에는 layout.tsx만 두었습니다. 이 범위에는 Route Handler나 다른 공개 페이지가 없다고 가정합니다. /dashboard, /dashboard/settings, /dashboard/profile 가운데 어떤 URL에 페이지가 남을까요? profile 경로를 Link의 href에 쓰면 페이지가 생길까요?
해설 보기
/dashboard/settings 페이지만 남습니다. /dashboard 자체의 page가 없어도 중첩 페이지의 URL은 사라지지 않습니다. profile에는 page가 없으므로 layout만으로는 그 URL의 고유한 페이지가 만들어지지 않습니다.
Link는 이미 있는 경로로 이동할 URL을 지정할 뿐 파일을 만들지 않습니다. 폴더는 세그먼트, page는 그 위치의 공개 UI, layout은 하위 UI를 감싸는 역할입니다.
2. Link 안의 a 태그
내비게이션 링크에 스타일 클래스를 주려고, 인터넷에서 찾은 예제를 그대로 옮겨 왔습니다. 다음 코드를 Next.js 16에서 쓰면 어떤 문제가 생길까요?
<Link href="/about">
<a className="nav-link">About</a>
</Link>해설 보기
Next.js 13부터 <Link>가 스스로 <a> 요소를 렌더링하므로, 안에 <a>를 또 넣으면 <a> 안에 <a>가 중첩되어 잘못된 HTML이 됩니다. className은 <Link>에 지정하고 텍스트를 바로 자식으로 넘겨 <Link href="/about" className="nav-link">About</Link>처럼 씁니다.
3. 개발 서버에서의 프리페치
npm run dev로 실행한 상태에서 루트 페이지를 열고 Network 탭을 보았더니, 링크가 화면에 보이기만 해서는 대상 페이지를 미리 가져오는 요청이 보이지 않았습니다. 프리페치가 고장 난 것일까요?
해설 보기
고장 난 것이 아닙니다. <Link>의 자동 프리페치는 프로덕션에서만 동작하고 개발 서버에서는 실행되지 않습니다. 프리페치를 확인하려면 npm run build로 빌드한 뒤 npm run start로 프로덕션 서버를 띄워 다시 살펴봅니다.
다음 글 레이아웃 컴포넌트에서는 여러 페이지가 함께 쓰는 UI를 layout.tsx로 만들고 중첩하는 방법을 살펴봅니다.