Next.js App Router · 선택 확장

React 이후 서버·클라이언트 경계와 동적 상세·404를 실행하며 App Router 확장하기

선택 확장으로 읽는 이유

React·Hooks·Router·TypeScript와 최종 품질 검사를 마친 뒤, 서버 렌더링과 서버 데이터 접근이 필요한 프로젝트에서 진행합니다. 기본 10주 과정의 필수 프레임워크 전환이 아닙니다. 아래 기존 개념 설명과 별도의 실행 실습을 구분해 읽습니다.

App Router에서 달라지는 것

  • 파일 규칙으로 정적·동적 경로와 상태 UI를 구성한다
  • Server Component와 Client Component 경계를 선택한다
  • 데이터 최신성에 맞는 캐시·갱신·오류 전략을 세운다

서버와 클라이언트 경계

프로젝트 만들기

공식 생성 도구로 App Router와 TypeScript 프로젝트를 시작합니다.

Shell
pnpm create next-app@latest my-appcd my-apppnpm dev

파일 기반 라우팅

app 아래 폴더가 URL 구간이 되고 page.tsx가 해당 경로의 화면이 됩니다. 공통 UI는 layout.tsx, 로딩은 loading.tsx, 예상하지 못한 오류는 error.tsx로 다룹니다.

Plain text
app/  layout.tsx  page.tsx  sessions/    page.tsx    [id]/page.tsx

Server Component 우선

App Router의 컴포넌트는 기본적으로 서버에서 실행됩니다. 브라우저 이벤트, state, Effect가 필요한 경계에만 "use client"를 추가해 전송되는 JavaScript를 줄입니다.

데이터 가져오기

서버 컴포넌트에서 데이터 소스에 직접 접근하고 독립적인 요청은 병렬로 시작합니다. 비밀 키는 클라이언트 코드에 넣지 않습니다.

TypeScript
// 구조 설명용 조각입니다. 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실시간 사용자 입력으로 반복 조회
비밀키 사용서버 전용 모듈클라이언트에 절대 전달하지 않음
클릭·입력 stateClient ComponentuseState, 이벤트 핸들러 필요
페이지 메타데이터metadata 또는 generateMetadata브라우저 Effect로 설정하지 않음
쓰기 작업Server Action 또는 Route Handler외부 클라이언트 API면 Route Handler

특수 파일 빠른 참조

파일역할확인할 상태
page.tsx경로의 화면정상 데이터
layout.tsx하위 경로 공통 UI중첩과 상태 유지
loading.tsx스트리밍 대기 UI레이아웃 점프
error.tsx예상하지 못한 오류재시도와 로깅
not-found.tsx존재하지 않는 리소스돌아갈 경로
route.tsHTTP 엔드포인트인증·검증·상태 코드
참고: "use client"는 파일의 모든 하위 import를 클라이언트 번들 경계로 가져옵니다. 상호작용이 필요한 가장 작은 컴포넌트에 둡니다.

데이터 갱신 질문

  1. 이 데이터는 사용자마다 다른가?
  2. 얼마나 오래되어도 되는가?
  3. 어떤 쓰기 작업 뒤 즉시 바뀌어야 하는가?
  4. 정적 생성 시 알 수 있는 경로인가?
  5. 실패하면 이전 데이터와 오류 중 무엇을 보여줄 것인가?

배포 전 확인 항목

  • 서버 전용 환경변수에 NEXT_PUBLIC_ 접두사를 붙이지 않았습니다.
  • 동적 params와 searchParams를 비동기로 처리합니다.
  • 이미지 크기와 원격 호스트가 설정되어 있습니다.
  • 로딩, 오류, 404 경로를 직접 열어 확인했습니다.

App Router 확장 예제

1. 경로 설계

Plain text
app/├─ layout.tsx├─ page.tsx├─ loading.tsx├─ error.tsx└─ sessions/   ├─ page.tsx   └─ [id]/      ├─ page.tsx      └─ not-found.tsx

2. 서버에서 데이터 읽기

세션 목록과 상세는 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 연동 예제가 아닙니다.

TypeScript
export const members = [  { id: 1, name: '가람', track: 'frontend' },  { id: 2, name: '나래', track: 'design' },];

app/members/page.tsx:

TypeScript
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:

TypeScript
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:

TypeScript
'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:

TypeScript
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 복구가 동작하고 서버 코드와 브라우저 상호작용의 위치를 설명하는 것입니다.

더 읽어보기

다음 단계

프론트엔드 품질과 배포에서 핵심 흐름을 테스트하고 접근성·성능·운영 환경을 최종 점검합니다.