Skip to main content

25장. 컴포넌트로 화면 나누기

[5부] 컴포넌트로 화면 나누기

[5부] 컴포넌트로 화면 나누기

[5부] 컴포넌트로 화면 나누기

화면이 커지기 시작하면, 잘라야 보인다

지난 장에서는 브라우저에 첫 화면을 띄웠다. 서비스 이름이 보이고, 설명 문장이 있고, 버튼도 눌러볼 수 있었다. 여기까지 오면 초보자도 꽤 뿌듯하다. 그런데 바로 다음 단계에서 자주 생기는 일이 있다. 파일 하나에 모든 내용을 계속 넣기 시작하는 것이다.

처음에는 괜찮다. 제목 한 줄, 버튼 두 개, 설명 문장 정도는 한 파일에 있어도 전혀 문제없다. 하지만 화면이 조금만 커져도 상황이 달라진다. 헤더가 생기고, 소개 영역이 생기고, 카드가 생기고, 목록이 생기고, 버튼 모양도 여기저기 반복된다. 그러면 코드는 점점 길어지고, 어디를 고쳐야 하는지 찾는 데 더 많은 시간이 든다.

이럴 때 필요한 것이 컴포넌트다. 컴포넌트는 화면을 작은 조각으로 나누는 방법이다. 어렵게 들릴 수 있지만, 사실은 아주 익숙한 생각이다. 문장을 문단으로 나누고, 서랍을 칸칸이 나누고, 도시락을 반찬통으로 나누는 것과 비슷하다. 한 덩어리로 보면 복잡하지만, 작은 단위로 나누면 훨씬 다루기 쉬워진다.

이 장의 핵심은 하나다. 복잡한 화면을 한 번에 보지 말고, 작은 블록들의 조합으로 보자. 이 감각이 생기면 앞으로 만들 목록 화면, 상세 화면, 폼 화면도 훨씬 덜 무섭게 느껴진다.

컴포넌트는 화면용 레고 블록이다

React와 Next.js에서 컴포넌트는 화면의 일부를 담당하는 함수라고 생각하면 된다. 어떤 컴포넌트는 버튼 하나를 만들고, 어떤 컴포넌트는 카드 하나를 만들고, 어떤 컴포넌트는 페이지의 큰 구역 하나를 맡는다.

중요한 것은 "작게 나누는 것" 자체보다 "의미 있게 나누는 것"이다. 예를 들어 이런 식으로 볼 수 있다.

  • 사이트 위쪽 공통 영역은 SiteHeader

  • 첫 화면 소개 구역은 HeroSection

  • 글 하나를 보여주는 카드는 PostCard

  • 섹션 제목 줄은 SectionTitle

이렇게 이름을 붙이기 시작하면 화면이 갑자기 덜 복잡해진다. 긴 코드 덩어리 대신, 역할이 붙은 부품들이 보이기 때문이다.

예를 들어 HomePage 안에 HTML 태그가 길게 이어져 있으면 처음 보는 사람은 코드를 읽다가 길을 잃기 쉽다. 하지만 HeroSection, FeatureCard, PostCardList 같은 이름이 보이면 화면 구조가 먼저 머릿속에 들어온다. 코드를 읽기 전에 의미가 보이는 것이다.

컴포넌트는 단지 재사용을 위한 기술이 아니다. 생각을 정리하는 도구이기도 하다.

언제 컴포넌트로 나누면 좋을까

초보자는 여기서 두 가지 극단으로 가기 쉽다. 하나는 아무것도 나누지 않고 전부 한 파일에 넣는 방식이고, 다른 하나는 너무 신나서 모든 태그를 컴포넌트로 만드는 방식이다. 둘 다 오래 가면 피곤해진다.

그렇다면 언제 나누면 좋을까. 가장 쉬운 기준은 세 가지다.

  • 같은 모양이나 구조가 반복될 때

  • 이름을 붙일 수 있는 하나의 역할이 있을 때

  • 한 파일이 너무 길어져 읽기 부담스러울 때

예를 들어 "글 카드"가 목록에서 여러 번 반복된다면 PostCard로 빼는 것이 자연스럽다. "로그인 버튼"과 "가입 버튼"이 같은 모양이라면 Button 컴포넌트로 묶을 수도 있다. 반대로 딱 한 번만 쓰이고, 구조도 아주 짧고, 역할도 애매하다면 굳이 분리하지 않아도 된다.

좋은 분리는 코드를 짧게 만드는 것이 아니라, 읽기 쉽게 만든다. 이 기준을 기억하면 과하게 쪼개는 실수를 줄일 수 있다.

첫 화면을 다시 보면 나눌 곳이 보인다

지난 장에서 만든 첫 화면을 다시 떠올려 보자. 대략 이런 구조였다.

헤더가 있고, 본문에는 서비스 소개 문구와 버튼이 있었다. 이 화면은 작아 보이지만 사실 이미 두 개 이상의 덩어리로 나눌 수 있다. 헤더와 소개 영역은 서로 역할이 다르기 때문이다.

처음에는 app/layout.tsx에 헤더를 직접 쓰고, app/page.tsx에 첫 화면 내용을 직접 써도 된다. 하지만 이제부터는 조금 더 서비스답게 키워 갈 것이므로, 공통 영역과 독립적인 화면 조각은 컴포넌트로 빼 두는 편이 좋다.

예를 들어 폴더 구조를 이렇게 잡을 수 있다.

app/
  layout.tsx
  page.tsx
components/
  SiteHeader.tsx
  HeroSection.tsx
  PostCard.tsx

이 구조를 보면 마음이 조금 편해진다. 페이지 파일은 화면을 조립하는 곳이고, components 폴더는 부품을 보관하는 곳이라는 감각이 생기기 때문이다.

공통 헤더를 컴포넌트로 빼기

먼저 여러 화면에서 계속 사용할 헤더를 분리해 보자. 헤더는 앞으로도 자주 살아남을 가능성이 높은 부품이다.

// components/SiteHeader.tsx
import Link from "next/link";

export default function SiteHeader() {
  return (
    <header className="site-header">
      <Link href="/" className="logo">
        Vibe Notes
      </Link>

      <nav className="site-nav">
        <Link href="/login">로그인</Link>
        <Link href="/posts">글 보러 가기</Link>
      </nav>
    </header>
  );
}

그리고 layout.tsx는 이렇게 더 간단해진다.

// app/layout.tsx
import "./globals.css";
import SiteHeader from "@/components/SiteHeader";

export const metadata = {
  title: "Vibe Notes",
  description: "생각을 기록하고 나누는 작은 웹 서비스",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <SiteHeader />
        <main>{children}</main>
      </body>
    </html>
  );
}

이 변화의 장점은 단순하다. layout.tsx는 이제 전체 뼈대에 집중하고, 헤더의 자세한 내용은 SiteHeader.tsx가 맡는다. 파일마다 책임이 조금 더 분명해진다.

코드 길이가 줄어든 것도 좋지만, 더 중요한 것은 읽는 순서가 자연스러워졌다는 점이다. RootLayout을 읽는 사람은 "아, 여기서 헤더를 넣고 본문을 감싸는구나"라고 빠르게 이해할 수 있다.

소개 영역도 하나의 컴포넌트가 된다

첫 화면의 소개 영역도 따로 빼 보자. 아직은 첫 화면에서만 쓰더라도, 역할이 분명한 구역이라면 컴포넌트로 만들 가치가 있다.

// components/HeroSection.tsx
import Link from "next/link";

export default function HeroSection() {
  return (
    <section className="hero-section">
      <p className="eyebrow">내 아이디어를 바로 서비스로</p>
      <h1>AI와 함께 만드는 나만의 첫 웹 서비스</h1>
      <p className="hero-text">
        회원가입, 로그인, 글 작성, 검색까지 직접 만들며
        풀스택 웹 서비스의 흐름을 익혀보세요.
      </p>

      <div className="hero-actions">
        <Link href="/signup" className="primary-button">
          시작하기
        </Link>
        <Link href="/posts" className="secondary-button">
          글 보러 가기
        </Link>
      </div>
    </section>
  );
}

이제 app/page.tsx는 아주 짧아진다.

// app/page.tsx
import HeroSection from "@/components/HeroSection";

export default function HomePage() {
  return <HeroSection />;
}

여기서 초보자는 종종 이렇게 묻는다. "이렇게 짧아지면 너무 단순한 거 아닌가요?" 오히려 좋다. 페이지 파일은 점점 "조립 설명서"처럼 보이면 된다. 여러 부품을 가져와 배치하는 역할을 맡고, 세부 구현은 각 컴포넌트가 맡으면 된다.

재사용의 핵심은 props다

컴포넌트를 진짜 유용하게 만드는 열쇠는 props다. props는 컴포넌트에 건네는 값이라고 생각하면 된다. 같은 틀을 쓰되, 안에 들어가는 내용만 바꾸고 싶을 때 사용한다.

쉬운 비유로 말하면 컴포넌트는 붕어빵 틀이고, props는 그 안에 넣는 팥이나 슈크림 같은 값이다. 틀은 같지만 내용은 바뀔 수 있다.

예를 들어 글 목록에서 사용할 카드 컴포넌트를 만들어 보자.

// components/PostCard.tsx
import Link from "next/link";

type PostCardProps = {
  id: string;
  title: string;
  summary: string;
  author: string;
};

export default function PostCard({
  id,
  title,
  summary,
  author,
}: PostCardProps) {
  return (
    <article className="post-card">
      <h3>{title}</h3>
      <p>{summary}</p>
      <div className="post-card-footer">
        <span>{author}</span>
        <Link href={`/posts/${id}`}>자세히 보기</Link>
      </div>
    </article>
  );
}

이 컴포넌트는 카드의 모양은 유지하면서, 제목과 설명과 작성자만 바꿔서 여러 번 사용할 수 있다. 실제 페이지에서는 이렇게 조립한다.

// app/posts/page.tsx
import PostCard from "@/components/PostCard";

const posts = [
  {
    id: "1",
    title: "오늘 떠오른 서비스 아이디어",
    summary: "출근길에 떠오른 생각을 짧게 정리했다.",
    author: "지민",
  },
  {
    id: "2",
    title: "첫 배포 전에 확인한 것들",
    summary: "작은 서비스라도 공개 전 체크할 항목이 있다.",
    author: "민수",
  },
];

export default function PostsPage() {
  return (
    <section className="post-list-page">
      <h1>최신 글</h1>
      <div className="post-list">
        {posts.map((post) => (
          <PostCard
            key={post.id}
            id={post.id}
            title={post.title}
            summary={post.summary}
            author={post.author}
          />
        ))}
      </div>
    </section>
  );
}

이 장면이 바로 컴포넌트의 힘이 드러나는 순간이다. 목록 화면을 만들 때 카드 HTML을 두 번, 세 번, 열 번 복붙하지 않아도 된다. 카드 모양을 바꾸고 싶다면 PostCard.tsx 한 곳만 고치면 된다.

페이지는 조립하고, 컴포넌트는 맡는다

컴포넌트 구조를 익히면 페이지를 바라보는 방식이 바뀐다. 예전에는 "이 페이지를 어떻게 한 번에 만들지?"라고 생각했다면, 이제는 "이 페이지는 어떤 부품들로 구성되지?"라고 생각하게 된다.

예를 들어 글 목록 페이지는 이렇게 볼 수 있다.

  • 페이지 제목

  • 검색 영역

  • 정렬 버튼

  • 글 카드 목록

  • 빈 상태 메시지

상세 페이지는 또 이렇게 볼 수 있다.

  • 제목 영역

  • 본문 영역

  • 작성자 정보

  • 수정/삭제 버튼

  • 댓글이나 관련 글 영역

이렇게 나누어 보면 갑자기 할 일이 작아진다. 하나의 거대한 화면이 아니라, 여러 개의 익숙한 블록이 되기 때문이다. 초보자가 컴포넌트를 배우고 얻는 가장 큰 이점은 문법 실력이 아니라, 복잡함을 분해하는 시선이다.

너무 잘게 쪼개면 오히려 더 헷갈린다

컴포넌트를 배우면 재미있어서 everything 컴포넌트화하고 싶어진다. 제목 한 줄도 컴포넌트, 문단 하나도 컴포넌트, 버튼 안 아이콘도 바로 컴포넌트로 뺄 수 있다. 물론 큰 프로젝트에서는 그렇게 세밀하게 나누는 경우도 있다. 하지만 지금 단계에서는 과한 분리가 오히려 독이 될 수 있다.

예를 들어 HeroTitle, HeroDescription, HeroButtons처럼 너무 잘게 나누면 파일 수만 늘어나고, 어디에 무엇이 있는지 찾기 어려워질 수 있다. 특히 아직 서비스 구조가 자주 바뀌는 초반에는 적당한 크기로 묶어 두는 편이 더 낫다.

처음에는 "한 컴포넌트가 하나의 명확한 역할을 맡는가?" 정도만 체크하면 충분하다. 헤더, 카드, 폼, 섹션 정도로 나누는 감각이면 좋다. 나중에 정말 필요해질 때 더 잘게 나누면 된다.

코드는 칼로 무를 썰 듯 적당히 잘라야 한다. 다지기 시작하면 요리가 아니라 수습이 된다.

좋은 컴포넌트 이름은 설명을 줄여 준다

이름을 잘 붙이면 코드는 절반쯤 설명된 것이나 마찬가지다. Box, Item, Data, Section2 같은 이름은 보기에는 짧지만, 의미가 거의 없다. 반대로 PostCard, LoginForm, SearchBar, SiteHeader 같은 이름은 역할이 바로 떠오른다.

컴포넌트 이름을 붙일 때는 보통 파스칼 케이스를 쓴다. 즉, 단어 첫 글자를 대문자로 쓰는 방식이다. PostCard, UserMenu, EmptyState 같은 형태다. 이것은 React 컴포넌트를 일반 HTML 태그와 구분하는 데도 도움이 된다.

이름이 애매하면, 그 컴포넌트의 역할도 애매한 경우가 많다. 그럴 때는 코드를 더 쓰기 전에 먼저 "이 부품은 정확히 무슨 일을 하지?"를 스스로 물어보는 편이 좋다.

스타일도 함께 옮겨 다녀야 한다

컴포넌트를 분리했는데도 화면이 어수선해 보인다면, 마크업만 옮기고 스타일 구조는 그대로 뒤엉켜 있는 경우가 많다. 컴포넌트를 나눴다면 스타일도 그 의미에 맞게 정리해 주는 편이 좋다.

예를 들어 globals.css 안에서 클래스 이름을 역할 중심으로 관리할 수 있다.

.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 24px;
  border-bottom: 1px solid #e5e7eb;
  background: white;
}

.hero-section {
  max-width: 760px;
  margin: 0 auto;
  padding: 80px 24px;
}

.post-card {
  padding: 20px;
  border: 1px solid #e5e7eb;
  border-radius: 16px;
  background: white;
}

.post-card-footer {
  display: flex;
  justify-content: space-between;
  margin-top: 16px;
}

아직은 CSS를 복잡하게 설계할 필요는 없다. 다만 클래스 이름이 컴포넌트 역할과 연결되어 있으면, 나중에 코드를 읽을 때 훨씬 덜 헷갈린다. div1, box2 같은 이름보다 post-card, site-header가 훨씬 낫다.

AI에게는 "어디까지 나눌지"도 함께 말해야 한다

Claude Code나 Codex에게 컴포넌트 분리를 부탁할 때 초보자가 자주 하는 요청은 이런 식이다.

"코드를 컴포넌트화해줘."

이 요청도 나쁘진 않지만, 너무 넓다. 그러면 AI가 필요 이상으로 많은 파일을 만들거나, 반대로 거의 안 나누고 끝낼 수도 있다. 더 좋은 요청은 범위와 기준을 함께 주는 것이다.

예를 들면 이렇게 말할 수 있다.

현재 Next.js 첫 화면 코드를 컴포넌트로 나눠줘.
components 폴더를 만들고,
공통 헤더는 SiteHeader,
첫 화면 소개 영역은 HeroSection으로 분리해줘.
지금은 버튼까지 따로 쪼개지 말고,
초보자가 읽기 쉬운 수준으로만 정리해줘.

글 목록 화면을 만들 때는 이렇게 요청할 수도 있다.

글 목록 UI를 재사용 가능한 구조로 바꿔줘.
PostCard 컴포넌트를 만들고,
props로 id, title, summary, author를 받게 해줘.
페이지 파일에서는 map으로 카드 목록을 렌더링해줘.
스타일 클래스 이름도 역할 중심으로 정리해줘.

핵심은 이것이다. AI에게 "쪼개라"가 아니라 "어떤 기준으로, 어느 정도까지 쪼갤지"를 말해 주자. 그래야 결과물이 현재 실력과 프로젝트 규모에 맞는다.

직접 확인해 보면 개념이 더 빨리 붙는다

컴포넌트로 나눈 뒤에는 눈으로 꼭 확인해 보자. 화면은 똑같아 보여도, 개발 경험은 완전히 달라졌을 수 있다.

이럴 때는 다음을 확인하면 좋다.

  • 페이지 파일이 이전보다 짧아졌는가

  • 반복되던 UI가 하나의 컴포넌트로 묶였는가

  • 같은 모양을 고칠 때 한 곳만 수정하면 되는가

  • 컴포넌트 이름만 보고도 역할이 짐작되는가

특히 마지막 항목이 중요하다. 이름만 보고 구조가 읽히기 시작하면, 이미 좋은 방향으로 가고 있는 것이다.

컴포넌트를 이해하면 큰 화면도 덜 무섭다

처음 웹 화면을 만들 때는 모든 것이 한꺼번에 몰려오는 것처럼 느껴진다. 제목도 넣어야 하고, 버튼도 넣어야 하고, 카드도 만들어야 하고, 목록도 보여줘야 한다. 그래서 화면 하나가 거대한 과제로 보인다.

하지만 컴포넌트를 이해하면 시선이 달라진다. 화면은 더 이상 거대한 벽이 아니라, 여러 개의 작은 블록 조합이 된다. 헤더 하나, 카드 하나, 폼 하나, 리스트 하나. 각각은 작고 단순하다. 그리고 그 작은 것들을 차근차근 이어 붙이면, 어느새 서비스 전체가 만들어진다.

이 감각은 앞으로 더 중요해진다. 입력 폼을 만들 때도, 목록과 상세 화면을 만들 때도, 로그인 화면을 붙일 때도 결국은 같은 방식으로 접근하게 된다. 무엇을 한 번에 다 만들지 고민하기보다, 어떤 블록으로 나눌지 먼저 생각하는 것이다.

화면을 나누는 힘은 곧 개발을 계속할 수 있게 만드는 힘이다. 복잡한 것을 작게 보는 눈이 생기면, 초보자도 끝까지 갈 수 있다.

다음 장에서는 이 작은 블록들 안에서 사용자의 입력을 받는 방법을 배운다. 이제 화면은 보이기만 하는 것이 아니라, 사용자가 직접 만질 수 있는 형태로 바뀌기 시작할 것이다.

책 목록

바이브 코딩으로 완성하는 첫 풀스택 웹 서비스

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기