넥스트JS

← 목록
2 / 2

Next.js 16: SSR 컴포넌트 분리 — 데이터 패치 원칙 실습

대상 앞 편(SSR과 CSR 상태 관리 비교)을 완료하고, Next.js 16 App Router, TypeScript, Server/Client 컴포넌트, searchParams await, Link를 이미 아는 학생

목표 SSR 페이지를 CategoryListCourseList 두 컴포넌트로 분리하며, "데이터는 자식이 스스로 패치하는 것이 기본"이라는 원칙과 부모-자식 결합도 낮추는 설계 철학을 실습으로 체득한다

완성물 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-1. app/step2/ssr/ 폴더 생성

    터미널에서 다음 명령을 실행합니다:

    mkdir -p app/step2/ssr

    Windows 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.tsx

    import 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. 구조 식별

    위 코드를 보고 두 영역을 구분해 보세요.

    1. 좌측 사이드바 영역: categories 배열을 map하여 Link 리스트를 렌더링하는 부분.
    2. 우측 메인 영역: 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를 "카테고리 목록"과 "코스 목록 + 페이지네이션" 두 영역으로 인식했습니다.

이어지는 내용은 회원만 볼 수 있어요

뉴렉처에 로그인하면 전체 학습 자료를 열람할 수 있습니다.

로그인하기
Next.js 16: SSR 컴포넌트 분리 — 데이터 패치 원칙 실습 - 넥스트JS 따라하기 | 뉴렉처