최근 옵션 패널 UI를 구현하면서,
컴포넌트를 좀 더 범용적으로 만들고 싶다는 욕심이 생겼다.
그래서 TypeScript의 제네릭(Generic)을 이용한 추상화를 시도했다.
결론부터 말하자면, 결과는 “굳이 그럴 필요는 없었다”였다.
1. 시작은 단순한 옵션 패널이었다
처음 작성했던 코드는 아래와 같다.
입력창, 슬라이더, 색상 선택 UI를 나란히 배치하는 단순한 구조였다.
<div className="bg-black flex-2 flex flex-col gap-8 h-full w-full items-center justify-center p-8">
<TextInput value={inputValue} onChange={setInputValue} />
<FontSizeSlider value={baseFontSize} onChange={setBaseFontSize} />
<ColorPicker
label="배경색"
value={bgColor}
onChange={setBgColor}
id="bgColor"
/>
<ColorPicker
label="글자색"
value={fontColor}
onChange={setFontColor}
id="fontColor"
/>
</div>
각 컴포넌트는 label + 입력 영역으로 구성되어 있었고,
비슷한 형태의 레이아웃이 반복되고 있었다.
또한 앞으로 다른 옵션(예: 테두리, 정렬 등) 을 추가할 계획도 있었기 때문에,
이 구조를 그대로 늘려가면 관리가 점점 어려워질 것 같았다.
그래서 “이 패턴을 하나의 컴포넌트로 추상화할 수 있지 않을까?”라는 생각을 하게 되었다.
2. 제네릭으로 추상화를 시도하다
그때 예전에 봤던 한 블로그 글이 떠올랐다.
[React] 컴포넌트를 제네릭 함수로 쓰기 - 테이블 컴포넌트 추상화
귀찮은 일은 꽤나 자주 일어난다. 외주 작업 중 테이블 컴포넌트를 구현해야하는 일이 있었고, 기존 코드는 Core UI라는 라이브러리를 사용하고 있었다. 라이브러리가 4.0버전으로 업그레이드 되
9yujin.tistory.com
<table> 형태로 데이터 타입에 따라 유연하게 렌더링되는 컴포넌트였다.</table
User, Post, Notice 같은 여러 데이터 구조를 하나의 Table로 처리하는 구조였다.
그걸 보고 이렇게 생각했다.
“나도 이런 식으로 제네릭을 써서,
OptionRow<string>, OptionRow<number> 같은 컴포넌트를 만들면 더 깔끔하겠지.”
그래서 나는 OptionRow를 만들고,
제네릭을 이용해 타입을 지정하려고 했다.
3. 그런데, 제네릭이 전혀 필요하지 않았다
직접 작성하면서 금방 이상하다는 걸 느꼈다.
내가 만든 OptionRow는 이렇게 생겼다.
<OptionRow label="텍스트" htmlFor="inputText">
<TextInput value={inputValue} onChange={setInputValue} />
</OptionRow>
그런데 OptionRow 내부에서는
value나 onChange 같은 데이터를 전혀 다루지 않았다.
단지 label과 children을 배치하는 UI 틀일 뿐이었다.
즉, <OptionRow<string>> 같은 제네릭은
타입 정보를 넘기긴 하지만, 내부에서 전혀 쓰이지 않았다.
이건 단순히 “제네릭을 쓴 것처럼 보이는 코드”일 뿐이었다.
4. 제네릭은 데이터, children은 UI
이 과정을 통해 명확히 정리할 수 있었다.
제네릭은 데이터 구조가 달라질 때,
children은 UI 구조가 달라질 때 사용하는 것이 맞다.
내가 만든 OptionRow는 데이터에 대응하는 컴포넌트가 아니라,
UI를 조립하기 위한 레이아웃 컴포넌트였다.
따라서 타입 안전성을 보장하는 제네릭보다는,
children 합성을 통해 UI를 유연하게 구성하는 게 훨씬 자연스러웠다.
5. 기술은 ‘왜 쓰는가’로 구분된다
솔직히 말하면,
그때 나는 제네릭을 잘 알고 있어서 쓴 게 아니었다.
그냥 “나도 제네릭을 알고 있다”는 걸 보여주고 싶었다.
하지만 막상 써보니 내 컴포넌트는 데이터를 다루는 구조가 아니었다.
엘리먼트가 바뀌는 상황에서는 제네릭보다 children이 훨씬 깔끔했다.
결국 제네릭은 다 지우고, React가 의도한 방식대로 Composition으로 바꿨다.
6. 결론
이번 시도를 통해 제네릭과 children의 역할 구분을 명확히 이해할 수 있었다.
- 제네릭은 타입에 따라 동작이 달라질 때만 의미가 있다.
단순히 컴포넌트에 <T>를 붙이는 것은 ‘타입 추상화’가 아니라, 문법의 남용일 수 있다. - UI가 달라질 때는 children으로 충분하다.
React의 Composition 철학과 가장 잘 맞는 방식이며, 구조적 유연성을 확보할 수 있다. - 추상화의 목적을 먼저 정의해야 한다.
“무엇을 유연하게 만들고 싶은가?”를 모른 채 일반화를 시도하면, 코드만 복잡해진다.
결국 추상화는 문법이 아니라 의도의 문제다.
데이터를 다룰 때는 제네릭이,
UI를 조립할 때는 children이 각각 제 역할을 한다.
“제네릭을 써야 React + TypeScript다운 코드다”라는 강박보다는,
내가 만든 컴포넌트가 데이터 중심인지, UI 중심인지
그 본질을 먼저 구분하는 것이 더 중요하다는 점을 이번에 확실히 배웠다.