이를 헤드리스 컴포넌트라 하는데 난해해 보일 수 있지만, 그 진정한 힘은 유연성, 재사용 가능성, 코드베이스의 구성과 깔끔함을 개선하는 능력에 있다.
이 글에서는 이 패턴이 정확히 무엇인지, 왜 유용한지, 인터페이스 디자인에 대한 접근 방식을 어떻게 혁신할 수 있는지를 조명하면서 이 패턴에 대해 설명한다.
예를 들어보면
const ToggleButton = () => {
const [isToggled, setIsToggled] = useState(false);
const toggle = useCallback(() => {
setIsToggled((prevState) => !prevState);
}, []);
return (
<div className="toggleContainer">
<p>Do not disturb</p>
<button onClick={toggle} className={isToggled ? "on" : "off"}>
{isToggled ? "ON" : "OFF"}
</button>
</div>
);
};
간단하게 버튼을 클릭할 때 마다 isToggled state의 값이 true와 false 사이에서 전환되는 컴포넌트가 있다.
이제 섹션의 세부 정보를 표시하거나 숨길 수 있는 완전히 다른 컴포넌트인 collapsableSection를 작성해보자.
const collapsableSection = ({ title, children }: ExpandableSectionType) => {
const [isOpen, setIsOpen] = useState(false);
const toggleOpen = useCallback(() => {
setIsOpen((prevState) => !prevState);
}, []);
return (
<div>
<h2 onClick={toggleOpen}>{title}</h2>
{isOpen && <div>{children}</div>}
</div>
);
};
여기서 ToggleButton의 켜고 끄는 동작과 collapsableSection의 펼치고 접는 동작은 서로 유사하다.
이러한 공통점을 인식하면 이 공유 기능을 별도의 함수로 추상화 할 수 있게 된다.
여기서 유사점이 있는 기능만 React Custom hook으로 빼보면 이런 모양이 될 것이다.
const useToggle = (init = false) => {
const [state, setState] = useState(init);
const toggle = useCallback(() => {
setState((prevState) => !prevState);
}, []);
return [state, toggle];
};
동작과 프레젠테이션을 분리하는 중요한 개념을 강조하며 이 Custom hook은 JSX와 독립적인 상태로 역할을 한다.
일정 규모 이상의 프로젝트에 시간을 투자해 본 사람이라면 대부분 업데이트나 버그가 UI 비주얼 부분이 아니라 로직과 관련이 있다는 것을 쉽게 알 수 있다.
Hook은 이러한 논리적 측면을 중앙 집중화하고 유지 관리하기 쉽게 만들 수 있다.
실제로 이 패턴을 사용하여 동작과 UI를 분리하는 라이브러리들이 이미 존재하며 가장 유명한 라이브러리는 Downshift이다.
Downshift는 UI를 랜더링하지 않고 동작과 상태를 관리하는 헤드리스 컴포넌트라는 개념을 적용한다.
const StateSelect = () => {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items: states});
return (
<div>
<label {...getLabelProps()}>Issued State:</label>
<div {...getToggleButtonProps()} className="trigger" >
{selectedItem ?? 'Select a state'}
</div>
<ul {...getMenuProps()} className="menu">
{isOpen &&
states.map((item, index) => (
<li
style={
highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
}
key={`${item}${index}`}
{...getItemProps({item, index})}
>
{item}
</li>
))}
</ul>
</div>
)
}
Downshift의 useSelect hook을 활용한 dropdown list 컴포넌트이다.
이처럼 활용했을 때 여러 컴포넌트나 프로젝트에서 동작 로직을 공유할 수 있어 유용하다.
다른 헤드리스 컴포넌트 라이브러리들도 있다.
이렇게 로직과 UI를 계속 분리하면 점차 계층화된 구조가 만들어진다.
이 구조는 애플리케이션 전체의 계층화된 아키텍처가 아니라 애플리케이션의 UI 부분에 한정된 구조를 말한다.
이 구조에서 JSX는 전달된 프로퍼티를 표시하는 역할을 담당한는 최상위 레이어에 정의된다.
그 바로 아래에 컴포넌트의 모든 동작을 유지하고, 상태를 관리하며, JSX가 상호작용할 수 있는 인터페이스를 제공하는 헤드리스 컴포넌트가 위치한다.
이 구조의 기반에 도메인별 로직을 캡슐화하는 데이터 모델이 있고 이 모델은 UI나 상태와는 관련이 없다.
대신 데이터 관리와 비즈니스 로직에 집중한다.
이러한 계층적 접근 방식은 관심사를 깔끔하게 분리하여 코드의 명확성과 유지보수성을 향상시킨다.
정리하자면
헤드리스 UI는 다른 아키텍처 패턴처럼 만능 솔루션이 아니다.
구체적인 요구 사항과 복잡성에 따라 개발자가 잘 판단해서 사용하는 것이 중요할 것 같다.