C.W.K.
Stream
Lesson 03 of 07 · published

상속 대신 composition — React 의 방식

~14 min · composition, children, slot-props

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
React 에는 본인이 extend 할 클래스 계층이 없어. 컴포넌트 안에 컴포넌트를 nesting 하고 UI 조각을 prop 이나 children 으로 넘겨서 복잡한 UI 를 지어. 그게 composition. 어떤 클래스 트리보다 멀리 확장돼.

두 가지 composition idiom

React composition 의 거의 모두가 두 패턴으로 환원돼:

  1. children prop — 부모가 frame 렌더 + 자식이 안쪽 채움.
  2. Slot prop — React node 받는 이름 붙은 prop (children 외) — 부모가 특정 자리에 composition.

Slot-prop 레이아웃 (header / body / footer) 가 대시보드, 다이얼로그, 카드에서 보임. children prop 만으로 단순 케이스 커버; slot prop 은 자리가 여러 개 있을 때 이김.

Compound component 패턴

가끔 호출자가 <Tabs><Tab>A</Tab><Tab>B</Tab></Tabs> 처럼 쓰게 하고 싶음. Tabs 부모가 active 상태 소유하고 Tab 자식이 context 로 읽음. 이게 compound component — 부모와 자식이 암묵적 state 계약 공유. 아끼며 써; 라이브러리에 좋음, 앱 코드엔 over-engineered 가능.

asChild / Slot 패턴

Radix UI 가 대중화한 패턴: 스타일 잡힌 <button><Link> 로 cloning 하는 대신, 호출자가 asChild 넘기면 컴포넌트가 자식을 실제 element 로 렌더하고 본인 동작 merge. 같은 Button<Link> 감싸면 진짜 앵커가 됨.

HOC 손이 가면 멈춰. Composition 이 거의 항상 더 깨끗. 2026년에 HOC 가 이기는 경우는 사라질 만큼 드물어 — 보통 라이브러리의 data-fetching 래퍼 (이젠 그들도 hook 씀). 이 트랙의 lesson 6 가 사망 통보.

Code

children — 단순 케이스·tsx
function Card({ children }: { children: React.ReactNode }) {
  return (
    <article className="rounded-card bg-bg-elevated p-6 shadow-sm">
      {children}
    </article>
  );
}

// 호출자가 안쪽 채움:
<Card>
  <h2>Hello</h2>
  <p>This is a card.</p>
</Card>
Slot prop — 이름 붙은 자리·tsx
type DialogProps = {
  title: React.ReactNode;
  body: React.ReactNode;
  actions?: React.ReactNode;
};

function Dialog({ title, body, actions }: DialogProps) {
  return (
    <div role="dialog" className="rounded-card bg-bg-elevated p-6 space-y-4">
      <header className="font-semibold text-fg">{title}</header>
      <div>{body}</div>
      {actions && (
        <footer className="flex justify-end gap-2">{actions}</footer>
      )}
    </div>
  );
}

// 호출자가 이름 붙은 자리에 composition:
<Dialog
  title="Delete conversation?"
  body={<p>This cannot be undone.</p>}
  actions={
    <>
      <button onClick={cancel}>Cancel</button>
      <button onClick={confirm}>Delete</button>
    </>
  }
/>
Compound component — 공유된 암묵 state·tsx
import { createContext, useContext, useState } from "react";

const TabsCtx = createContext<{
  active: string;
  setActive: (id: string) => void;
} | null>(null);

function Tabs({ defaultActive, children }: {
  defaultActive: string;
  children: React.ReactNode;
}) {
  const [active, setActive] = useState(defaultActive);
  return (
    <TabsCtx.Provider value={{ active, setActive }}>
      <div className="flex gap-2 border-b">{children}</div>
    </TabsCtx.Provider>
  );
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const ctx = useContext(TabsCtx);
  if (!ctx) throw new Error("<Tab> 은 <Tabs> 안에 있어야 해");
  const isActive = ctx.active === id;
  return (
    <button
      onClick={() => ctx.setActive(id)}
      className={isActive ? "text-brand border-b-2 border-brand" : "text-muted"}
    >
      {children}
    </button>
  );
}

// 호출자가 둘 같이 사용; state 는 숨겨짐:
<Tabs defaultActive="a">
  <Tab id="a">First</Tab>
  <Tab id="b">Second</Tab>
</Tabs>

External links

Exercise

세 slot prop (header, body, footer) 으로 Modal 컴포넌트 작성. 그 다음 compound-component 패턴으로 Modal2.Header, Modal2.Body, Modal2.Footer 자식 쓰는 형제 Modal2. 둘 다 써서 같은 delete-confirmation UI 렌더. 호출 지점에서 어느 쪽이 더 읽기 자연스러운지, 정의하기 쉬운 쪽은 어디인지 관찰.
Hint
Compound 패턴: Modal2 정의 후 Modal2.Header = function Header(...). 자식들이 context 또는 marker prop 으로 자기 위치 inspect.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.