🎯 학습 목표
- 순수 함수 컴포넌트 원칙을 이해하고 지킬 수 있다
- Props의 불변성 원칙과 위반 시 발생하는 문제를 설명할 수 있다
- children prop을 활용한 컴포지션 패턴을 익힌다
- 컴포넌트를 언제 분리할지 기준을 세울 수 있다
- Props 기본값을 ES6 기본 매개변수로 설정할 수 있다
📖 핵심 개념 1 — 순수 함수 컴포넌트
React 컴포넌트는 순수 함수(pure function)여야 합니다. 순수 함수란 같은 입력에 대해 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 함수입니다.
// ✅ 순수 함수 컴포넌트 — 같은 props → 항상 같은 UI
function UserCard({ name, email, role }) {
return (
<div className="user-card">
<h3>{name}</h3>
<p>{email}</p>
<span className={`badge badge-${role}`}>{role}</span>
</div>
);
}
// ❌ 순수하지 않은 컴포넌트 — 렌더링마다 다른 결과 반환
let renderCount = 0; // 외부 변수 참조
function ImpureComponent({ name }) {
renderCount++; // 외부 상태 변경 — 금지!
return <p>{renderCount}번째 렌더: {name}</p>;
}
순수 함수 원칙을 지키면 React의 StrictMode에서 두 번 렌더링해도 동일한 결과가 나오고, 미래의 React 최적화(동시성 모드 등)와도 호환됩니다. 패널에서 “순수성을 지키지 않으면 동시성 모드(Concurrent Mode)에서 예측 불가능한 버그가 발생한다”고 강조했습니다.
📖 핵심 개념 2 — Props 불변성
Props는 부모 컴포넌트에서 자식으로 전달되는 읽기 전용 데이터입니다. 자식 컴포넌트는 절대 props를 직접 수정해서는 안 됩니다.
// ❌ Props 직접 수정 — 절대 금지!
function BadChild({ user }) {
user.name = '수정된 이름'; // props 직접 변경 — React 원칙 위반
user.score += 10; // 부모 데이터를 몰래 변경
return <p>{user.name}: {user.score}점</p>;
}
// ✅ 로컬 변수로 가공하거나 state를 사용
function GoodChild({ user }) {
// 새 객체를 만들어 가공 (원본 user는 변경 없음)
const displayName = user.name.toUpperCase();
const adjustedScore = user.score + 10;
return <p>{displayName}: {adjustedScore}점</p>;
}
// 수정이 필요하다면 부모에게 콜백으로 요청
function EditableChild({ user, onUpdate }) {
return (
<div>
<p>{user.name}</p>
{/* 직접 수정하지 않고 부모에게 위임 */}
<button onClick={() => onUpdate({ ...user, name: '새 이름' })}>
이름 변경
</button>
</div>
);
}
Props를 직접 수정하면 부모 컴포넌트의 상태를 예측 불가능하게 만들고, React의 단방향 데이터 흐름 원칙을 깨뜨립니다. 이로 인해 디버깅이 매우 어려워집니다.
📖 핵심 개념 3 — children prop
React의 모든 컴포넌트는 자동으로 children prop을 받을 수 있습니다. 이를 활용하면 HTML처럼 컴포넌트를 래퍼(wrapper)로 사용하는 컴포지션 패턴이 가능합니다.
// children을 활용한 Card 컴포넌트
function Card({ title, children, footer }) {
return (
<div className="card">
{title && <div className="card-header"><h3>{title}</h3></div>}
<div className="card-body">
{children} {/* 사용 시 태그 사이에 넣은 모든 내용 */}
</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 사용 예시 — 어떤 내용이든 Card 안에 넣을 수 있음
function App() {
return (
<div>
<Card title="사용자 정보" footer={<button>저장</button>}>
<p>이름: 김개발</p>
<p>이메일: dev@example.com</p>
</Card>
<Card title="공지사항">
<ul>
<li>서비스 점검: 3월 15일</li>
<li>신기능 출시 예정</li>
</ul>
</Card>
</div>
);
}
📖 핵심 개념 4 — 컴포넌트 분리 기준
컴포넌트를 언제 분리할지는 경험 있는 개발자도 고민하는 문제입니다. 패널에서 합의한 분리 기준은 다음과 같습니다.
- 재사용: 동일한 UI가 2곳 이상에서 쓰인다면 분리합니다.
- 복잡도: 하나의 컴포넌트가 100줄을 넘어간다면 분리를 검토합니다.
- 단일 책임 원칙(SRP): 컴포넌트가 하나의 일만 하도록 분리합니다. “이 컴포넌트가 하는 일이 뭔가요?”라는 질문에 “A와 B와 C…”처럼 여러 가지가 나온다면 분리 신호입니다.
- 독립적 테스트: 단독으로 테스트하고 싶은 로직이 있다면 분리합니다.
// 분리 전 — 하나의 컴포넌트가 너무 많은 일을 함
function UserDashboard({ userId }) {
// ... 데이터 페칭, 상태 관리, 복잡한 렌더링 모두 한 곳에
}
// 분리 후 — 각 컴포넌트가 하나의 책임만 가짐
function UserDashboard({ userId }) {
return (
<div>
<UserProfile userId={userId} />
<UserStats userId={userId} />
<UserActivityFeed userId={userId} />
</div>
);
}
💻 코드 예제 — Props 기본값과 컴포넌트 설계
// Props 기본값: ES6 기본 매개변수 방식 권장 (defaultProps는 React 19에서 deprecated)
function Button({
children,
variant = 'primary', // 기본값
size = 'medium', // 기본값
disabled = false, // 기본값
onClick,
}) {
const className = `btn btn-${variant} btn-${size}`;
return (
<button
className={className}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Avatar 컴포넌트 — 여러 곳에서 재사용
function Avatar({ src, alt, size = 40, fallback }) {
if (!src) {
return (
<div
className="avatar-fallback"
style={{ width: size, height: size }}
>
{fallback || alt?.[0]?.toUpperCase() || '?'}
</div>
);
}
return (
<img
src={src}
alt={alt}
width={size}
height={size}
className="avatar"
/>
);
}
// 사용 예시
function App() {
return (
<div>
<Button>기본 버튼</Button>
<Button variant="danger" size="large">삭제</Button>
<Button variant="secondary" disabled>비활성화</Button>
<Avatar src="/profile.jpg" alt="김개발" size={48} />
<Avatar alt="박프론트" size={32} fallback="P" />
</div>
);
}
export default App;
⚠️ 흔한 실수 (よくあるミス)
- Props를 직접 수정:
props.name = '새 이름'— React의 단방향 데이터 흐름을 깨뜨립니다. 절대 금지. - defaultProps 사용: React 19부터 deprecated입니다. ES6 기본 매개변수를 사용하세요.
- 너무 많은 Props: Props가 5개 이상 넘어간다면 객체로 묶거나 컴포넌트 분리를 검토합니다. “Props drilling”이 시작되는 신호일 수 있습니다.
- 컴포넌트 안에서 컴포넌트 정의: 렌더링마다 새 컴포넌트가 생성되어 성능 문제와 상태 초기화 버그가 발생합니다. 컴포넌트는 항상 파일 최상위에 정의하세요.
- children을 배열로 가정: children은 단일 요소, 배열, 문자열 등 다양한 형태일 수 있습니다. 배열 메서드가 필요하다면
React.Children.toArray(children)을 사용하세요.
💡 실무 팁
- 컴포넌트 파일 구조: 컴포넌트가 많아지면
src/components/Button/Button.jsx,src/components/Button/index.js형태로 폴더별로 관리하면 임포트가 깔끔해집니다. - Storybook 도입 검토: 중규모 이상 프로젝트에서는 Storybook으로 컴포넌트를 독립적으로 문서화하고 시각적으로 테스트하는 것이 생산성을 크게 높입니다.
- PropTypes vs TypeScript: 런타임 타입 검사가 필요하다면 PropTypes, 정적 타입 검사를 원한다면 TypeScript를 사용합니다. 현업에서는 TypeScript가 표준입니다 (13강에서 다룹니다).
- 컴포넌트 네이밍: 컴포넌트 이름은 기능을 명확하게 드러내야 합니다.
Card보다UserProfileCard가,List보다ProductList가 명확합니다.