React 디자인 패턴 완벽 가이드: Compound Components부터 Custom Hooks까지
TL;DR - 이 글의 핵심
- Compound Components: 유연한 UI 구조가 필요할 때
- Render Props: 로직은 공유, UI는 위임할 때
- Custom Hooks: 상태 로직을 재사용할 때
- State Reducer: 제어 역전이 필요할 때
React Design Patterns
시니어 개발자로 가는 길
React로 개발하다 보면 “어떻게 하면 더 깔끔하게 짤 수 있을까?”라는 고민을 하게 됩니다. 이 글에서는 실무에서 자주 사용되는 React 디자인 패턴들을 코드와 함께 상세히 알아봅니다.
디자인 패턴을 배우면 코드의 재사용성과 유지보수성이 크게 향상됩니다. 특히 팀 프로젝트에서 일관된 패턴을 사용하면 코드 리뷰와 협업이 훨씬 수월해집니다. 더 자세한 내용은 Compound Components 심화 가이드에서 확인하세요.
1Compound Components 패턴
Compound Components는 여러 컴포넌트가 암묵적으로 상태를 공유하면서 하나의 완전한 UI를 구성하는 패턴입니다. HTML의 <select>와<option>의 관계를 생각하면 이해하기 쉽습니다.
문제 상황: Props 지옥
아래와 같은 Modal 컴포넌트가 있다고 가정해봅시다.
<Modal
isOpen={isOpen}
onClose={onClose}
title="삭제 확인"
description="정말로 삭제하시겠습니까?"
showCloseButton={true}
showFooter={true}
primaryButtonText="삭제"
secondaryButtonText="취소"
onPrimaryClick={handleDelete}
onSecondaryClick={onClose}
size="medium"
centered={true}
// ... 더 많은 props
/>Props가 10개가 넘어가면 유지보수가 어려워집니다. 새로운 요구사항이 생길 때마다 props가 늘어나죠.
해결책: Compound Components
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<Modal.Title>삭제 확인</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<p>정말로 삭제하시겠습니까?</p>
<p className="text-red-500">이 작업은 되돌릴 수 없습니다.</p>
</Modal.Body>
<Modal.Footer>
<Button variant="ghost" onClick={onClose}>취소</Button>
<Button variant="destructive" onClick={handleDelete}>삭제</Button>
</Modal.Footer>
</Modal>핵심 포인트
사용하는 쪽에서 구조를 자유롭게 결정할 수 있습니다. Footer가 필요 없으면 빼면 되고, Body에 뭘 넣을지도 마음대로입니다.
2Render Props 패턴
Render Props는 로직은 공유하되 UI는 사용처에서 결정하게 하는 패턴입니다. “어떻게 렌더링할지”를 함수로 전달받습니다.
마우스 위치 추적 예제
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// UI는 사용하는 쪽에서 결정
return render(position);
}
// 사용 예시
<MouseTracker
render={({ x, y }) => (
<div>마우스 위치: ({x}, {y})</div>
)}
/>3Custom Hooks 패턴
React Hooks가 등장하면서 가장 널리 사용되는 패턴이 됐습니다. 상태 로직을 재사용 가능한 함수로 추출합니다. 관련 내용은 Custom Hooks 실전 가이드에서 더 자세히 다룹니다.
function useAsync<T>(asyncFn: () => Promise<T>) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({ data: null, loading: true, error: null });
useEffect(() => {
setState(prev => ({ ...prev, loading: true }));
asyncFn()
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, []);
return state;
}
// 사용 예시
function UserProfile({ userId }) {
const { data: user, loading, error } = useAsync(
() => fetchUser(userId)
);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <Profile user={user} />;
}4State Reducer 패턴
State Reducer는 사용자가 상태 변경 로직을 커스터마이징할 수 있게 합니다. Kent C. Dodds가 만든 패턴으로, 라이브러리 개발에 특히 유용합니다.
언제 사용하나요?
컴포넌트 라이브러리를 만들 때, 기본 동작은 제공하되 사용자가 필요에 따라 동작을 오버라이드할 수 있게 하고 싶을 때 사용합니다.
마치며
디자인 패턴은 은탄환이 아닙니다. 상황에 따라 적절한 패턴을 선택하는 것이 중요합니다.
Compound Components
유연한 UI 구조가 필요할 때
Render Props
로직 공유, UI는 위임할 때
Custom Hooks
상태 로직을 재사용할 때
State Reducer
제어 역전이 필요할 때