[React] Component UI의 재사용성

Sunghoman·2022년 11월 27일

React

목록 보기
4/5
post-thumbnail

재사용성을 고려한 UI Component

서비스가 커질수록, 매번 UI Component를 직접 작성하는 일은 비효율적이고, 유지 보수성이 매우 떨어짐

그동안 styled-components를 이용해서 css 작업을 했음.

근데, components의 size는 매번 렌더링 되는 페이지에 맞춰서 직접 작성해왔는데,

새로운 component를 만들어야 할 때마다, 이전에 만들어 둔 component를 재사용하고 싶었음

근데, 최근에 괜찮은 방법을 알아서 적어보고자 함.

Step 1. 최초 구현

interface Props {
    isOpen: boolean;
    title: string;
    buttonLabel: string;
    onClickButton: (e:MouseEvent) => void;
}

function Dialog({ isOpen, title, buttonLabel, onClickButton }: Props){
    if (!isOpen){
        return null;
    }
    return React.createPortal(
        <div>
            <span>{title}</span>
            <button onClick={onClickButton}>{buttonLabel}</button>
        </div>
    ,document.body)
}

Step 2. 추가 요구사항

이런! 추가 요구사항이 발생했다!

Dialog에 체크박스나 버튼이 더 생기기도 하고, 주석이나 설명 등과 같은 부가적인 요소가 생겼다!

interface Props {
    isOpen: boolean;
    title: string;
    buttonLabel: string;
    onClickButton: (e: MouseEvent) => void;
    isChecked?: boolean;
    checkBoxLabel?: string;
    onClickCheckBox? : (e: MouseEvent) => void;
    descriptionList?: string[]
}

function Dialog({ 
        isOpen, 
        title, 
        buttonLabel, 
        onClickButton, 
        isChecked, 
        checkBoxLabel, 
        onClickCheckBox, 
        descriptionList 
    }: Props){
     if (!isOpen){
        return null;
    }
    return React.createPortal(
        <div>
            <span>{title}</span>
            {descriptionList && descriptionList.map(desc => <span key={desc}>{desc}</span>)}
            {checkBoxLabel && <div>
                <input checked={isChecked} onClick={onClickCheckBox} type="checkbox" id="dialog_checkbox">
                <label for="dialog_checkbox">{checkBoxLabel}</label>
            </div>}
            <button onClick={onClickButton}>{buttonLabel}</button>
        </div>
    ,document.body)
}

이 시점에서 우리는 여러 생각이 든다.

앞으로 얼마나 더 많은 요구사항이 추가될까?

버튼 하나의 추가만으로, 3개의 props가 늘어남.

Step 3. 합성 컴포넌트의 도입

Step 3-1. 서브 컴포넌트 구현

interface DialogTitleProps {
    children?: ReactNode;
}
function DialogTitle({children}: DialogTitleProps){
    return <div css={/*DialogTitle 스타일*/}>{children}</div>
}

interface DialogLabelButtonProps {
    children?: ReactNode;
    onClick?: (e: MouseEvent) => void;
}
function DialogLabelButton({children}: DialogLabelButtonProps){
    return <div css={/*DialogLabelButton 스타일*/}>{children}</div>
}

Step 3-2. 메인 컴포넌트 구현

interface DialogMainProps {
    children?: ReactNode;
    isOpen: boolean;
}
function DialogMain({children, isOpen}: DialogMainProps){
    if(!isOpen) {
        return null;
    }
    return createPortal(<div>{children}</div>, document.body)
}

이러면 children으로 들어오는 서브 컴포넌트들은 순서에 따라 위 -> 아래로 배치될 듯

그리고 컴포넌트들을 분류해서 어느정도 위치를 잡아줄 수 있도록 구현

const DialogLabelButtonType = (<DialogLabelButton />).type;
function getDialogLabelButtons(children: ReactNode) {
  const childrenArray = Children.toArray(children);
  return childrenArray
    .filter(
      child => isValidElement(child) && child.type === DialogLabelButtonType,
    )
    .slice(0, 2);
}

interface DialogMainProps {
    children?: ReactNode;
    isOpen: boolean;
}

function DialogMain({children, isOpen}: DialogMainProps){
    if(!isOpen) {
        return null;
    }
    const dialogContents = getDialogContents(children);
    const dialogLabelButtons = getDialogLabelButtons(children);
    const dialogDimmed = getDialogDimmed(children);
    
    return createPortal(
        <div>
            <div>{getDialogDimmed(children)}</div>
            {dialogContents && (
                <div>{dialogContents}</div>
            )}
            {dialogLabelButtons && (
                <div>{dialogLabelButtons}</div>
            )}
        </div>,
    document.body)
}

Step 3-3. 메인 & 서브 컴포넌트들을 묶어서 export

export const Dialog = Object.assign(DialogMain, {
  Dimmed: DialogDimmed,
  Title: DialogTitle,
  Subtitle: DialogSubtitle,
  Description: DialogDescription,
  Comment: DialogComment,
  CheckButton: DialogCheckButton,
  CheckBox: DialogCheckBox,
  TextButton: DialogTextButton,
  Button: DialogButton,
  LabelButton: DialogLabelButton,
  Divider: DialogDivider,
});

// Usage
<Dialog>
    <Dialog.Title>제목</Dialog.Title>
</Dialog>
profile
쉽게만 살아가면 개재밌어 빙고

0개의 댓글