[실무] refactoring Tag component

Jang Seok Woo·2022년 9월 30일
0

실무

목록 보기
130/136

기존의 UI컴포넌트인 Tags 컴포넌트를 리팩토링하는 Task를 진행하였다.

아래 기존코드를 보면 renderTag prop이 Tag컴포넌트의 children을 render하도록 작성이 되어있는데,
renderTag라 함은 tag 자체를 render하는 것이 더 합리적이라고 생각하여 해당 구조를 변경하게 되었다.

import { isObject } from 'lodash';
import React from 'react';
import styled from 'styled-components';

import Link from 'components/Link';
import type { LinkProps } from 'components/Link';
import Stack from 'components/Stack';
import type { StackProps } from 'components/Stack';
import Tag from 'components/Tag';
import type { TagProps } from 'components/Tag';

interface ITag extends Pick<LinkProps, 'openInNew'>, Pick<TagProps, 'disabled'> {
  to?: LinkProps['to'];
  label?: string;
  [x: string]: any;
}

interface TagsProps extends Pick<TagProps,
  'size'
  | 'solid'
  | 'rounded'
> {
  tags: Array<ITag>;
  className?: string;
  disabled?: boolean;
  getId?: (tag: ITag) => string | number;
  onClick?: (tag: ITag) => void;
  onRemove?: (tag: ITag) => void;
  renderTag?: (tag: ITag) => React.ReactNode;
  renderTagContainer?: (params: {
    tag: ITag,
  } & Pick<TagsProps,
    'onClick'
    | 'onRemove'
    | 'disabled'
    | 'renderTag'
  >) => React.ReactNode;
  spacing?: StackProps['spacing'];
}

const defaultProps: Partial<TagsProps> = {
  className: undefined,
  disabled: false,
  getId: undefined,
  onClick: undefined,
  onRemove: undefined,
  renderTag: (obj) => obj.label,
  renderTagContainer: undefined,
  spacing: 's',
};

// ====

const Root = styled.div``;

const Tags = ({
  tags,
  size,
  solid,
  spacing,
  rounded,
  disabled,
  onClick,
  onRemove,
  renderTag,
  renderTagContainer,
  getId,
  className,
}: TagsProps) => (
  <Root className={className}>
    <Stack spacing={spacing}>
      {
        tags.map((tag, i) => {
          const { to = '', openInNew = false } = isObject(tag) ? tag : {};
          const id = getId ? getId(tag) : String(i);
          return (
            <Stack.Item key={id}>
              {
                renderTagContainer
                ? renderTagContainer({
                    tag,
                    onClick,
                    onRemove,
                    disabled,
                    renderTag,
                  })
                : (
                  to
                  ? (
                    <Link to={to} openInNew={openInNew}>
                      <Tag
                        size={size}
                        solid={solid}
                        rounded={rounded}
                        disabled={tag.disabled || disabled}
                        onClick={onClick && (() => onClick(tag))}
                        onRemove={onRemove && (() => onRemove(tag))}
                      >
                        {renderTag(tag)}
                      </Tag>
                    </Link>
                  )
                  : (
                    <Tag
                      size={size}
                      solid={solid}
                      rounded={rounded}
                      disabled={tag.disabled || disabled}
                      onClick={onClick && (() => onClick(tag))}
                      onRemove={onRemove && (() => onRemove(tag))}
                    >
                      {renderTag(tag)}
                    </Tag>
                  )
                )
              }
            </Stack.Item>
          );
        })
      }
    </Stack>
  </Root>
);

Tags.defaultProps = defaultProps;

export default Tags;
export type { TagsProps };

변경된 구조

import React from 'react';

import Stack from 'components/Stack';
import type { StackProps } from 'components/Stack';
import Tag from 'components/Tag';
import type { TagProps } from 'components/Tag';

interface TagsProps
  extends Pick<TagProps,
  'size'
  | 'solid'
  | 'rounded'
  | 'disabled'> {
  tags: Array<TagProps>,
  className?: string,
  getId?: (tag: TagProps) => string,
  onTagClick?: () => void,
  onTagRemove?: () => void,
  renderTag?: (
    tag: TagProps,
    renderTagContent: (tag: TagProps) => React.ReactNode,
    tagProps: TagProps,
  ) => React.ReactNode,
  renderTagContent?: (tag: TagProps) => React.ReactNode,
  spacing?: StackProps['spacing'];
}

const defaultProps: Partial<TagsProps> = {
  tags: [],
  className: '',
  disabled: undefined,
  getId: undefined,
  onTagClick: undefined,
  onTagRemove: undefined,
  renderTag: (
    tag,
    renderTagContent,
    {
      id,
      disabled,
      onClick,
      onRemove,
      size,
      solid,
      rounded,
      to,
      ...linkProps
    },
  ) => (
    <Tag
      {...linkProps}
      key={id}
      disabled={disabled}
      onClick={onClick}
      onRemove={onRemove}
      size={size}
      solid={solid}
      rounded={rounded}
      to={to}
    >
      {renderTagContent(tag)}
    </Tag>
  ),
  renderTagContent: (tag: TagProps) => tag.label,
  spacing: 's',
};

// ====

const Tags = ({
  tags,
  className,
  disabled,
  getId,
  onTagClick,
  onTagRemove,
  renderTag,
  renderTagContent,
  rounded,
  size,
  solid,
  spacing,
}: TagsProps) => (
  <Stack
    className={className}
    spacing={spacing}
  >
    {
      tags.map((tag, i) => {
        const id = getId ? getId(tag) : String(i);
        return (
          renderTag(
            tag,
            renderTagContent,
            {
              ...tag,
              id,
              rounded,
              size,
              solid,
              disabled: disabled || tag.disabled,
              onClick: onTagClick,
              onRemove: onTagRemove,
            },
          )
        );
      })
    }
  </Stack>
);

Tags.defaultProps = defaultProps;

export default Tags;
export type { TagsProps };

이렇게 구현하게되면 무엇보다 prop의 name과 그 역할이 더 명확하고 잘 구분이 되게 된다.
renderTag는 Tag 그 자체를 return하며, renderTagContent는 Tag 컴포넌트 하에서 children을 render하게 된다.

위 코드와 아래 코드는 같은 목적을 위해 사용될 컴포넌트이지만, 명확하게 Prop의 역할을 구분짓고 협업시 컴포넌트의 코드에 대한 가독성을 높여줄 것으로 기대한다.

profile
https://github.com/jsw4215

0개의 댓글