ReactComponent Design

Modal & BottomSheet 컴포넌트 원페이지 정리

Modal의 타입/버튼 처리와 BottomSheet의 목록 렌더링, props, 조건부 렌더링, 재사용 설계까지 한 페이지에서 정리합니다.

1. 전체 구조부터 보기

Modal

Modal ├─ 아이콘 ├─ 제목 ├─ 메인 설명 ├─ 서브 설명 ├─ children └─ ButtonGroup ├─ Button └─ Button

BottomSheet

BottomSheet ├─ 닫기 버튼 │ └─ CloseIcon └─ content ├─ title / subText ├─ items 목록 │ └─ CheckIcon + label + rightText └─ Button

2. Modal 컴포넌트 핵심

타입별 CSS 선택

const MODAL_TYPES = {
  alert: styles.alert,
  info: styles.info,
  confirm: styles.confirm,
  error: styles.error,
}

type="error"가 들어오면 styles.error가 선택됩니다. 타입에 따라 색상이나 아이콘 스타일을 다르게 만들기 위한 구조입니다.

타입별 기본 제목

const MODAL_TITLES = {
  alert: '알림',
  info: '안내',
  confirm: '확인',
  error: '오류',
}

사용자가 title을 직접 전달하지 않아도 type에 맞는 기본 제목이 출력됩니다.

ModalIcon

function ModalIcon({ type }) {
  if (type === 'confirm') {
    return <svg>체크 아이콘</svg>
  }

  if (type === 'error') {
    return <svg>X 아이콘</svg>
  }

  if (type === 'info') {
    return <svg>정보 아이콘</svg>
  }

  return <svg>알림 아이콘</svg>
}

조건에 따라 다른 SVG를 반환합니다. 마지막 return은 기본값이며 사실상 alert용 기본 아이콘입니다.

aria-hidden="true"는 아이콘이 장식용일 때 스크린리더가 굳이 읽지 않도록 하는 접근성 처리입니다.

버튼 variant 자동 결정

function getButtonVariant(button, index, total) {
  if (button.variant) return button.variant
  if (total === 1) return 'secondary'

  return index === 0 ? 'outlineLight' : 'secondary'
}

우선순위는 다음과 같습니다.

버튼 1개 [ 확인 ] → secondary 버튼 2개 [ 취소 ] [ 확인 ] ↓ ↓ outline secondary

3. Modal props

export default function Modal({
  type = 'alert',
  title,
  mainText,
  subText,
  children,
  buttons = [{ label: '버튼명' }],
  buttonRatio = '50-50',
  icon,
  className,
  iconClassName,
  contentClassName,
  buttonGroupClassName,
  role = 'dialog',
  ...props
})
prop역할
typealert / info / confirm / error
title모달 제목을 직접 지정
mainText주요 설명
subText보조 설명
children정해진 구조 외의 추가 JSX
buttons하단 버튼 설정 배열
buttonRatio버튼 2개일 때 폭 비율
icon기본 ModalIcon 대신 사용자 지정 아이콘
...props나머지 속성을 최상위 section에 전달

4. ?? 연산자의 실제 사용

modalType

const modalType =
  MODAL_TYPES[type] ?? MODAL_TYPES.alert

존재하지 않는 타입이 들어오면 MODAL_TYPES[type]이 undefined가 되므로 기본 alert 스타일을 사용합니다.

modalTitle

const modalTitle =
  title ?? MODAL_TITLES[type] ?? MODAL_TITLES.alert
우선순위 1. 사용자가 전달한 title 2. type에 해당하는 기본 제목 3. alert 기본 제목

사용자 아이콘 우선

{icon ?? <ModalIcon type={type} />}

사용자 지정 icon이 있으면 그것을 사용하고, 없으면 기본 아이콘을 사용합니다.

5. buttons.filter(Boolean).slice(0, 2)

const buttonItems =
  buttons.filter(Boolean).slice(0, 2)

filter(Boolean)

[
  { label: '취소' },
  null,
  false,
  { label: '확인' }
]

위 배열에서 null, false 같은 falsy 값을 제거합니다.

slice(0, 2)

앞에서부터 최대 2개까지만 사용합니다. 따라서 Modal의 디자인 규칙인 버튼 최대 2개를 코드에서 강제합니다.

6. Modal의 조건부 렌더링

{modalTitle && <strong>{modalTitle}</strong>}
{mainText && <p>{mainText}</p>}
{subText && <p>{subText}</p>}
{children}

값 && JSX는 값이 존재할 때만 해당 JSX를 렌더링하는 React의 대표적인 조건부 렌더링 패턴입니다.

children

<Modal
  type="confirm"
  mainText="정말 삭제하시겠습니까?"
>
  <div>삭제 후 복구할 수 없습니다.</div>
</Modal>

children 덕분에 Modal 내부에 정해진 props 외의 자유로운 JSX를 추가할 수 있습니다.

7. ButtonGroup과 buttons.map()

<ButtonGroup
  ratio={
    buttonItems.length === 1
      ? 'equal'
      : buttonRatio
  }
>
  {buttonItems.map((button, index) => (
    <Button
      key={`${button.label}-${index}`}
      size="xl"
      variant={getButtonVariant(
        button,
        index,
        buttonItems.length
      )}
      onClick={button.onClick}
      disabled={button.disabled}
      type={button.type ?? 'button'}
    >
      {button.label}
    </Button>
  ))}
</ButtonGroup>

버튼 설정 객체 배열을 map()으로 실제 <Button> 컴포넌트들로 변환합니다.

buttons={[
  {
    label: '취소',
    onClick: handleCancel
  },
  {
    label: '확인',
    onClick: handleConfirm
  }
]}
데이터 객체 배열 ↓ map() Button 컴포넌트 배열 ↓ [ 취소 ] [ 확인 ]

8. Modal 실제 사용 예

<Modal
  type="confirm"
  mainText="정말 삭제하시겠습니까?"
  subText="삭제된 데이터는 복구할 수 없습니다."
  buttons={[
    {
      label: '취소',
      onClick: handleCancel,
    },
    {
      label: '삭제',
      onClick: handleDelete,
    },
  ]}
/>
type="confirm" ↓ styles.confirm ↓ 기본 제목 "확인" ↓ 체크 아이콘 ↓ mainText / subText ↓ buttons.map() ↓ [ 취소 ] [ 삭제 ]

9. BottomSheet 핵심 구조

CheckIcon / CloseIcon

작은 SVG를 별도 함수 컴포넌트로 분리해서 메인 JSX가 지나치게 복잡해지지 않게 합니다.

function CheckIcon() {
  return <svg>...</svg>
}

function CloseIcon() {
  return <svg>...</svg>
}

주요 props

export default function BottomSheet({
  title = '주요문구',
  subText = '부가설명',
  items = [],
  buttonLabel = '버튼명',
  onButtonClick,
  onClose,
  className,
  contentClassName,
  buttonClassName,
  ...props
})
prop역할
title제목
subText보조 설명
items목록 데이터 배열
buttonLabel하단 버튼 문구
onButtonClick하단 버튼 클릭 함수
onClose닫기 버튼 클릭 함수
...props최상위 section에 추가 속성 전달

10. BottomSheet의 접근성과 닫기 버튼

<section
  role="dialog"
  aria-modal="true"
>

현재 영역이 대화상자 형태의 UI라는 것을 보조기기에 알려줍니다.

<button
  type="button"
  aria-label="Close"
  onClick={onClose}
>
  <CloseIcon />
</button>

아이콘만 있는 버튼은 의미가 시각적으로만 전달되기 쉬우므로 aria-label을 넣습니다. 한글 서비스라면 aria-label="닫기"처럼 작성할 수도 있습니다.

11. items.length와 items.map()

{items.length > 0 && (
  <ul>
    {items.map((item, index) => (
      <li key={`${item.label}-${index}`}>
        ...
      </li>
    ))}
  </ul>
)}

배열에 항목이 하나 이상 있을 때만 목록 전체를 렌더링하고, 각 객체를 <li>로 변환합니다.

items={[
  {
    label: '보장내용 확인',
    rightText: '필수'
  },
  {
    label: '약관 동의',
    rightText: '선택'
  }
]}
✓ 보장내용 확인 필수 ✓ 약관 동의 선택

12. item.icon ?? CheckIcon

<span className={styles.check}>
  {item.icon ?? <CheckIcon />}
</span>

개별 item에 사용자 지정 아이콘이 있으면 그것을 사용하고, 없으면 기본 체크 아이콘을 사용합니다.

{
  label: '완료',
  icon: <StarIcon />
}
이 패턴은 Modal의 icon ?? <ModalIcon />과 동일합니다.
즉 기본 UI를 제공하되 필요하면 props로 교체할 수 있게 만드는 재사용 설계입니다.

13. rightText와 하단 Button

{item.rightText && (
  <span className={styles.itemRight}>
    {item.rightText}
  </span>
)}

rightText가 있을 때만 우측 부가 정보를 출력합니다.

<Button
  variant="gradation"
  size="xxl"
  fullWidth
  onClick={onButtonClick}
>
  {buttonLabel}
</Button>

기존 Button 컴포넌트의 variant, size, fullWidth 기능을 그대로 재사용합니다.

14. BottomSheet 실제 사용 예

<BottomSheet
  title="보험 가입 안내"
  subText="가입 전 아래 내용을 확인해주세요."
  items={[
    {
      label: '보장 내용을 확인했습니다.',
      rightText: '필수',
    },
    {
      label: '약관을 확인했습니다.',
      rightText: '필수',
    },
    {
      label: '마케팅 정보 수신',
      rightText: '선택',
    },
  ]}
  buttonLabel="확인"
  onClose={handleClose}
  onButtonClick={handleConfirm}
/>
┌────────────────────────────┐ │ X │ │ 보험 가입 안내 │ │ 가입 전 아래 내용을... │ │ │ │ ✓ 보장 내용을 확인... 필수 │ │ ✓ 약관을 확인... 필수 │ │ ✓ 마케팅 정보... 선택 │ │ │ │ [ 확인 ] │ └────────────────────────────┘

15. Modal vs BottomSheet

ModalBottomSheet
중앙 팝업 형태화면 하단에서 올라오는 형태
alert / info / confirm / error 타입하나의 기본 형태
버튼 1~2개주요 버튼 1개
ButtonGroup 사용Button 단독 사용
children으로 자유 콘텐츠 가능items 배열 중심
확인 / 경고 / 오류에 적합상세 안내 / 선택 정보에 적합

16. 이 코드에서 배울 수 있는 핵심 React 패턴

조건부 렌더링
{value && <Component />}
기본값 + 사용자 커스터마이징
{icon ?? <DefaultIcon />}
배열 → 컴포넌트
{items.map(item => (
  <Item />
))}
props 전달
<section {...props}>

17. 컴포넌트 조립 관점에서 보기

작은 컴포넌트 Button / Icon / Badge ↓ 중간 컴포넌트 ButtonGroup / Input / RadioGroup ↓ 큰 조합형 컴포넌트 Modal / BottomSheet / Header / Form

즉 Modal과 BottomSheet는 모든 기능을 처음부터 직접 만드는 것이 아니라, 이미 만들어 둔 Button, ButtonGroup, Icon 같은 작은 컴포넌트를 조합해 만든 조합형 컴포넌트(Composite Component)로 이해하면 좋습니다.

18. 최종 핵심 요약