화면 구조를 컴포넌트를 나열한 코드가 아니라 설정 데이터(보통 배열)로 선언하고, 렌더러가 그 설정을 읽어 그리는 패턴. 구조를 바꿀 때 렌더링 코드 대신 데이터만 고친다.
type Section =
| { type: 'banner'; id: string; image: string }
| { type: 'products'; id: string; heading: string; query: string }
| { type: 'custom'; id: string; render: () => ReactNode; shouldRender?: () => boolean }
const PAGE: Section[] = [
{ type: 'banner', id: 'hero', image: '/hero.png' },
{ type: 'products', id: 'best', heading: '베스트', query: 'sort=best' },
]
function Page() {
return PAGE.filter(s => s.type !== 'custom' || (s.shouldRender?.() ?? true)).map(s => {
switch (s.type) {
case 'banner': return <Banner key={s.id} src={s.image} />
case 'products': return <ProductList key={s.id} heading={s.heading} query={s.query} />
case 'custom': return <Fragment key={s.id}>{s.render()}</Fragment>
}
})
}이 구조는 몇 가지 생각이 겹친 것이다.
- 선언적 렌더링(Declarative Rendering): 어떻게 그릴지가 아니라 무엇을 그릴지를 적는다(선언형과 명령형 프로그래밍)
- 판별 유니언(Discriminated Union):
type필드로 항목 종류를 나눠 각 항목에 맞는 속성을 타입으로 보장한다(판별 유니언) - 전략(Strategy): 항목마다
render·shouldRender함수를 두면 렌더링 방식을 항목이 고른다(전략 패턴)
같은 설정 배열에서 탭 메뉴·목차·스크롤 위치 같은 파생 UI도 함께 만들 수 있어, 구조 정보가 한곳에만 있게 된다(DRY 원칙). 라우터의 라우트 설정, 폼 라이브러리의 필드 설정, Storybook의 스토리 설정(Storybook)이 같은 계열이다.
설정에 조건과 예외가 계속 늘면 설정 자체가 읽기 어려운 작은 언어가 된다. 항목이 몇 개뿐이라면 컴포넌트를 그냥 나열하는 편이 단순하다(YAGNI).