Skip to content

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>

컴포넌트 역할

Dropdown.tsx

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.tsx

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>
  );
}

Input.tsx

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>
    )}
  </>
);