forked from f-lab-edu/jiary
-
Notifications
You must be signed in to change notification settings - Fork 0
05. Compound Component Pattern
pozafly edited this page Sep 13, 2023
·
1 revision
Compound Component Pattern은 하나의 컴포넌트를 여러 개의 컴포넌트로 분리하고, 사용하는 쪽에서 조합하여 사용하는 컴포넌트 패턴입니다.
변화에 유연하게 대처가 가능하고, 컴포넌트가 세부적으로 나뉘어 있기 때문에 조합할 때 다르게 조합하여 사용 가능합니다. 또한, 일반 HTML과 비슷하게 생겼으므로 컴포넌트 내부 구조를 파악하기 쉽습니다.
Dropdown은 여러 곳에서 사용될 수 있으며 대상 Element를 클릭하면 하단에 생성 되고 외부를 클릭하면 닫히는 공통적인 기능을 갖습니다. 하지만, Dropdown 내부에 들어가는 요소는 다를 수 있습니다. 따라서 Compound Component Pattern을 이용해 작성하는 것이 좋겠다고 생각해 구현했습니다.
<Dropdown>
<Dropdown.Trigger className={style.newButton}>New</Dropdown.Trigger>
<Dropdown.List width="200px">
<Dropdown.Title title="제목을 입력해주세요" />
<Dropdown.Input
validation={{
required: '👋 제목 입력은 필수 입니다.',
maxLength: 30,
}}
value={inputValue}
onChange={handleOnChange}
onKeyUp={handleKeyUp}
/>
<Dropdown.SubmitButton
onClick={handleCreateDoc}
disabled={isDisabled}
>
제출
</Dropdown.SubmitButton>
</Dropdown.List>
</Dropdown>
Container의 역할로, Dropdown 전체 상태 값을 Context API로 관리합니다. 합성할 수 있는 새로운 컴포넌트를 등록합니다.
export default function Dropdown({ children }: { children: ReactNode[] }) {
(...)
return (
<DropdownContext.Provider value={contextValue}>
{children}
</DropdownContext.Provider>
);
}
Dropdown.Input = Input;
Dropdown.Title = Title;
(...)
주요 내부 컴포넌트는 Trigger, Input 컴포넌트입니다.
Trigger는 클릭했을 때, Dropdown을 열 수 있는 기능을 가지고 있으며, 대상이 되는 컴포넌트를 children으로 받아 렌더링 합니다. className을 props로 받아 Trigger 컴포넌트에 스타일을 입힐 수 있습니다.
export default function Trigger({ children, className }: Props) {
const { isShow, setIsShow, setTriggerRef } = useContext(DropdownContext);
const triggerRef = useCallback(
(node: HTMLButtonElement) => setTriggerRef(node),
[setTriggerRef],
);
return (
<button
className={className}
onClick={() => setIsShow(!isShow)}
ref={triggerRef}
>
{children}
</button>
);
}
validation props로 input 자체적인 검증 로직을 사용할 수 있습니다.
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const input = inputRef.current as HTMLInputElement | null;
if (validation?.required) {
setIsValid(!!input?.value);
}
onChange(e);
};
return (
<>
<input
className={style.input}
type="text"
maxLength={validation?.maxLength}
value={value}
ref={inputRef}
onChange={handleChange}
onKeyUp={handleKeyUp}
/>
{!isValid && (
<span className={style.requiredText}>{validation?.required}</span>
)}
</>
);