Next.js App Router · 선택 확장
React 이후 서버·클라이언트 경계와 동적 상세·404를 실행하며 App Router 확장하기
선택 확장으로 읽는 이유
React·Hooks·Router·TypeScript와 최종 품질 검사를 마친 뒤, 서버 렌더링과 서버 데이터 접근이 필요한 프로젝트에서 진행합니다. 기본 10주 과정의 필수 프레임워크 전환이 아닙니다. 아래 기존 개념 설명과 별도의 실행 실습을 구분해 읽습니다.
App Router에서 달라지는 것
- 파일 규칙으로 정적·동적 경로와 상태 UI를 구성한다
- Server Component와 Client Component 경계를 선택한다
- 데이터 최신성에 맞는 캐시·갱신·오류 전략을 세운다
서버와 클라이언트 경계
프로젝트 만들기
공식 생성 도구로 App Router와 TypeScript 프로젝트를 시작합니다.
pnpm create next-app@latest my-appcd my-apppnpm dev파일 기반 라우팅
app 아래 폴더가 URL 구간이 되고 page.tsx가 해당 경로의 화면이 됩니다. 공통 UI는 layout.tsx, 로딩은 loading.tsx, 예상하지 못한 오류는 error.tsx로 다룹니다.
app/ layout.tsx page.tsx sessions/ page.tsx [id]/page.tsxServer Component 우선
App Router의 컴포넌트는 기본적으로 서버에서 실행됩니다. 브라우저 이벤트, state, Effect가 필요한 경계에만 "use client"를 추가해 전송되는 JavaScript를 줄입니다.
데이터 가져오기
서버 컴포넌트에서 데이터 소스에 직접 접근하고 독립적인 요청은 병렬로 시작합니다. 비밀 키는 클라이언트 코드에 넣지 않습니다.
// 구조 설명용 조각입니다. getSessions와 SessionList는 프로젝트에서 정의해야 합니다.export default async function SessionsPage() { const sessions = await getSessions() return <SessionList sessions={sessions} />}동적 경로와 params
동적 구간의 params는 현재 Next.js에서 비동기 값입니다. 존재하지 않는 리소스는 notFound()로 명확히 처리합니다.
캐시와 갱신
데이터가 언제 바뀌고 얼마나 오래되어도 되는지 먼저 결정합니다. 모든 요청을 무조건 캐시하거나 매번 새로 가져오지 말고 데이터 성격에 맞게 캐시 수명과 갱신 시점을 선택합니다.
쓰기 작업과 인증 경계
Server Action이나 Route Handler는 공개된 서버 진입점입니다. 폼 UI에서 이미 검사했더라도 서버에서 입력, 로그인 상태, 객체 권한을 다시 확인하고 성공 후 영향을 받는 데이터만 갱신합니다.
| 확인 | 실패했을 때 |
|---|---|
| 입력 스키마와 허용 값 | 필드 오류가 있는 400 수준 응답 |
| 로그인 상태 | 로그인 경로 또는 401 |
| 리소스 소유권·역할 | 403 또는 존재를 숨기는 404 |
| 중복·현재 상태 | 409와 복구 가능한 설명 |
Client Component에 숨긴 버튼과 middleware 경로 검사만으로 서버 작업의 권한이 보장되지는 않습니다.
메타데이터와 이미지
페이지 제목과 설명은 Metadata API로 제공하고 콘텐츠 이미지는 next/image로 크기를 명시합니다. 외부 이미지 도메인은 설정에서 허용 범위를 제한합니다.
App Router 경계 지도
| 관심사 | 기본 위치 | 브라우저 코드가 필요한 조건 |
|---|---|---|
| 데이터 조회 | Server Component | 실시간 사용자 입력으로 반복 조회 |
| 비밀키 사용 | 서버 전용 모듈 | 클라이언트에 절대 전달하지 않음 |
| 클릭·입력 state | Client Component | useState, 이벤트 핸들러 필요 |
| 페이지 메타데이터 | metadata 또는 generateMetadata | 브라우저 Effect로 설정하지 않음 |
| 쓰기 작업 | Server Action 또는 Route Handler | 외부 클라이언트 API면 Route Handler |
특수 파일 빠른 참조
| 파일 | 역할 | 확인할 상태 |
|---|---|---|
page.tsx | 경로의 화면 | 정상 데이터 |
layout.tsx | 하위 경로 공통 UI | 중첩과 상태 유지 |
loading.tsx | 스트리밍 대기 UI | 레이아웃 점프 |
error.tsx | 예상하지 못한 오류 | 재시도와 로깅 |
not-found.tsx | 존재하지 않는 리소스 | 돌아갈 경로 |
route.ts | HTTP 엔드포인트 | 인증·검증·상태 코드 |
참고: "use client"는 파일의 모든 하위 import를 클라이언트 번들 경계로 가져옵니다. 상호작용이 필요한 가장 작은 컴포넌트에 둡니다.데이터 갱신 질문
- 이 데이터는 사용자마다 다른가?
- 얼마나 오래되어도 되는가?
- 어떤 쓰기 작업 뒤 즉시 바뀌어야 하는가?
- 정적 생성 시 알 수 있는 경로인가?
- 실패하면 이전 데이터와 오류 중 무엇을 보여줄 것인가?
배포 전 확인 항목
- 서버 전용 환경변수에
NEXT_PUBLIC_접두사를 붙이지 않았습니다. - 동적 params와 searchParams를 비동기로 처리합니다.
- 이미지 크기와 원격 호스트가 설정되어 있습니다.
- 로딩, 오류, 404 경로를 직접 열어 확인했습니다.
App Router 확장 예제
1. 경로 설계
app/├─ layout.tsx├─ page.tsx├─ loading.tsx├─ error.tsx└─ sessions/ ├─ page.tsx └─ [id]/ ├─ page.tsx └─ not-found.tsx2. 서버에서 데이터 읽기
세션 목록과 상세는 Server Component에서 가져옵니다. 검색 입력처럼 브라우저 이벤트가 필요한 작은 경계만 Client Component로 만듭니다.
3. 스트리밍과 오류
느린 상세 정보를 Suspense 경계로 분리하고 로딩 스켈레톤을 제공합니다. 없는 ID는 notFound(), 예상하지 못한 오류는 error.tsx에서 복구 경로와 함께 처리합니다.
4. 쓰기와 갱신
신청 폼의 입력을 서버 경계에서 다시 검증하고 성공 후 영향을 받는 목록과 상세 캐시를 갱신합니다. 중복 제출과 권한 검사를 포함합니다.
5. 메타데이터
세션 제목과 설명으로 동적 metadata를 만들고 공유 URL의 의미가 드러나는지 확인합니다.
실행 실습: 목록과 동적 상세 경로
별도의 새 프로젝트에서 npx create-next-app@latest lion-next를 실행하고 TypeScript·App Router를 선택합니다. 아래 경로는 src 디렉터리를 사용하지 않은 경우입니다. src를 선택했다면 app과 lib를 src 아래에 둡니다. 생성된 layout.tsx와 전역 CSS는 유지합니다.
lib/members.ts를 만듭니다. 이번 데이터는 가상 고정 배열이며 실제 DB 연동 예제가 아닙니다.
export const members = [ { id: 1, name: '가람', track: 'frontend' }, { id: 2, name: '나래', track: 'design' },];app/members/page.tsx:
import Link from 'next/link';import { members } from '../../lib/members';
export default function MembersPage() { return <main> <h1>회원 명단</h1> <ul>{members.map(member => <li key={member.id}><Link href={'/members/' + member.id}>{member.name}</Link></li> )}</ul> </main>;}app/members/[id]/page.tsx:
import Link from 'next/link';import { notFound } from 'next/navigation';import { members } from '../../../lib/members';import NoteToggle from './note-toggle';
export default async function MemberPage({ params }: { params: Promise<{ id: string }>;}) { const { id } = await params; if (!/^[1-9][0-9]*$/.test(id)) notFound(); const member = members.find(item => item.id === Number(id)); if (!member) notFound(); return <main> <h1>{member.name}</h1> <p>{member.track}</p> <NoteToggle /> <Link href="/members">명단으로</Link> </main>;}app/members/[id]/note-toggle.tsx:
'use client';
import { useState } from 'react';
export default function NoteToggle() { const [open, setOpen] = useState(false); return <section> <button type="button" aria-expanded={open} aria-controls="member-note" onClick={() => setOpen(value => !value)}>학습 안내 {open ? '접기' : '보기'}</button> <p id="member-note" hidden={!open}>가상 회원으로 경로와 상호작용을 연습합니다.</p> </section>;}app/members/[id]/not-found.tsx:
import Link from 'next/link';
export default function NotFound() { return <main><h1>회원을 찾을 수 없습니다.</h1><Link href="/members">명단으로</Link></main>;}npm run dev를 실행하고 출력된 주소의 /members, /members/1, /members/999, /members/abc를 확인합니다. 목록·상세는 Server Component이고 버튼 상호작용만 Client Component입니다. Client Component도 최초 화면에서는 서버에서 HTML로 미리 렌더링될 수 있으므로 ‘브라우저에서만 실행’이라고 단순화하지 않습니다.
권장 100분: 경로 25분, 서버·클라이언트 경계 25분, 404·직접 진입 25분, 빌드·설명 25분입니다. npm run build로 검사하고 성공 뒤 npm start로 빌드 결과를 확인합니다. 느린 실제 데이터가 아직 없으므로 loading·cache 동작을 이 고정 배열만으로 검증했다고 주장하지 않습니다. 다음 확장에서는 공식 데이터 조회 문서를 따라 실제 소스를 연결하고 새로고침·실패·갱신 시나리오를 추가합니다.
완료 기준은 직접 URL 진입과 없는 ID 복구가 동작하고 서버 코드와 브라우저 상호작용의 위치를 설명하는 것입니다.
더 읽어보기
다음 단계
프론트엔드 품질과 배포에서 핵심 흐름을 테스트하고 접근성·성능·운영 환경을 최종 점검합니다.