Next.js 16: SSR 컴포넌트 분리 — 데이터 패치 원칙 실습
대상 앞 편(SSR과 CSR 상태 관리 비교)을 완료하고, Next.js 16 App Router, TypeScript, Server/Client 컴포넌트,
searchParamsawait,Link를 이미 아는 학생목표 SSR 페이지를
CategoryList와CourseList두 컴포넌트로 분리하며, "데이터는 자식이 스스로 패치하는 것이 기본"이라는 원칙과 부모-자식 결합도 낮추는 설계 철학을 실습으로 체득한다완성물
app/step2/ssr/page.tsx,app/step2/ssr/CategoryList.tsx,app/step2/ssr/CourseList.tsx세 파일로 구성된 분리된 SSR 페이지
준비물
- Next.js 16 프로젝트 (App Router, TypeScript 설정 완료)
- Newtil CSS가 설치되어 있어야 합니다. (
npm install newtil-css) app/layout.tsx에 Newtil CSS가 import되어 있어야 합니다.- 앞 편에서 만든
app/step1/ssr/page.tsx코드가 참고용으로 존재하면 좋습니다.
STEP 1 / 10`app/step2/ssr/` 폴더 생성 및 `page.tsx` 작성
0개 완료 · 10개 남음
앞 편에서 만든 단일 파일 구조를 새 폴더로 복사하고, 어떤 부분이 "카테고리 목록"이고 어떤 부분이 "코스 목록 + 페이지네이션"인지 식별해 볼게요. 이 단계에서는 아직 코드를 분리하지 않고, 구조를 파악하는 데 집중합니다.
1-1.
app/step2/ssr/폴더 생성터미널에서 다음 명령을 실행합니다:
mkdir -p app/step2/ssrWindows PowerShell에서는
mkdir app\step2\ssr또는New-Item -ItemType Directory -Path "app\step2\ssr"를 사용할 수 있습니다.1-2.
page.tsx파일 생성앞 편
app/step1/ssr/page.tsx의 코드를 그대로 복사하여 새 위치에 파일을 만듭니다. 이 파일은 아직 분리 전 상태입니다.파일:
app/step2/ssr/page.tsximport Link from "next/link"; export default async function SSRPage({ searchParams, }: { searchParams: Promise<{ category?: string; page?: string }>; }) { const params = await searchParams; const category = params.category || "all"; const page = parseInt(params.page || "1", 10); const categories = ["all", "frontend", "backend", "design"]; const items = [ { id: 1, title: "React 기초", category: "frontend" }, { id: 2, title: "Next.js 실습", category: "frontend" }, { id: 3, title: "Node.js 서버", category: "backend" }, { id: 4, title: "UI 디자인 원칙", category: "design" }, { id: 5, title: "TypeScript 입문", category: "frontend" }, ]; const filteredItems = items.filter((item) => (category === "all" ? true : item.category === category)); const itemsPerPage = 3; const totalPages = Math.ceil(filteredItems.length / itemsPerPage); const currentPageItems = filteredItems.slice((page - 1) * itemsPerPage, page * itemsPerPage); return ( <div className="site-body site-body:aside padding:4"> <aside className="site-aside"> <section className="m3-card card:outlined card-padding:self"> <div className="card-header"> <div className="card-titles"> <h2 className="card-subhead">카테고리</h2> </div> </div> <ul className="m3-list list-size:compact"> {categories.map((cat) => { const isActive = category === cat; return ( <li key={cat}> <Link href={`/step2/ssr?category=${cat}&page=1`} className={`list-item${isActive ? " list-active" : ""}`} aria-current={isActive ? "page" : undefined} > <span className="list-content">{cat === "all" ? "전체" : cat}</span> </Link> </li> ); })} </ul> </section> </aside> <main className="site-content"> <h1 className="font-size:heading-md font-weight:bold margin-bottom:4"> {category === "all" ? "전체" : category} 항목 </h1> <ul className="display:flex flex-direction:column gap:2 margin-bottom:6"> {currentPageItems.map((item) => ( <li key={item.id} className="m3-card card:outlined card-padding:self padding:3"> <div className="display:flex justify-content:space-between align-items:center"> <span className="font-weight:medium">{item.title}</span> <span className="m3-badge badge:inline badge-color:neutral font-size:body-xs">{item.category}</span> </div> </li> ))} {currentPageItems.length === 0 && ( <li className="text-align:center padding-y:8 color:text-muted">항목이 없습니다.</li> )} </ul> <div className="display:flex justify-content:center align-items:center gap:2"> {page > 1 ? ( <Link href={`/step2/ssr?category=${category}&page=${page - 1}`} className="m3-btn btn:outlined btn-size:sm"> 이전 </Link> ) : ( <button className="m3-btn btn:outlined btn-size:sm" disabled>이전</button> )} <span className="font-size:body-sm font-weight:medium padding-x:2"> {page} / {totalPages || 1} </span> {page < totalPages ? ( <Link href={`/step2/ssr?category=${category}&page=${page + 1}`} className="m3-btn btn:outlined btn-size:sm"> 다음 </Link> ) : ( <button className="m3-btn btn:outlined btn-size:sm" disabled>다음</button> )} </div> </main> </div> ); }1-3. 구조 식별
위 코드를 보고 두 영역을 구분해 보세요.
- 좌측 사이드바 영역:
categories배열을 map하여Link리스트를 렌더링하는 부분. - 우측 메인 영역:
items배열을 필터링하고 슬라이싱하여currentPageItems를 렌더링하는 부분.
✅ 확인
app/step2/ssr/page.tsx파일이 생성되었습니다.- 브라우저에서
http://localhost:3000/step2/ssr으로 접속하면 앞 편과 동일한 UI가 표시됩니다. - 코드에서
categories.map블록과currentPageItems.map블록의 위치를 눈으로 확인할 수 있습니다.
🎓 newlec 쌤의 코멘트
이제 우리는 이 긴 파일을 두 개의 컴포넌트로 쪼개야 합니다. 하지만 단순히 코드를 잘라내는 것이 아닙니다. "무엇을 props(속성들)로 넘길지"와 "무엇을 자식 내부에 둘지"를 결정하는 설계 과정입니다. 이 단계에서는 아직 분리하지 않고, 어떤 데이터가 부모(
page.tsx)에 있고, 어떤 데이터가 자식에게 필요한지 파악하는 시간을 갖습니다.📝 이 단계의 핵심
app/step2/ssr/폴더에 분리 전 상태의page.tsx를 준비했습니다.- UI를 "카테고리 목록"과 "코스 목록 + 페이지네이션" 두 영역으로 인식했습니다.
- 좌측 사이드바 영역: