값 → {value}
<Button label="확인" count={5} />
function Button({ label, count }) {
return (
<button>
{label} ({count})
</button>
)
}
label은 문자열, count는 숫자로 전달되고 JSX 안에서 그대로 출력됩니다.
기존 병합본의 내용을 유지하면서, 단편적으로 적혀 있던 예제를 모두 “실제 사용 → props 받기 → 내부 처리 → return 렌더링” 흐름으로 다시 구성한 완전 예제 버전입니다.
가장 단순한 props도 항상 “사용 코드 + 컴포넌트 전체”로 봅니다.
{value}<Button label="확인" count={5} />
function Button({ label, count }) {
return (
<button>
{label} ({count})
</button>
)
}
label은 문자열, count는 숫자로 전달되고 JSX 안에서 그대로 출력됩니다.
<Button />
function Button({
variant = 'primary',
size = 'md',
disabled = false
}) {
return (
<button
className={cn(
styles.button,
styles[variant],
styles[size]
)}
disabled={disabled}
>
저장
</button>
)
}
<Button>
확인
</Button>
function Button({ children }) {
return (
<button>
{children}
</button>
)
}
children에는 문자열뿐 아니라 JSX Element도 들어올 수 있습니다.
??로 대체값 사용<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>
)
}
&& 조건부 렌더링<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>
)
}
<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>
)
}
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>
)
}
selectedItem을 직접 변경하는 것이 아닙니다.
클릭 → value 변경 → 재렌더링 → find() 재실행 → selectedItem 재계산
순서입니다.
<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>
)
}
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>
)
}
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(' ')
}
active = true, disabled = false이면
cn(styles.button, styles.active, false)가 되고,
최종적으로 button + active 클래스 문자열이 반환됩니다.
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 — 객체 방식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>
)
}
classnames/bindimport 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>
)
}
styles.button, styles.active를 반복하지 않고 cx('button', { active, disabled })처럼 작성할 수 있습니다.active = true, disabled = false이므로 어떤 방식을 사용해도
기본 button 클래스 + active 클래스가 적용되고, 실제 HTML 버튼은 비활성화되지 않습니다.
| 방식 | 특징 | 코드 느낌 |
|---|---|---|
| 배열 + filter + join | 라이브러리 없이 순수 JS | 원리를 이해하기 좋음 |
| 직접 만든 cn() | 프로젝트 공통 유틸 | 간결하고 자유롭게 확장 가능 |
| classnames 조건식 | npm 패키지 사용 | cn()과 매우 비슷함 |
| classnames 객체 | 조건과 클래스 관계가 명확 | 조건이 많을 때 읽기 편함 |
| classnames/bind | CSS Modules와 결합 | styles. 반복을 줄임 |
<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>
)
}
동적으로 변하는 폭, 위치, 투명도 등에 적합합니다.
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} />
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%로 동작합니다.
/* ① 부모에 선언 → 자식에서 사용 가능 */
<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를 선언하면 그 아래의
.progressBar뿐 아니라 다른 자식 요소에서도 같은 값을 공유할 수 있습니다.
즉, “이 변수를 어디에서 사용할 것인가?”를 보고 공통으로 사용할 수 있는 적절한 부모에 선언하면 됩니다.
<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;
}
var(--progress), var(--bar-height)처럼 그 값을 받아 실제 디자인을 처리합니다.
<Button disabled>
저장
</Button>
function Button({
disabled = false,
children
}) {
return (
<button
disabled={disabled}
className={
disabled
? 'button disabled'
: 'button'
}
>
{disabled
? '사용 불가'
: children}
</button>
)
}
<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>
)
}
<Button
icon={<PlusIcon size={20} />}
>
추가
</Button>
function Button({
icon,
children
}) {
return (
<button>
{icon}
{children}
</button>
)
}
{icon}으로 그대로 렌더링합니다.<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>
)
}
icon={PlusIcon} = “컴포넌트를 줄 테니 네가 만들어 써.” / icon={<PlusIcon />} = “이미 만들어 놨으니 그대로 써.”<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 변수에 문자열 태그 또는 컴포넌트를 담아 동적으로 렌더링합니다.
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="h2", as="a"처럼 HTML 태그 이름을 전달할 수도 있고,
as={Link}, as={CustomButton}처럼 React 컴포넌트 자체를 전달할 수도 있습니다.
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>
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>
)
}
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>
)
}
function App() {
function handleAdd() {
console.log('추가')
}
return (
<Button onClick={handleAdd}>
추가
</Button>
)
}
function Button({
onClick,
children
}) {
return (
<button onClick={onClick}>
{children}
</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()}처럼 바로 호출하지 않고, 값이 필요하면 화살표 함수로 감쌉니다.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?.(...)는 함수가 전달된 경우에만 호출합니다.
<Input
onChange={(value) => {
console.log(value)
}}
/>
function Input({ onChange }) {
function handleChange(event) {
const value =
event.target.value
onChange?.(value)
}
return (
<input
onChange={handleChange}
/>
)
}
브라우저 이벤트 전체 대신 부모에게 필요한 값만 넘길 수 있습니다.
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>
)
}
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>
)
}
renderItem={renderUser}는 함수를 실행하지 않고 전달하고, renderItem(item)은 전달받은 함수를 실제로 실행합니다.renderUser는 이 예제에서 <RenderUser />처럼 사용하는 컴포넌트가 아니라 renderUser(item)으로 직접 실행해 JSX를 반환하는 일반 함수입니다.<DataLoader>
{(data) => (
<div>{data.name}</div>
)}
</DataLoader>
function DataLoader({
children
}) {
const data = {
name: '홍길동'
}
return (
<section>
{children(data)}
</section>
)
}
일반 {children}이 아니라 children(data)처럼 실행합니다.
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>
)
}
const user = {
name: '홍길동',
age: 30
}
<UserCard {...user} />
function UserCard({
name,
age
}) {
return (
<article>
<strong>{name}</strong>
<span>{age}</span>
</article>
)
}
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>
}
“전체 데이터 + 현재 선택값” 패턴입니다.
...props<Input
placeholder="이름"
maxLength={20}
disabled
/>
function Input({
className,
...props
}) {
return (
<input
className={className}
{...props}
/>
)
}
placeholder, maxLength, disabled 등이 ...props에 모여 실제 input으로 전달됩니다.
<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으로 내려갑니다.
<Input type="password" />
function Input(props) {
return (
<>
{/* props.type이 뒤에서 덮어씀 */}
<input
type="text"
{...props}
/>
{/* 마지막 type="text"가 최종 적용 */}
<input
{...props}
type="text"
/>
</>
)
}
<Button
id="saveButton"
title="저장"
disabled
aria-label="저장"
>
저장
</Button>
function Button({
children,
...props
}) {
return (
<button {...props}>
{children}
</button>
)
}
<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가 관리합니다.
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
)
}}
/>
)
}
<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>
)
}
부모 state ↓ value / checked props 전달 ↓ 자식 컴포넌트 return JSX ↓ 사용자 입력/클릭 ↓ 자식이 onChange(...) 호출 ↓ 부모 state 변경 ↓ 새 props 전달 ↓ 자식 재렌더링
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()는 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>
<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
children으로 받은 직접적인 자식에게 props를 자동 주입 → cloneElement 고려cloneElement() = children으로 받은 기존 React 요소에 부모가 props를 추가/변경해서 새로운 React 요소를 만드는 것.
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 전달 없이 자손 컴포넌트가 공유합니다.
<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 | {text} | 값 직접 출력 |
true / false | loading && ... | 조건부 렌더링/상태 |
"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/defaultValue | value ?? internalValue | Controlled/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>
)
}
<Component ... />를 본다.function Component({ ... })에서 어떻게 받는지 본다.