부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방법인 Props를 깊이 있게 학습한다.
Props는 Properties(속성)의 줄임말로, 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때 사용하는 방법이다.
// 일반 자바스크립트 함수
function greet(name) { // name은 매개변수
return `안녕, ${name}!`;
}
greet("철수"); // "철수"는 인자
// React 컴포넌트
function Greeting(props) { // props는 매개변수
return <h1>안녕, {props.name}!</h1>;
}
<Greeting name="철수" /> // name="철수"는 prop
// 할아버지 → 아버지 → 자식 순서로 흐름
function Grandparent() {
const data = "중요한 정보";
return <Parent info={data} />; // 아래로 전달
}
function Parent(props) {
return <Child info={props.info} />; // 더 아래로 전달
}
function Child(props) {
return <div>{props.info}</div>; // 최종 사용
}
⚠️ 자식이 부모에게 직접 데이터를 전달할 수는 없다!// ❌ 절대 하면 안 되는 것
function Profile(props) {
props.name = "다른 이름"; // 오류! Props는 읽기 전용!
return <h1>{props.name}</h1>;
}
// ✅ 올바른 방법: 새로운 변수를 만들어 사용
function Profile(props) {
const displayName = props.name.toUpperCase();
return <h1>{displayName}</h1>;
}
HTML 속성을 쓰는 것과 비슷하게 컴포넌트 태그에 속성을 추가한다.
// App.jsx (부모 컴포넌트)
function App() {
return (
<div>
{/* 문자열 prop: 따옴표 사용 */}
<Greeting name="김철수" />
{/* 숫자 prop: 중괄호 사용 */}
<Age value={25} />
{/* 불리언 prop */}
<User isAdmin={true} />
<User isAdmin={false} />
<User isAdmin /> {/* true는 생략 가능 */}
{/* 배열 prop */}
<List items={[1, 2, 3, 4, 5]} />
{/* 객체 prop */}
<Profile user={{ name: "철수", age: 25 }} />
{/* 여러 props 동시에 전달 */}
<Card
title="제목"
content="내용"
author="작성자"
date="2024-01-01"
/>
</div>
);
}
name="철수"age={25}, isActive={true}// ❌ 잘못된 예
<Age value="25" /> // 숫자가 아니라 문자열 "25"가 전달됨!
// ✅ 올바른 예
<Age value={25} /> // 숫자 25가 전달됨
자식 컴포넌트 함수의 첫 번째 매개변수로 props 객체를 받는다.
// Profile.jsx (자식 컴포넌트)
function Profile(props) {
// props는 객체 형태로 전달됨
console.log(props);
// { name: "김철수", age: 25, isMember: true }
return (
<div style={{ border: '1px solid #ddd', padding: '20px', margin: '10px' }}>
<h2>이름: {props.name}</h2>
<p>나이: {props.age}세</p>
<p>회원여부: {props.isMember ? "정회원" : "준회원"}</p>
{/* props를 이용한 조건부 렌더링 */}
{props.isMember && <span style={{ color: 'green' }}>✓ 인증됨</span>}
</div>
);
}
export default Profile;
function Button(props) {
// props로 버튼의 모양과 동작을 커스터마이징
const buttonStyle = {
backgroundColor: props.color || '#007bff',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
fontSize: props.size === 'large' ? '18px' : '14px',
cursor: 'pointer'
};
return (
<button
style={buttonStyle}
onClick={props.onClick}
disabled={props.disabled}
>
{props.text}
</button>
);
}
export default Button;
App.jsx
import Button from './Button';
function App() {
const handleClick = () => {
alert('버튼 클릭!');
};
return (
<div style={{ padding: '20px' }}>
{/* 같은 Button 컴포넌트를 다양하게 재사용 */}
<Button
text="저장"
color="#28a745"
onClick={handleClick}
/>
<Button
text="삭제"
color="#dc3545"
onClick={handleClick}
/>
<Button
text="큰 버튼"
color="#6f42c1"
size="large"
onClick={handleClick}
/>
<Button
text="비활성화됨"
color="#6c757d"
disabled={true}
onClick={handleClick}
/>
</div>
);
}
Props가 전달되지 않았을 때 사용할 기본값을 설정할 수 있다.
// 방법 1: || 연산자 사용
function Button(props) {
const color = props.color || '#007bff'; // props.color가 없으면 기본값
return <button style={{ backgroundColor: color }}>{props.text}</button>;
}
// 방법 2: defaultProps 사용 (React 전통 방식)
function Button(props) {
return <button style={{ backgroundColor: props.color }}>{props.text}</button>;
}
Button.defaultProps = {
color: '#007bff',
text: '클릭',
size: 'medium'
};
// 방법 3: 구조 분해 할당에서 기본값 (가장 현대적)
function Button({ color = '#007bff', text = '클릭', size = 'medium' }) {
return <button style={{ backgroundColor: color }}>{text}</button>;
}
매번 props.name, props.age처럼 props.를 반복하는 것은 번거롭다. 구조 분해 할당을 사용하면 코드가 훨씬 간결해진다.
function Profile(props) {
return (
<div>
<h2>{props.name}</h2>
<p>나이: {props.age}</p>
<p>직업: {props.job}</p>
<p>이메일: {props.email}</p>
</div>
);
}
After (구조 분해 할당 사용)
function Profile({ name, age, job, email }) {
return (
<div>
<h2>{name}</h2>
<p>나이: {age}</p>
<p>직업: {job}</p>
<p>이메일: {email}</p>
</div>
);
}
같은 기능이지만 훨씬 읽기 쉽다! 실무에서는 대부분 이 방식을 사용한다.
// 일반 객체 구조 분해
const user = { name: "철수", age: 25 };
const { name, age } = user;
console.log(name); // "철수"
console.log(age); // 25
// Props 구조 분해
function Greeting({ name, age }) {
return <h1>{name}님, {age}세</h1>;
}
function Button({ text = "클릭", color = "blue", size = "medium" }) {
return (
<button style={{
backgroundColor: color,
fontSize: size === 'large' ? '20px' : '14px'
}}>
{text}
</button>
);
}
// 사용
<Button /> // 모든 기본값 사용: text="클릭", color="blue", size="medium"
<Button text="저장" /> // text만 변경, 나머지는 기본값
<Button text="삭제" color="red" /> // text, color 변경
function User({ name: userName, age: userAge }) {
// name을 userName으로, age를 userAge로 받기
return <div>{userName}님은 {userAge}세입니다.</div>;
}
function Card({ title, content, ...rest }) {
// title, content를 제외한 나머지 props는 rest 객체에 담김
console.log(rest); // { author: "철수", date: "2024-01-01", ... }
return (
<div {...rest}> {/* 나머지 props를 div에 전달 */}
<h2>{title}</h2>
<p>{content}</p>
</div>
);
}
// 사용
<Card
title="제목"
content="내용"
className="card"
id="card-1"
style={{ border: '1px solid #ddd' }}
/>
function UserProfile({ user: { name, age, address: { city } } }) {
return (
<div>
<h2>{name}</h2>
<p>나이: {age}</p>
<p>도시: {city}</p>
</div>
);
}
// 사용
<UserProfile
user={{
name: "철수",
age: 25,
address: {
city: "서울",
street: "강남대로"
}
}}
/>
function UserCard({
name,
age,
job = "미입력", // 기본값
avatar = "https://via.placeholder.com/150", // 기본값
isOnline = false // 기본값
}) {
return (
<div style={{
border: '2px solid #ddd',
borderRadius: '10px',
padding: '20px',
width: '300px',
position: 'relative'
}}>
{/* 온라인 상태 표시 */}
{isOnline && (
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
width: '12px',
height: '12px',
backgroundColor: '#28a745',
borderRadius: '50%'
}} />
)}
{/* 프로필 이미지 */}
<img
src={avatar}
alt={name}
style={{
width: '100px',
height: '100px',
borderRadius: '50%',
marginBottom: '10px'
}}
/>
{/* 사용자 정보 */}
<h2 style={{ margin: '10px 0' }}>{name}</h2>
<p style={{ color: '#666' }}>{age}세 | {job}</p>
<p style={{ fontSize: '12px', color: isOnline ? '#28a745' : '#dc3545' }}>
{isOnline ? '🟢 온라인' : '⚫ 오프라인'}
</p>
</div>
);
}
// App.jsx에서 사용
function App() {
return (
<div style={{ display: 'flex', gap: '20px', padding: '20px' }}>
<UserCard
name="김철수"
age={28}
job="프론트엔드 개발자"
isOnline={true}
/>
<UserCard
name="이영희"
age={25}
job="디자이너"
isOnline={false}
avatar="https://via.placeholder.com/150/FF6B6B"
/>
<UserCard
name="박민수"
age={30}
{/* job은 기본값 "미입력" 사용 */}
/>
</div>
);
}
Children은 컴포넌트 태그 사이에 넣은 내용을 전달하는 특별한 prop이다. 일반 props와 달리 속성으로 전달하지 않는다.
// 일반 Props: 속성으로 전달
<Button text="클릭" />
// Children: 태그 사이에 전달
<Button>클릭</Button>
두 방식 모두 가능하지만, Children은 JSX 요소를 전달할 때 특히 유용하다.
// Card.jsx - 틀(레이아웃) 컴포넌트
function Card({ children }) {
return (
<div style={{
border: '1px solid #ddd',
borderRadius: '10px',
padding: '20px',
boxShadow: '2px 2px 5px rgba(0,0,0,0.1)',
margin: '10px'
}}>
{children} {/* 여기에 전달받은 내용이 들어감 */}
</div>
);
}
// App.jsx - 사용하는 쪽
function App() {
return (
<div>
{/* 간단한 텍스트 */}
<Card>
안녕하세요!
</Card>
{/* HTML 요소 */}
<Card>
<h2>제목</h2>
<p>내용입니다.</p>
</Card>
{/* 복잡한 구조 */}
<Card>
<h3>사용자 정보</h3>
<ul>
<li>이름: 김철수</li>
<li>나이: 25세</li>
</ul>
<button>더보기</button>
</Card>
</div>
);
}
function Card({ title, children, footer }) {
return (
<div style={{ border: '1px solid #ddd', padding: '20px' }}>
{/* 제목 영역 */}
{title && (
<div style={{
borderBottom: '1px solid #ddd',
paddingBottom: '10px',
marginBottom: '10px',
fontWeight: 'bold'
}}>
{title}
</div>
)}
{/* 본문 영역 (Children) */}
<div style={{ minHeight: '100px' }}>
{children}
</div>
{/* 푸터 영역 */}
{footer && (
<div style={{
borderTop: '1px solid #ddd',
paddingTop: '10px',
marginTop: '10px',
fontSize: '12px',
color: '#666'
}}>
{footer}
</div>
)}
</div>
);
}
// 사용
<Card
title="공지사항"
footer="작성일: 2024-01-01"
>
<p>중요한 공지사항 내용입니다.</p>
<p>모든 회원은 확인해주세요.</p>
</Card>
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null; // 모달이 닫혀있으면 아무것도 렌더링 안 함
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)', // 반투명 배경
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000
}}>
{/* 모달 박스 */}
<div style={{
backgroundColor: 'white',
borderRadius: '10px',
padding: '20px',
maxWidth: '500px',
width: '90%',
maxHeight: '80vh',
overflow: 'auto'
}}>
{/* 헤더 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '1px solid #ddd',
paddingBottom: '10px',
marginBottom: '20px'
}}>
<h2 style={{ margin: 0 }}>{title}</h2>
<button
onClick={onClose}
style={{
border: 'none',
background: 'none',
fontSize: '24px',
cursor: 'pointer'
}}
>
×
</button>
</div>
{/* 본문 (Children) - 여기에 다양한 내용 가능 */}
<div>
{children}
</div>
</div>
</div>
);
}
// App.jsx에서 사용
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>
모달 열기
</button>
{/* 같은 Modal 컴포넌트, 다양한 내용 */}
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="공지사항"
>
{/* 이 부분이 Modal의 children으로 전달됨 */}
<p>중요한 공지사항입니다.</p>
<ul>
<li>첫 번째 내용</li>
<li>두 번째 내용</li>
</ul>
<button onClick={() => setShowModal(false)}>
확인
</button>
</Modal>
</div>
);
}
Children을 사용하면 일관된 레이아웃을 쉽게 만들 수 있다.
// Container.jsx
function Container({ children }) {
return (
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
{children}
</div>
);
}
// Section.jsx
function Section({ title, children }) {
return (
<section style={{ marginBottom: '40px' }}>
<h2 style={{
borderBottom: '2px solid #333',
paddingBottom: '10px',
marginBottom: '20px'
}}>
{title}
</h2>
{children}
</section>
);
}
// App.jsx - 레이아웃 조립
function App() {
return (
<Container>
<Section title="최신 뉴스">
<p>뉴스 내용 1</p>
<p>뉴스 내용 2</p>
</Section>
<Section title="인기 글">
<ul>
<li>인기 글 1</li>
<li>인기 글 2</li>
</ul>
</Section>
</Container>
);
}
지금까지 배운 모든 Props 개념을 활용하여 실전 프로젝트를 만들어본다.
function Product({
image = "https://via.placeholder.com/200",
name = "상품명",
price = 0,
discount = 0,
rating = 0,
reviews = 0,
isNew = false,
isBestSeller = false,
stock = 0
}) {
// 할인가 계산
const discountedPrice = price - (price * discount / 100);
const isOutOfStock = stock === 0;
return (
<div style={{
border: '1px solid #ddd',
borderRadius: '10px',
overflow: 'hidden',
width: '250px',
position: 'relative',
opacity: isOutOfStock ? 0.6 : 1
}}>
{/* 배지들 */}
<div style={{ position: 'absolute', top: '10px', left: '10px', zIndex: 1 }}>
{isNew && (
<span style={{
backgroundColor: '#ff6b6b',
color: 'white',
padding: '5px 10px',
borderRadius: '5px',
fontSize: '12px',
fontWeight: 'bold',
marginRight: '5px'
}}>
NEW
</span>
)}
{isBestSeller && (
<span style={{
backgroundColor: '#ffd700',
color: '#000',
padding: '5px 10px',
borderRadius: '5px',
fontSize: '12px',
fontWeight: 'bold'
}}>
BEST
</span>
)}
</div>
{/* 품절 표시 */}
{isOutOfStock && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'rgba(0,0,0,0.7)',
color: 'white',
padding: '10px 20px',
borderRadius: '5px',
fontSize: '18px',
fontWeight: 'bold',
zIndex: 2
}}>
품절
</div>
)}
{/* 상품 이미지 */}
<img
src={image}
alt={name}
style={{ width: '100%', height: '200px', objectFit: 'cover' }}
/>
{/* 상품 정보 */}
<div style={{ padding: '15px' }}>
<h3 style={{
margin: '0 0 10px 0',
fontSize: '16px',
fontWeight: 'bold',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{name}
</h3>
{/* 평점 */}
<div style={{ marginBottom: '10px', fontSize: '14px' }}>
<span style={{ color: '#ffd700' }}>
{"★".repeat(rating)}{"☆".repeat(5-rating)}
</span>
<span style={{ color: '#666', marginLeft: '5px' }}>
({reviews})
</span>
</div>
{/* 가격 */}
<div>
{discount > 0 ? (
<>
<span style={{
color: '#ff6b6b',
fontWeight: 'bold',
fontSize: '18px',
marginRight: '5px'
}}>
{discount}%
</span>
<span style={{
textDecoration: 'line-through',
color: '#999',
fontSize: '14px',
marginRight: '5px'
}}>
{price.toLocaleString()}원
</span>
<div style={{
fontSize: '20px',
fontWeight: 'bold',
color: '#000',
marginTop: '5px'
}}>
{discountedPrice.toLocaleString()}원
</div>
</>
) : (
<div style={{
fontSize: '20px',
fontWeight: 'bold'
}}>
{price.toLocaleString()}원
</div>
)}
</div>
{/* 재고 표시 */}
<div style={{
marginTop: '10px',
fontSize: '12px',
color: stock < 10 ? '#ff6b6b' : '#666'
}}>
{isOutOfStock ? '품절' : `재고 ${stock}개`}
</div>
{/* 구매 버튼 */}
<button
disabled={isOutOfStock}
style={{
width: '100%',
padding: '12px',
marginTop: '15px',
border: 'none',
borderRadius: '5px',
backgroundColor: isOutOfStock ? '#ccc' : '#007bff',
color: 'white',
fontSize: '16px',
fontWeight: 'bold',
cursor: isOutOfStock ? 'not-allowed' : 'pointer'
}}
>
{isOutOfStock ? '품절' : '장바구니에 담기'}
</button>
</div>
</div>
);
}
export default Product;
2단계: ProductList 컴포넌트 (Children 활용)
function ProductList({ title, children }) {
return (
<div style={{ padding: '20px' }}>
<h1 style={{
fontSize: '32px',
marginBottom: '30px',
borderBottom: '3px solid #333',
paddingBottom: '15px'
}}>
{title}
</h1>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gap: '20px'
}}>
{children}
</div>
</div>
);
}
export default ProductList;
3단계: App.jsx에서 조립
import Product from './Product';
import ProductList from './ProductList';
function App() {
return (
<div style={{ backgroundColor: '#f5f5f5', minHeight: '100vh' }}>
<ProductList title="🔥 인기 상품">
<Product
image="https://via.placeholder.com/200/4A90E2"
name="무선 블루투스 이어폰"
price={89000}
discount={20}
rating={5}
reviews={234}
isNew={true}
isBestSeller={true}
stock={15}
/>
<Product
image="https://via.placeholder.com/200/50E3C2"
name="스마트 워치 프로"
price={350000}
discount={15}
rating={4}
reviews={89}
isBestSeller={true}
stock={5}
/>
<Product
image="https://via.placeholder.com/200/F5A623"
name="노트북 거치대"
price={45000}
rating={5}
reviews={456}
isNew={true}
stock={30}
/>
<Product
image="https://via.placeholder.com/200/BD10E0"
name="메카니컬 키보드"
price={120000}
discount={10}
rating={4}
reviews={178}
stock={0}
/>
<Product
image="https://via.placeholder.com/200/7ED321"
name="게이밍 마우스"
price={65000}
rating={5}
reviews={320}
stock={2}
/>
<Product
image="https://via.placeholder.com/200/D0021B"
name="USB-C 멀티 허브"
price={35000}
discount={25}
rating={3}
reviews={67}
isNew={true}
stock={50}
/>
<ProductList>
</div>
);
}
export default App;
| 개념 | 설명 | 예시 |
|---|---|---|
| Props | 부모 → 자식 데이터 전달 | <User name="철수" /> |
| 읽기 전용 | 자식은 props 수정 불가 | props.name = "X" ❌ |
| 구조 분해 | props. 반복 제거 | function({name, age}) |
| 기본값 | props 없을 때 사용 | {name = "익명"} |
| Children | 태그 사이 내용 | <Card>내용</Card> |