React Study · Complete Example Edition

React Props 전달 → 렌더링 패턴 전체 지도

기존 병합본의 내용을 유지하면서, 단편적으로 적혀 있던 예제를 모두 “실제 사용 → props 받기 → 내부 처리 → return 렌더링” 흐름으로 다시 구성한 완전 예제 버전입니다.

예제를 볼 때는 항상 <컴포넌트 props... /> → function 컴포넌트({ props }) → 계산/조건 → return(...) 순서로 읽습니다.
1

기본값을 받고 그대로 렌더링하기

가장 단순한 props도 항상 “사용 코드 + 컴포넌트 전체”로 봅니다.

문자열 · 숫자

값 → {value}

<Button label="확인" count={5} />

function Button({ label, count }) {
  return (
    <button>
      {label} ({count})
    </button>
  )
}

label은 문자열, count는 숫자로 전달되고 JSX 안에서 그대로 출력됩니다.

기본값

props가 없으면 기본값 사용

<Button />

function Button({
  variant = 'primary',
  size = 'md',
  disabled = false
}) {
  return (
    <button
      className={cn(
        styles.button,
        styles[variant],
        styles[size]
      )}
      disabled={disabled}
    >
      저장
    </button>
  )
}
<Button /> → props 전달 안 함 → 기본값 적용 variant = 'primary' → styles.primary size = 'md' → styles.md disabled = false → 활성화된 버튼
children

태그 사이 내용

<Button>
  확인
</Button>

function Button({ children }) {
  return (
    <button>
      {children}
    </button>
  )
}

children에는 문자열뿐 아니라 JSX Element도 들어올 수 있습니다.

fallback

??로 대체값 사용

<Field
  errorText="이름을 입력해주세요."
>
  이름
</Field>

function Field({
  text,
  children,
  errorText,
  helperText
}) {
  const label = text ?? children
  const message = errorText ?? helperText
  const invalid = Boolean(errorText)

  return (
    <div
      className={cn(
        styles.field,
        invalid && styles.invalid
      )}
    >
      <label>{label}</label>

      <input
        aria-invalid={invalid}
      />

      {message && (
        <p
          className={cn(
            styles.message,
            invalid && styles.error
          )}
        >
          {message}
        </p>
      )}
    </div>
  )
}
errorText 있음 → message = errorText → invalid = true → styles.invalid + styles.error + aria-invalid={true} 적용 errorText 없음 → message = helperText → invalid = false → 일반 Field + 일반 안내 메시지
2

조건부 렌더링과 props 가공

&& 조건부 렌더링

<InputField
  label="이름"
  required
  errorText="필수 입력입니다."
/>

function InputField({
  label,
  required = false,
  errorText
}) {
  return (
    <div>
      {label && (
        <label>
          {label}
          {required && <span>*</span>}
        </label>
      )}

      <input />

      {errorText && (
        <p>{errorText}</p>
      )}
    </div>
  )
}

값이 존재하거나 Boolean이 true일 때만 UI를 렌더링합니다.

삼항 연산자

<Button loading={true}>
  저장
</Button>

function Spinner() {
  return <span>로딩중...</span>
}

function Button({
  loading = false,
  children
}) {
  return (
    <button>
      {loading
        ? <Spinner />
        : children}
    </button>
  )
}
조건 ? A : B → 둘 중 하나만 렌더링

props → 계산 → 렌더링

<Progress value={30} max={100} />

function Progress({
  value,
  max = 100
}) {
  const percent =
    (value / max) * 100

  return (
    <div>
      <div
        style={{
          width: `${percent}%`
        }}
      />
      <span>{percent}%</span>
    </div>
  )
}
원본 props → 계산 → 새 변수 → JSX에서 사용

배열에서 선택값 찾기

const items = [
  { label: '서울', value: 'seoul' },
  { label: '부산', value: 'busan' }
]

<SelectLabel
  items={items}
  value="busan"
/>

function SelectLabel({
  items,
  value
}) {
  const selectedItem =
    items.find(
      item => item.value === value
    )

  return (
    <span>
      {selectedItem?.label ?? '선택'}
    </span>
  )
}

Dropdown, Select, Tab에서 매우 자주 등장합니다.

중요: 위 예제의 selectedItem은 클릭으로 직접 들어가는 값이 아니라, 컴포넌트가 렌더링될 때 현재 value를 기준으로 find()가 계산한 결과입니다.

클릭으로 선택값이 바뀌는 경우

function App() {
  const items = [
    { label: '서울', value: 'seoul' },
    { label: '부산', value: 'busan' }
  ]

  const [value, setValue] =
    useState('seoul')

  return (
    <SelectLabel
      items={items}
      value={value}
      onChange={setValue}
    />
  )
}

function SelectLabel({
  items,
  value,
  onChange
}) {
  // 렌더링될 때 현재 value로
  // 선택된 객체를 다시 계산
  const selectedItem =
    items.find(
      item => item.value === value
    )

  return (
    <div>
      <strong>
        현재 선택:
        {selectedItem?.label ?? '선택'}
      </strong>

      {items.map((item) => (
        <button
          key={item.value}
          onClick={() =>
            onChange(item.value)
          }
        >
          {item.label}
        </button>
      ))}
    </div>
  )
}
최초 value = "seoul" → find() 실행 → selectedItem = 서울 → 화면에 서울 렌더링 [부산] 클릭 → onClick → onChange("busan") → setValue("busan") → 부모 state 변경 → 재렌더링 → value = "busan" → find() 다시 실행 → selectedItem = 부산 → 화면에 부산 렌더링
핵심: 클릭이 selectedItem을 직접 변경하는 것이 아닙니다. 클릭 → value 변경 → 재렌더링 → find() 재실행 → selectedItem 재계산 순서입니다.
3

props로 className · style 결정하기

값 → class 선택

<Button variant="outline">
  저장
</Button>

const BUTTON_VARIANTS = {
  primary: 'btnPrimary',
  outline: 'btnOutline',
  danger: 'btnDanger'
}

function Button({
  variant = 'primary',
  children
}) {
  const variantClass =
    BUTTON_VARIANTS[variant]

  return (
    <button
      className={`button ${variantClass}`}
    >
      {children}
    </button>
  )
}
variant="outline" → btnOutline 선택

Boolean → class 추가

Boolean props에 따라 클래스를 붙이는 대표적인 방법들을 같은 Button 예제로 비교합니다.

① 배열 + filter(Boolean) + join()

<Button active disabled={false}>
  저장
</Button>

function Button({
  active = false,
  disabled = false,
  children
}) {
  const className = [
    'button',
    active && 'active',
    disabled && 'disabled'
  ]
    .filter(Boolean)
    .join(' ')

  return (
    <button
      className={className}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
['button', 'active', false] → filter(Boolean) → ['button', 'active'] → join(' ') → "button active"

② 직접 만든 cn() 유틸

import { cn } from '@/utils/cn'
import styles from './Button.module.css'

<Button active disabled={false}>
  저장
</Button>

function Button({
  active = false,
  disabled = false,
  children
}) {
  return (
    <button
      className={cn(
        styles.button,
        active && styles.active,
        disabled && styles.disabled
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
active가 true이면 styles.active가 추가되고, false인 값은 cn()이 제외합니다.
직접 만든 cn() 유틸 실제 소스
export function cn(...inputs) {
  return inputs
    .flatMap((input) => {
      if (!input) return []

      if (Array.isArray(input)) {
        return cn(...input)
      }

      if (typeof input === 'object') {
        return Object.entries(input)
          .filter(([, value]) => Boolean(value))
          .map(([key]) => key)
      }

      return String(input)
    })
    .join(' ')
}
cn( styles.button, active && styles.active, disabled && styles.disabled ) → falsy 값 제거 → 배열이면 재귀 처리 → 객체면 value가 true인 key만 추출 → 문자열로 변환 → 공백으로 join(' ')
예를 들어 active = true, disabled = false이면 cn(styles.button, styles.active, false)가 되고, 최종적으로 button + active 클래스 문자열이 반환됩니다.

③ npm classnames — 조건식 방식

import classNames from 'classnames'
import styles from './Button.module.css'

<Button active disabled={false}>
  저장
</Button>

function Button({
  active = false,
  disabled = false,
  children
}) {
  return (
    <button
      className={classNames(
        styles.button,
        active && styles.active,
        disabled && styles.disabled
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
classNames(styles.button, styles.active, false) → false 제외 → button + active 클래스 적용

④ npm classnames — 객체 방식

import classNames from 'classnames'
import styles from './Button.module.css'

<Button active disabled={false}>
  저장
</Button>

function Button({
  active = false,
  disabled = false,
  children
}) {
  return (
    <button
      className={classNames(
        styles.button,
        {
          [styles.active]: active,
          [styles.disabled]: disabled
        }
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
styles.button → 항상 적용 [styles.active]: active → true일 때 적용 [styles.disabled]: disabled → true일 때 적용

⑤ CSS Modules + classnames/bind

import classNames from 'classnames/bind'
import styles from './Button.module.css'

const cx = classNames.bind(styles)

<Button active disabled={false}>
  저장
</Button>

function Button({
  active = false,
  disabled = false,
  children
}) {
  return (
    <button
      className={cx(
        'button',
        {
          active,
          disabled
        }
      )}
      disabled={disabled}
    >
      {children}
    </button>
  )
}
CSS Modules를 사용할 때 styles.button, styles.active를 반복하지 않고 cx('button', { active, disabled })처럼 작성할 수 있습니다.
현재 예제의 결과: active = true, disabled = false이므로 어떤 방식을 사용해도 기본 button 클래스 + active 클래스가 적용되고, 실제 HTML 버튼은 비활성화되지 않습니다.
방식특징코드 느낌
배열 + filter + join라이브러리 없이 순수 JS원리를 이해하기 좋음
직접 만든 cn()프로젝트 공통 유틸간결하고 자유롭게 확장 가능
classnames 조건식npm 패키지 사용cn()과 매우 비슷함
classnames 객체조건과 클래스 관계가 명확조건이 많을 때 읽기 편함
classnames/bindCSS Modules와 결합styles. 반복을 줄임

inline style

<Progress value={65} />

function Progress({ value }) {
  const percent = Math.min(
    Math.max(value, 0),
    100
  )

  return (
    <div className="track">
      <div
        className="bar"
        style={{
          width: `${percent}%`,
          opacity: percent / 100
        }}
      />
    </div>
  )
}

동적으로 변하는 폭, 위치, 투명도 등에 적합합니다.

CSS 변수 전달

React에서 동적인 값을 --변수명 형태의 CSS Custom Property로 전달하고, CSS에서는 var(--변수명)으로 꺼내 사용할 수 있습니다.

① 가장 단순한 구조

<Progress value={70} />

function Progress({ value }) {
  return (
    <div
      className="progress"
      style={{
        '--progress': `${value}%`
      }}
    />
  )
}

/* CSS */
.progress {
  width: var(--progress);
}
<Progress value={70} /> → value = 70 → `${value}%` = "70%" → style="--progress: 70%" → CSS의 var(--progress) → width: 70%

② 실제 Progress에서 더 자연스러운 구조

<Progress value={70} />

function Progress({ value }) {
  return (
    <div
      className="progress"
      style={{
        '--progress': `${value}%`
      }}
    >
      <div className="progressBar" />
    </div>
  )
}

/* CSS */
.progress {
  width: 100%;
  height: 8px;
  background: #eee;
  border-radius: 4px;
  overflow: hidden;
}

.progressBar {
  width: var(--progress);
  height: 100%;
  background: #f44336;
  transition: width 0.3s ease;
}
왜 자식인 .progressBar에서도 사용할 수 있나?
부모 .progress에 선언한 --progress CSS 변수는 자식 요소에서도 참조할 수 있기 때문에 .progressBar { width: var(--progress); }가 width: 70%로 동작합니다.
CSS 변수는 어디에 선언해도 될까?
아무 위치에나 선언할 수 있는 것은 아닙니다. CSS 변수를 사용하는 요소 자기 자신 또는 그 요소의 조상(부모/상위 부모)에 선언해야 합니다. CSS Custom Property도 상속 규칙을 따르기 때문입니다.
/* ① 부모에 선언 → 자식에서 사용 가능 */
<div
  class="progress"
  style="--progress: 70%"
>
  <div class="progressBar"></div>
</div>

/* CSS */
.progressBar {
  width: var(--progress);
}

/*
progress
--progress: 70%
    ↓ 상속
progressBar
width: var(--progress)
→ width: 70%
*/
/* ② 사용하는 요소 자신에게 선언 → 가능 */
<div class="progress">
  <div
    class="progressBar"
    style="--progress: 70%"
  ></div>
</div>

.progressBar {
  width: var(--progress);
}
/* ③ 더 상위 부모에 선언 → 가능 */
<section style="--progress: 70%">
  <div class="progress">
    <div class="progressBar"></div>
  </div>
</section>

.progressBar {
  width: var(--progress);
}

/*
section
  ↓ 상속
progress
  ↓ 상속
progressBar
*/
/* ④ 형제 요소에 선언 → 사용 불가 */
<div class="progress">
  <span
    style="--progress: 70%"
  ></span>

  <div class="progressBar"></div>
</div>

.progressBar {
  width: var(--progress);
  /* span의 자식이 아니므로
     span의 --progress를 상속받지 못함 */
}
선언 위치 판단 기준 변수를 사용하는 요소 자신 → 가능 부모 / 상위 부모 → 상속받으므로 가능 형제 요소 → 상속 관계가 아니므로 불가능 자식 요소 → 부모가 자식의 변수를 역으로 상속받지 않으므로 불가능
Progress에서는 왜 부모에 두는가?
.progress에 --progress를 선언하면 그 아래의 .progressBar뿐 아니라 다른 자식 요소에서도 같은 값을 공유할 수 있습니다. 즉, “이 변수를 어디에서 사용할 것인가?”를 보고 공통으로 사용할 수 있는 적절한 부모에 선언하면 됩니다.
React <Progress value={70} /> ↓ style={ { '--progress': '70%' } } ↓ 렌더링된 DOM 개념 <div class="progress" style="--progress: 70%"> <div class="progressBar"></div> </div> ↓ CSS .progressBar { width: var(--progress); } ↓ width: 70% ↓ ██████████████------ 70%

③ 여러 CSS 변수 전달

<Progress
  value={70}
  height={8}
/>

function Progress({
  value,
  height
}) {
  return (
    <div
      className="progress"
      style={{
        '--progress': `${value}%`,
        '--bar-height': `${height}px`
      }}
    >
      <div className="progressBar" />
    </div>
  )
}

/* CSS */
.progress {
  width: 100%;
  height: var(--bar-height);
  background: #eee;
}

.progressBar {
  width: var(--progress);
  height: 100%;
  background: #f44336;
}
핵심: React는 동적인 값을 CSS 변수에 넣고, CSS는 var(--progress), var(--bar-height)처럼 그 값을 받아 실제 디자인을 처리합니다.

하나의 prop을 여러 곳에서 사용

<Button disabled>
  저장
</Button>

function Button({
  disabled = false,
  children
}) {
  return (
    <button
      disabled={disabled}
      className={
        disabled
          ? 'button disabled'
          : 'button'
      }
    >
      {disabled
        ? '사용 불가'
        : children}
    </button>
  )
}

props → 다른 값 계산

<Button
  size="lg"
  icon={PlusIcon}
>
  추가
</Button>

const ICON_SIZE = {
  lg: 24,
  md: 20,
  sm: 16
}

function Button({
  size = 'md',
  icon: Icon,
  children
}) {
  const iconSize =
    ICON_SIZE[size]

  return (
    <button>
      {Icon && (
        <Icon size={iconSize} />
      )}
      {children}
    </button>
  )
}
4

JSX Element vs Component 자체 전달

① Element 전달

<Button
  icon={<PlusIcon size={20} />}
>
  추가
</Button>

function Button({
  icon,
  children
}) {
  return (
    <button>
      {icon}
      {children}
    </button>
  )
}
이미 생성된 JSX를 넘기므로 자식은 보통 {icon}으로 그대로 렌더링합니다.

② Component 전달

<Button
  icon={PlusIcon}
  size="lg"
>
  추가
</Button>

function Button({
  icon: Icon,
  size = 'md',
  children
}) {
  const iconSize =
    size === 'lg' ? 24 : 16

  return (
    <button>
      {Icon && (
        <Icon
          size={iconSize}
          stroke="currentColor"
        />
      )}
      {children}
    </button>
  )
}
컴포넌트 함수 자체를 넘기므로 받는 쪽이 새로운 props를 붙여 생성할 수 있습니다.
핵심: icon={PlusIcon} = “컴포넌트를 줄 테니 네가 만들어 써.” / icon={<PlusIcon />} = “이미 만들어 놨으니 그대로 써.”

Slot props

<Card
  header={<Header />}
  footer={<Footer />}
>
  본문 내용
</Card>

function Card({
  header,
  footer,
  children
}) {
  return (
    <section>
      <header>{header}</header>
      <main>{children}</main>
      <footer>{footer}</footer>
    </section>
  )
}

특정 위치를 외부 JSX로 채우는 패턴입니다.

as / 동적 태그

<Text as="h2">
  제목
</Text>

function Text({
  as: Component = 'span',
  children
}) {
  return (
    <Component>
      {children}
    </Component>
  )
}

Component 변수에 문자열 태그 또는 컴포넌트를 담아 동적으로 렌더링합니다.

HTML 태그뿐 아니라 React 컴포넌트도 가능

function Link({
  to,
  children,
  ...props
}) {
  return (
    <a
      href={to}
      {...props}
    >
      {children}
    </a>
  )
}

<Text
  as={Link}
  to="/home"
>
  홈으로
</Text>

function Text({
  as: Component = 'span',
  children,
  ...props
}) {
  return (
    <Component {...props}>
      {children}
    </Component>
  )
}
as={Link} → Component = Link → <Component ...> → <Link to="/home"> → Link 컴포넌트가 <a href="/home">로 렌더링
as="h2", as="a"처럼 HTML 태그 이름을 전달할 수도 있고, as={Link}, as={CustomButton}처럼 React 컴포넌트 자체를 전달할 수도 있습니다.

Button에서 사용하면 이해하기 더 쉬움

function Button({
  as: Component = 'button',
  variant = 'primary',
  children,
  className,
  ...props
}) {
  return (
    <Component
      className={cn(
        styles.button,
        styles[variant],
        className
      )}
      {...props}
    >
      {children}
    </Component>
  )
}

// ① as를 생략
<Button onClick={handleClick}>
  저장
</Button>

// Component = 'button'
// 실제 렌더링:
// <button>저장</button>


// ② a 태그로 변경
<Button
  as="a"
  href="/home"
>
  홈으로
</Button>

// Component = 'a'
// 실제 렌더링:
// <a href="/home">홈으로</a>
Button의 스타일/기능은 그대로 유지 ↓ as가 없으면 Component = "button" ↓ <button> as="a"이면 Component = "a" ↓ <a>
왜 as: Component = 'button'이라고 쓰나?
as는 React 전용 속성이 아니라 개발자가 정한 일반 props 이름일 뿐입니다. 그리고 as: Component의 :는 React 문법이 아니라 JavaScript 객체 구조분해 할당에서 변수 이름을 바꾸는 문법입니다.

따라서 as: Component = 'button' 대신 tagName: Tag = 'p'처럼 다른 이름을 사용해도 전혀 문제없습니다. 중요한 것은 props로 받은 태그 이름을 Component, Tag처럼 대문자로 시작하는 변수에 담아 JSX에서 동적 태그로 렌더링하는 것입니다.
import { cn } from "@/utils/cn"
import styles from "./Typography.module.css"

export default function Typography({
  tagName: Tag = "p",
  className = "",
  children,
  ...rest
}) {
  const moduleClassName = className
    .split(/\s+/)
    .filter(Boolean)
    .map((className) => styles[className])

  return (
    <Tag
      className={cn(
        moduleClassName,
        className
      )}
      {...rest}
    >
      {children}
    </Tag>
  )
}
tagName → 개발자가 정한 props 이름 tagName: Tag → 객체 구조분해 할당으로 tagName을 내부에서 Tag라는 이름으로 사용 = "p" → tagName이 전달되지 않았을 때 기본값 <Tag> → 전달받은 태그를 실제 JSX 태그로 렌더링
정리: as: Component = 'button'과 tagName: Tag = 'p'는 원리가 같습니다.
as나 tagName은 React가 정해준 이름이 아니므로 컴포넌트 설계에 따라 원하는 props 이름을 사용할 수 있습니다.
/* 풀어서 쓰면 */

function Button({
  as = 'button',
  children
}) {
  const Component = as

  return (
    <Component>
      {children}
    </Component>
  )
}

/* 위 코드를 간단히 쓰면 */

function Button({
  as: Component = 'button',
  children
}) {
  return (
    <Component>
      {children}
    </Component>
  )
}
5

함수 props와 이벤트 흐름

함수를 그대로 연결

function App() {
  function handleAdd() {
    console.log('추가')
  }

  return (
    <Button onClick={handleAdd}>
      추가
    </Button>
  )
}

function Button({
  onClick,
  children
}) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
}
부모 함수 → props 전달 → button 이벤트 연결 → 클릭 시 실행

값을 추가해서 호출

function App() {
  function handleAdd(count) {
    console.log(count)
  }

  return (
    <Button
      onClick={() => handleAdd(10)}
    >
      10개 추가
    </Button>
  )
}

function Button({
  onClick,
  children
}) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
}
onClick={handleAdd()}처럼 바로 호출하지 않고, 값이 필요하면 화살표 함수로 감쌉니다.

자식에서 callback 호출

const items = [
  { label: 'A', value: 'a' },
  { label: 'B', value: 'b' }
]

<Dropdown
  items={items}
  onChange={(item, index) => {
    console.log(item, index)
  }}
/>

function Dropdown({
  items,
  onChange
}) {
  return (
    <div>
      {items.map((item, index) => (
        <button
          key={item.value}
          onClick={(event) => {
            onChange?.(
              item,
              index,
              event
            )
          }}
        >
          {item.label}
        </button>
      ))}
    </div>
  )
}

onChange?.(...)는 함수가 전달된 경우에만 호출합니다.

event를 필요한 값으로 가공

<Input
  onChange={(value) => {
    console.log(value)
  }}
/>

function Input({ onChange }) {
  function handleChange(event) {
    const value =
      event.target.value

    onChange?.(value)
  }

  return (
    <input
      onChange={handleChange}
    />
  )
}

브라우저 이벤트 전체 대신 부모에게 필요한 값만 넘길 수 있습니다.

Render props

const users = [
  { id: 1, name: '철수' },
  { id: 2, name: '영희' }
]

<List
  items={users}
  renderItem={(user) => (
    <strong>{user.name}</strong>
  )}
/>

function List({
  items,
  renderItem
}) {
  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>
          {renderItem(item)}
        </div>
      ))}
    </div>
  )
}
함수 props → 실행 → JSX 반환 → 렌더링

구체적인 예제 — 회원 목록 렌더링

renderUser 함수를 별도로 만든 뒤 renderItem prop으로 함수 자체를 전달하는 예제입니다. 함수 전달 → 실행 흐름을 확인하기 쉽습니다.

const users = [
  {
    id: 1,
    name: '철수',
    age: 25,
    job: '프론트엔드 개발자'
  },
  {
    id: 2,
    name: '영희',
    age: 28,
    job: 'UI 디자이너'
  }
]

// ① 각 user를 어떻게 렌더링할지 정의
function renderUser(user) {
  return (
    <div className="userCard">
      <strong>{user.name}</strong>
      <span>{user.age}세</span>
      <p>{user.job}</p>
      <button>상세보기</button>
    </div>
  )
}

function App() {
  return (
    <List
      items={users}
      renderItem={renderUser}
    />
  )
}

function List({
  items,
  renderItem
}) {
  return (
    <div className="list">
      {items.map((item) => (
        <div
          key={item.id}
          className="listItem"
        >
          {renderItem(item)}
        </div>
      ))}
    </div>
  )
}
① function renderUser(user) 생성 ↓ ② renderItem={renderUser} → 함수 자체를 props로 전달 ↓ ③ List에서 renderItem으로 받음 ↓ ④ items.map()으로 item을 꺼냄 ↓ ⑤ renderItem(item) 실행 ↓ ⑥ 사실상 renderUser(item) 실행 ↓ ⑦ item이 user 파라미터로 들어감 ↓ ⑧ JSX 반환 → 화면 렌더링
핵심: renderItem={renderUser}는 함수를 실행하지 않고 전달하고, renderItem(item)은 전달받은 함수를 실제로 실행합니다.
renderUser는 이 예제에서 <RenderUser />처럼 사용하는 컴포넌트가 아니라 renderUser(item)으로 직접 실행해 JSX를 반환하는 일반 함수입니다.

children을 함수로 사용

<DataLoader>
  {(data) => (
    <div>{data.name}</div>
  )}
</DataLoader>

function DataLoader({
  children
}) {
  const data = {
    name: '홍길동'
  }

  return (
    <section>
      {children(data)}
    </section>
  )
}

일반 {children}이 아니라 children(data)처럼 실행합니다.

6

배열 · 객체 props

배열 → map()

const items = [
  { label: '홈', value: 'home' },
  { label: '회원', value: 'user' }
]

<Tabs items={items} />

function Tabs({ items = [] }) {
  return (
    <div>
      {items.map((item) => (
        <button
          key={item.value}
        >
          {item.label}
        </button>
      ))}
    </div>
  )
}

items를 받으면 map을 예상하는 습관이 중요합니다.

객체 → 필요한 값 추출

<User
  user={{
    name: '홍길동',
    age: 20
  }}
/>

function User({ user }) {
  return (
    <div>
      <strong>
        {user.name}
      </strong>
      <span>
        {user.age}
      </span>
    </div>
  )
}

객체 spread 전달

const user = {
  name: '홍길동',
  age: 30
}

<UserCard {...user} />

function UserCard({
  name,
  age
}) {
  return (
    <article>
      <strong>{name}</strong>
      <span>{age}</span>
    </article>
  )
}
{...user} → name과 age를 각각 props로 펼쳐 전달

배열 + 현재 value

const items = [
  { label: '홈', value: 'home' },
  { label: '상품', value: 'product' }
]

<TabLabel
  items={items}
  value="product"
/>

function TabLabel({
  items,
  value
}) {
  const selectedItem =
    items.find(
      item => item.value === value
    )

  const label =
    selectedItem?.label ?? '선택'

  return <span>{label}</span>
}

“전체 데이터 + 현재 선택값” 패턴입니다.

7

HTML 속성 전달과 ...props

나머지 props 한 번에 전달

<Input
  placeholder="이름"
  maxLength={20}
  disabled
/>

function Input({
  className,
  ...props
}) {
  return (
    <input
      className={className}
      {...props}
    />
  )
}

placeholder, maxLength, disabled 등이 ...props에 모여 실제 input으로 전달됩니다.

일부 props 소비 + 나머지만 전달

<Button
  variant="primary"
  size="lg"
  id="saveButton"
  title="저장"
>
  저장
</Button>

function Button({
  variant = 'primary',
  size = 'md',
  children,
  className,
  ...props
}) {
  const classes =
    `button ${variant} ${size} ${className ?? ''}`

  return (
    <button
      className={classes}
      {...props}
    >
      {children}
    </button>
  )
}

variant/size는 컴포넌트가 소비하고 나머지만 DOM으로 내려갑니다.

props 순서와 덮어쓰기

<Input type="password" />

function Input(props) {
  return (
    <>
      {/* props.type이 뒤에서 덮어씀 */}
      <input
        type="text"
        {...props}
      />

      {/* 마지막 type="text"가 최종 적용 */}
      <input
        {...props}
        type="text"
      />
    </>
  )
}
기억: 같은 prop 이름이면 뒤쪽 값이 최종 적용됩니다.

HTML 속성 직접 전달

<Button
  id="saveButton"
  title="저장"
  disabled
  aria-label="저장"
>
  저장
</Button>

function Button({
  children,
  ...props
}) {
  return (
    <button {...props}>
      {children}
    </button>
  )
}
8

props와 state 연결 패턴

Uncontrolled

내부 state 관리

<Tab defaultValue="home" />

function Tab({
  defaultValue = 'home'
}) {
  const [value, setValue] =
    useState(defaultValue)

  return (
    <div>
      <button
        onClick={() => setValue('home')}
      >
        홈
      </button>

      <button
        onClick={() => setValue('product')}
      >
        상품
      </button>

      <p>선택: {value}</p>
    </div>
  )
}

부모는 초기값만 주고 이후 값은 자식 내부 state가 관리합니다.

Controlled

부모가 값 관리

function App() {
  const [name, setName] =
    useState('')

  return (
    <Input
      value={name}
      onChange={setName}
    />
  )
}

function Input({
  value,
  onChange
}) {
  return (
    <input
      value={value}
      onChange={(event) => {
        onChange(
          event.target.value
        )
      }}
    />
  )
}
부모 state → value → 자식 → onChange → 부모 state 변경
둘 다 지원

Controlled + Uncontrolled

<Tab
  defaultValue="home"
  onChange={(value) => {
    console.log(value)
  }}
/>

function Tab({
  value,
  defaultValue = 'home',
  onChange
}) {
  const [
    internalValue,
    setInternalValue
  ] = useState(defaultValue)

  const selectedValue =
    value ?? internalValue

  function handleChange(nextValue) {
    if (value === undefined) {
      setInternalValue(nextValue)
    }

    onChange?.(nextValue)
  }

  return (
    <div>
      <button
        onClick={() =>
          handleChange('home')
        }
      >
        홈
      </button>

      <button
        onClick={() =>
          handleChange('product')
        }
      >
        상품
      </button>

      <p>
        선택: {selectedValue}
      </p>
    </div>
  )
}
value 있으면 Controlled / 없으면 내부 state 사용

Controlled 전체 흐름

부모 state
↓
value / checked props 전달
↓
자식 컴포넌트 return JSX
↓
사용자 입력/클릭
↓
자식이 onChange(...) 호출
↓
부모 state 변경
↓
새 props 전달
↓
자식 재렌더링
9

Group 컴포넌트와 고급 패턴

Children.toArray

<ButtonGroup>
  <button>확인</button>
  <button>취소</button>
</ButtonGroup>

function ButtonGroup({
  children
}) {
  const items =
    Children.toArray(children)

  return (
    <div className="buttonGroup">
      {items.map((child, index) => (
        <div key={index}>
          {child}
        </div>
      ))}
    </div>
  )
}

여러 children을 배열처럼 다룰 때 사용합니다.

cloneElement

<RadioGroup
  value="a"
  onChange={(value) =>
    console.log(value)
  }
>
  <Radio value="a">A</Radio>
  <Radio value="b">B</Radio>
</RadioGroup>

function RadioGroup({
  value,
  onChange,
  children
}) {
  return (
    <div>
      {Children.map(
        children,
        (child) =>
          cloneElement(child, {
            checked:
              child.props.value === value,
            onChange
          })
      )}
    </div>
  )
}

function Radio({
  value,
  checked,
  onChange,
  children
}) {
  return (
    <label>
      <input
        type="radio"
        checked={checked}
        onChange={() =>
          onChange?.(value)
        }
      />
      {children}
    </label>
  )
}

부모 Group이 자식 요소에 필요한 props를 추가로 주입합니다.

cloneElement는 정확히 언제 사용하는가?

cloneElement()는 children으로 전달받은 기존 React 요소에 부모가 props를 추가하거나 변경해서 새로운 React 요소를 만들고 싶을 때 고려할 수 있습니다.

<RadioGroup value="a">
  <Radio value="a">A</Radio>
  <Radio value="b">B</Radio>
</RadioGroup>

// RadioGroup 내부에서 개념적으로
cloneElement(child, {
  checked: child.props.value === value,
  onChange
})

// 원래 자식
<Radio value="a">A</Radio>

// cloneElement 적용 후 개념적인 모습
<Radio
  value="a"
  checked={true}
  onChange={onChange}
>
  A
</Radio>
부모가 children을 받음 ↓ children이 React Element ↓ 부모가 그 자식에게 checked / active / onChange 같은 props를 자동으로 넣어줘야 함 ↓ cloneElement 고려

Tabs에서도 같은 방식으로 사용 가능

<Tabs value="product">
  <Tab value="home">홈</Tab>
  <Tab value="product">상품</Tab>
</Tabs>

function Tabs({
  value,
  children
}) {
  return (
    <div>
      {Children.map(
        children,
        (child) =>
          cloneElement(child, {
            active:
              child.props.value === value
          })
      )}
    </div>
  )
}

// 결과 개념
// home    → active=false
// product → active=true
선택 기준

부모가 자식을 직접 생성 → 그냥 props 전달

부모가 children으로 받은 직접적인 자식에게 props를 자동 주입 → cloneElement 고려

여러 단계 아래 자식들이 부모 상태를 공유 → Context 고려
한 줄 정리: cloneElement() = children으로 받은 기존 React 요소에 부모가 props를 추가/변경해서 새로운 React 요소를 만드는 것.

Context

const RadioContext =
  createContext(null)

<RadioGroup
  value="a"
  onChange={(value) =>
    console.log(value)
  }
>
  <Radio value="a">A</Radio>
  <Radio value="b">B</Radio>
</RadioGroup>

function RadioGroup({
  value,
  onChange,
  children
}) {
  return (
    <RadioContext.Provider
      value={{ value, onChange }}
    >
      {children}
    </RadioContext.Provider>
  )
}

function Radio({
  value,
  children
}) {
  const group =
    useContext(RadioContext)

  const checked =
    group?.value === value

  return (
    <label>
      <input
        type="radio"
        checked={checked}
        onChange={() =>
          group?.onChange?.(value)
        }
      />
      {children}
    </label>
  )
}

Group의 상태를 중간 props 전달 없이 자손 컴포넌트가 공유합니다.

props에 따라 태그 자체 변경

<Action
  type="link"
  href="/home"
>
  홈으로
</Action>

function Action({
  type = 'button',
  href,
  children
}) {
  if (type === 'link') {
    return (
      <a href={href}>
        {children}
      </a>
    )
  }

  return (
    <button>
      {children}
    </button>
  )
}

같은 컴포넌트 API를 유지하면서 실제 렌더링 태그를 바꿀 수 있습니다.

10

최우선 암기표 + 전체 압축 지도

부모가 넘긴 값자식에서 자주 보는 코드의미
"저장", 10{text}값 직접 출력
true / falseloading && ...조건부 렌더링/상태
"primary"styles[variant]CSS class 선택
<Icon />{icon}React Element 그대로 렌더
Icon<Icon size={20} />Component를 동적으로 생성
children{children}태그 사이 내용 렌더
함수onClick={fn}DOM 이벤트 연결
함수fn?.(value)callback 호출
배열items.map(...)반복 렌더링
객체user.name객체 데이터 추출
나머지 속성<input {...props} />DOM/컴포넌트로 전달
value/defaultValuevalue ?? internalValueControlled/Uncontrolled
부모 사용
<Component props... />
        ↓
자식 props 받기
function Component({ props... })
        ↓
내부 소비
├─ 그대로 렌더        → {text}
├─ 기본값             → size = 'md'
├─ 조건               → loading && ...
├─ 선택               → condition ? A : B
├─ fallback           → label ?? children
├─ class 선택         → styles[variant]
├─ style 계산         → style={{ width }}
├─ HTML 속성          → disabled={disabled}
├─ Element            → {icon}
├─ Component          → <Icon />
├─ 함수 연결          → onClick={onClick}
├─ callback 호출      → onChange?.(value)
├─ 배열 반복          → items.map(...)
├─ 객체 접근          → user.name
├─ 나머지 props       → {...props}
├─ state 초기화       → useState(defaultValue)
├─ state 선택         → value ?? internalValue
├─ slot               → header/footer/right
├─ render props       → renderItem(item)
├─ children 배열화    → Children.toArray
├─ 자식 props 주입    → cloneElement
└─ 간접 공유          → Context
        ↓
return (
  JSX
)

처음 보는 컴포넌트를 완전한 흐름으로 해석

<Dropdown
  items={items}
  value={value}
  icon={ArrowIcon}
  disabled={false}
  onChange={handleChange}
/>

function Dropdown({
  items,
  value,
  icon: Icon,
  disabled = false,
  onChange
}) {
  const selectedItem =
    items.find(
      item => item.value === value
    )

  return (
    <button
      disabled={disabled}
      onClick={() =>
        onChange?.(selectedItem)
      }
    >
      {Icon && <Icon size={16} />}
      {selectedItem?.label ?? '선택'}
    </button>
  )
}
items → 배열/find value → 현재 선택값 icon → Component disabled → Boolean onChange → callback ↓ 모두 return JSX 안에서 실제 렌더링/이벤트로 연결
11

컴포넌트 분석할 때 보는 순서

1 먼저 실제 사용 코드 <Component ... />를 본다.
2 어떤 props가 전달되는지 자료형을 본다.
3 function Component({ ... })에서 어떻게 받는지 본다.
4 기본값이 있는지 본다.
5 내부에서 새 변수로 계산/가공하는지 본다.
6 조건부 렌더링에 쓰는지 본다.
7 className/style/HTML 속성에 쓰는지 본다.
8 함수라면 연결인지 직접 호출인지 본다.
9 배열이면 map/find를 쓰는지 본다.
10 value/defaultValue/state 흐름인지 본다.
11 children/cloneElement/Context가 있는지 본다.
12 마지막으로 return JSX에서 최종 결과를 확인한다.
최종 핵심: 단편적인 한 줄만 보지 말고 항상 “사용 코드 → props 구조분해 → 내부 가공 → return JSX” 4단계로 읽으면 컴포넌트 설계가 훨씬 빠르게 보입니다.