Post

React props를 컴포넌트 API로 설계하기

props를 컴포넌트 API로 설계하고 children·slot·분리 기준을 다루는 가이드

React props를 컴포넌트 API로 설계하기

React에서 props를 “부모가 자식에게 데이터 넘기는 수단” 정도로 이해하면, 금방 재사용성이 무너진다. props는 본질적으로 컴포넌트의 공개 인터페이스(API)다. 즉, 컴포넌트가 외부에 제공하는 사용 방법(계약)이고, 이 계약이 설계를 좌우한다.


props의 역할과 한계

개념

props는 부모가 자식 컴포넌트를 사용할 때 제공하는 입력값이다.

  • 입력(Inputs): 데이터(title, items), 설정(variant, size)
  • 행동(Callbacks): 이벤트/상태 변경을 위임(onChange, onSubmit)
  • 구성(Composition): children/slot을 통한 구조 주입

props는 기본적으로 단방향 데이터 흐름(부모 → 자식)을 강제한다. 이게 React 유지보수성의 핵심이다.

왜 중요한가

props를 API로 보면 설계 판단이 명확해진다.

  • 이 컴포넌트는 “무엇을 책임지나?”
  • 외부가 조절해야 하는 건 무엇인가? (Config)
  • 내부에 숨겨야 하는 건 무엇인가? (Encapsulation)
  • 외부와의 경계는 어디인가? (Contract)

즉 props 설계는 재사용성, 결합도, 변경 비용을 결정한다.

한계 (props로 해결하면 망하는 지점)

  • 너무 많은 props: 옵션 폭발(variant 지옥)
  • 의미가 불명확한 props: boolean flag 남발(isBlue, isLarge, useShadow)
  • 구조까지 props로 고정: UI 확장성이 막힘
  • 깊은 트리에서 props 전달: Prop Drilling → 아키텍처 문제로 전이

재사용 가능한 컴포넌트 vs 재사용하기 어려운 컴포넌트

재사용 가능한 컴포넌트의 특징

  • 역할이 좁고 명확하다 (Single Responsibility)
  • props가 도메인(업무) 용어가 아니라 UI/행동 용어에 가깝다
  • 확장이 필요할 때 children/slot로 열려 있다
  • 상태 소유가 합리적이다(외부 제어 가능하거나 내부 캡슐화)

예: 범용 버튼

1
2
3
4
5
6
7
function Button({ variant = "primary", disabled, onClick, children }) {
  return (
    <button disabled={disabled} onClick={onClick} data-variant={variant}>
      {children}
    </button>
  );
}

재사용하기 어려운 컴포넌트의 특징

  • 도메인 로직이 UI 컴포넌트에 섞여 있다
  • props가 특정 화면/특정 요구사항에 종속된다
  • 확장을 boolean flag로 때운다
  • 외부가 바꾸고 싶은 부분을 바꿀 수 없다 (닫힌 구조)

나쁜 예: 옵션 폭발 + 도메인 결합

1
2
3
4
5
6
7
8
<UserCard
  isAdmin
  showCompany
  showPhone
  highlightIfOverdue
  overdueDays={3}
  onAdminClick={...}
/>

이건 시간이 지나면 “카드”가 아니라 “프로젝트의 모든 요구사항을 빨아먹는 덩어리”가 된다.


children의 본질: “구조를 주입하는 API”

개념

children은 단순히 “자식 요소”가 아니라 부모가 자식 컴포넌트 내부 구조 일부를 결정할 수 있게 하는 구성(Composition) API다.

1
2
3
4
<Card>
  <h2>Title</h2>
  <p>Content</p>
</Card>

왜 중요한가

children을 쓰면 다음이 가능해진다.

  • 컴포넌트가 “틀(컨테이너)” 역할을 하고
  • 내부 콘텐츠/레이아웃의 일부는 외부가 결정한다
  • 결과적으로 재사용성이 올라가고, props 옵션 폭발이 줄어든다

즉, children은 “유연성의 탈출구”다.

언제 문제가 되는가

  • children에 너무 많은 책임을 넘기면 컴포넌트가 껍데기만 남는다(의미 상실)
  • children 안에서 필요한 데이터를 다시 props로 뿌려야 해서 구조가 복잡해질 수 있다
  • 특정 위치에만 들어가야 하는 콘텐츠를 children 하나로는 표현하기 어렵다 → 그래서 slot 패턴이 나온다

컴포넌트 분리 기준: “변경 이유”로 나눠라

개념

컴포넌트를 언제 분리하느냐는 결국 “같이 바뀌는 것끼리 묶고, 따로 바뀌는 것은 분리”로 귀결된다.

왜 중요한가

분리를 잘못하면 둘 중 하나로 망한다.

  • 과도한 분리: 파일 쪼개기만 늘고 추적 비용 증가
  • 부족한 분리: 단일 컴포넌트가 요구사항을 흡수하며 비대화

실무에서 유효한 분리 체크리스트

아래 중 2개 이상이면 분리 후보:

  • UI가 아니라 데이터/비즈니스 규칙이 섞여 있다
  • 같은 패턴이 2번 이상 반복된다
  • props가 늘어나는 속도가 너무 빠르다(옵션 폭발)
  • 재사용하려고 가져가면 항상 “조금씩 뜯어고침”이 필요하다
  • 상태가 여러 종류로 섞여 있고, 업데이트 이유가 다르다

Slot 패턴 사고방식: children의 한계를 “명명된 구멍”으로 해결

개념

children은 한 덩어리 슬롯(기본 슬롯)이다. 하지만 실무 UI는 보통 “여러 위치에 각기 다른 콘텐츠”를 꽂아야 한다.

slot 패턴은 컴포넌트 내부에 명명된 자리(구멍)를 만들고, 부모가 그 자리에 콘텐츠를 주입하게 한다.

왜 중요한가

slot을 쓰면:

  • “옵션 props” 대신 “구조 주입”으로 확장한다
  • 재사용성과 가독성이 올라간다
  • 컴포넌트의 기본 레이아웃은 유지하면서 필요한 부분만 갈아끼운다

구현 방식 1: 명시적 props로 슬롯 제공

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Modal({ header, footer, children, onClose }) {
  return (
    <div role="dialog">
      <div className="header">{header}</div>
      <div className="body">{children}</div>
      <div className="footer">{footer}</div>
      <button onClick={onClose}>닫기</button>
    </div>
  );
}

// 사용
<Modal
  header={<h2>삭제 확인</h2>}
  footer={<button onClick={confirm}>삭제</button>}
  onClose={close}
>
  정말 삭제할까요?
</Modal>

구현 방식 2: Compound Components (슬롯을 컴포넌트로 표현)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Modal({ children }) {
  return <div role="dialog">{children}</div>;
}

Modal.Header = function Header({ children }) {
  return <div className="header">{children}</div>;
};
Modal.Body = function Body({ children }) {
  return <div className="body">{children}</div>;
};
Modal.Footer = function Footer({ children }) {
  return <div className="footer">{children}</div>;
};

// 사용
<Modal>
  <Modal.Header>삭제 확인</Modal.Header>
  <Modal.Body>정말 삭제?</Modal.Body>
  <Modal.Footer>
    <button onClick={confirm}>삭제</button>
  </Modal.Footer>
</Modal>

언제 문제가 되는가

  • slot이 늘어나면 API가 무거워진다(필요한 슬롯만 열어라)
  • Compound 패턴은 자유도가 높은 대신 규칙이 없으면 난잡해진다(사용 규약 필요)
  • slot로 도메인 로직까지 주입하기 시작하면 경계가 무너진다

props 설계가 잘못되었을 때의 유지보수 문제 (현실적인 증상)

1) boolean flag 폭발

처음엔 편하다. 나중엔 props가 요구사항 히스토리 박물관이 된다.

  • 새로운 요구사항 = 새로운 flag
  • flag 조합 = 테스트 지옥
  • UI 분기 = 가독성 붕괴

2) 도메인 결합으로 인한 재사용 불가

UserCard, OrderCard, ProjectCard 같은 이름은 대개 위험하다. 카드가 아니라 “그 화면의 정책”이 들어간다.

대안:

  • UI 컴포넌트는 범용(Card, List, Table)
  • 도메인 결합은 상위 레벨 컨테이너에서 처리(UserCardContainer)

3) props 계약이 불명확해서 변경 비용 증가

  • 어떤 props가 필수인지 불명확
  • 어떤 props 조합이 유효한지 불명확
  • 내부 구현 바꾸면 외부가 깨짐 (캡슐화 실패)

해결 방향:

  • props를 “명확한 타입/의미”로 설계 (필요하면 TypeScript로 계약 강화)
  • 범용 옵션은 제한된 variant로, 확장은 slot/children으로

정리: 좋은 props API는 “옵션이 많아서”가 아니라 “경계가 명확해서” 나온다

  • props는 데이터 전달이 아니라 사용 계약(API)
  • children은 “콘텐츠”가 아니라 구조 주입
  • 분리는 “변경 이유” 기준
  • 확장은 flag 추가가 아니라 slot/컴포지션 우선
  • 나쁜 props 설계는 결국 유지보수 비용으로 터진다

This post is licensed under CC BY 4.0 by the author.