🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] Avatar Component 구현기
    Front-End/React 2024. 5. 9. 19:52
    🍀 목차 
    Avatar Component에는 어떤 속성이 필요할까?
    Interface 구현기
    UI 구현기
    회고

     

     

    Avatar Component에는 어떤 속성이 필요할까?

     

     Avatar Component는 사용자 또는 개체를 대표하는 이미지를 표시하는 데 사용된다. 예를 들어 프로필 이미지, 쇼핑 사이트의 상점 프로필 등이 포함된다. 다른 디자인 라이브러리를 보며 컴포넌트에 어떤 속성들이 필요할지 참고해 보자.

     

     

    (필수) radius, size, imageUrl, altText

    Ant Design Avatar
    Ant Design - Basic Avatar
    Radix Avatar
    Radix

     

     가장 익숙한 형태의 기본 Avatar를 구현하려면 Border Radius와 size, 그리고 img 태그의 src 속성에 전달할 image의 url과 alt 속성에 전달할 대체 텍스트인 altText가 필요할 것으로 예상할 수 있다. 

     

     

    (선택) badge

    Ant Design - With Badge Avatar
    Ant Design - With Badge Avatar

     

    Geist Design System - Git 아이콘 뱃지
    Geist Design System - Git 아이콘 뱃지

     

      유저가 확인해야 할 정보가 있음을 알려주거나 부가적인 정보를 badge, icon을 사용하여 나타낼 수도 있다.

     

     

    (선택) grouping 

     

    Ant Design - Avatar.Group
    Ant Design - Avatar.Group
    Geist Design System - Group
    Geist Design System - Group

     

     group은 여러 Avatar를 하나로 묶어 표시한다. 사용자들의 그룹, 팀에 속한 멤버들을 한데 묶어 보여줄 때 유용하다.

    group, badge 모두 프로젝트에 따라 선택적으로 구현할 속성들이다.

     

     

     

    Interface 구현기

    Avatar Interface

    interface AvatarProps {
    	imageUrl?: string; //처음에는 src, 피드백 받아 수정
        altText: string;
      	badgeIcon?: ReactNode;
      	badgeIconBackgroundColor?: keyof typeof colors;
      	radius?: Radius; //type Radius
      	size?: Size; //type Size
    }

     

     인터페이스의 초안은 이랬다.

    Avatar는 이미지를 표시하는 AvatarImage와 뱃지를 표시하는 AvatarBadgeIcon으로 나뉘는데 이를 합성 컴포넌트로 구성하면 재사용성이 높아질 것 같다는 피드백을 받았다. 추천받은 자료는 합성 컴포넌트로 재사용성 극대화하기이다.

     

     

    AvatarGroup Interface

     

     AvatarGroup를 구성하며 헤매게 되었는데, 그 이유는 합성 컴포넌트에 대한 이해도가 낮은 상태로 무작정 Ant Design의 AvatarGroup 형태를 따라 하려 했기 때문이었다.

     

     

    Antd - Avatar Group code
    Antd - Avatar Group code

     

    <Avatar.Group>
    	<Avatar />
        <Avatar />
    </Avatar.Group>

     

     깊게 생각하지 않고 해당 형태를 가져가도록 구현해야지~ 하며 아래와 같이 export 시켰다.

    export const Avatar = Object.assign(AvatarMain, {
      Image: AvatarImage,
      BadgeIcon: AvatarBadgeIcon,
      Group: AvatarGroup,
    });

     

     Avatar를 포함하는 더 큰 범위인 AvatarGroup이 Avatar를 구성하는 요소인 Image와 BadgeIcon과 같은 계층인 게 이상하다고 생각은 했지만 어떻게 분리해야 할지 감이 안 왔다. 

     감을 잡기 위해서는 AvatarGroup 속성들을 제대로 알아야 했다. Avatar와 공유하는 속성이 없다면 둘을 다른 하나의 서브 컴포넌트로 묶는 게 어색해지기 때문이다.

     

     

    Antd
    Antd

     

     Ant Design의 경우는 그룹 전체 size와 shape 속성을 가지며 Avatar 요소를 몇 개 보여줄지 결정하는 maxCount로 Avatar 수가 그를 초과하면 초과숫자를 표시해 주는 것으로 보인다.

     

     

    Geist Design System
    Geist Design System

     

    Geist의 경우도 선택 속성 limit로 보여줄 member 수를 조절한다.

     

     

     

     회사 프로젝트에서 사용되는 AvatarGroup은 어떤 데이터들이 들어오는지 알고 싶었고, 동료분이 개발자 도구의 네트워크 탭을 활용해서 어떤 데이터가 들어오고 있는지 알 수 있는 미리보기 기능을 알려주셨다. 그렇게 알게 된 형태는 아래와 같다.

    data:
    	...
        recent_users: [{id: 7777, profile_image: "https://media...", username: "oz", ...} ...] // length: 3
        user_count: 8

     

     서버에서 특정 기준으로 선별된 유저 3명을 recent_users로, 해당 그룹에 속한 유저들의 수를 user_count로 보내주고 있다.

    이를 보고 limit라는 속성을 사용하여 보여줄 아바타 수를 자유롭게 조정하고자 했다.

     

    interface AvatarBaseProps {
      imageUrl?: string;
      altText: string;
      badgeIcon?: ReactNode;
      badgeIconBackgroundColor?: keyof typeof colors;
    }
    
    interface AvatarGroupProps {
    	previewAvatars?: AvatarBaseProps[]; //처음에는 avatars, 그러다보니 avatarTotalCount와 관련성이 있어보여(avatars.length 같아보임) 수정.
        avatarLimitCount?: number;
        avatarTotalCount?: number;
        size?: Size;
        radius?: Radius;
    }

     

     avatars를 AvatarProps[]로 하면 불필요한 정보도 포함되기에 별도의 인터페이스를 만드는 게 좋을 것 같다고 피드백을 받아 필요한 속성들만 담은 AvatarBaseProps를 만들어주었다.

     

     

     대략적으로 AvatarGroup의 인터페이스가 구현되었다. 

    결국 Avatar의 서브 컴포넌트로 들어가기에 AvatarGroup은 부적절했다. 그렇게 Avatar와 AvatarGroup은 별도로 export 하게 되었다.

     

     

    avatarLimitCount가 정말 필요한가?

     

     그런데 해당 인터페이스로 UI 구현을 시뮬레이션해보니 limitCount를 둠으로써 생각해야 될 요소들이 너무 많아졌다.

    • 초과 숫자 UI를 보여주기 위한 previewAvatars, totalCount, limitCount를 활용한 계산
    • limitCount에 대한 예외 처리
    • ...

     

     동료가 복잡성 + 서버 데이터 의존도가 심해 보인다는 피드백을 준 것도 limitCount의 존재 여부에 대한 생각을 하게 해 주었다.

    현재 프로젝트에서 사용되는 AvatarGroup은 3명의 유저들만 보여주는데 불필요한 확장을 고려했다는 것도 깨달았다. 필요한 경우 잘라서 쓰고, 그게 반복된다면 고려해야 할 사항이기에 limitCount를 삭제하였다. 

     

     그렇게 AvatarGroup은 아래와 같은 구조를 가져가게 되었다.

    <AvatarGroup previewAvatars={} avatarTotalCount={}>
        {previewAvatars.map ...
            <Avatar /> ....
        }
       {avatarTotalCount && <div>{avatarTotalCount - previewAvatars.length}</div> }
    </AvatarGroup>

     

     

     

     

     동료분들의 피드백을 받아 최종적으로 완성된 인터페이스이다. 

    interface AvatarProps
     {
      imageUrl?: string;
      altText: string;
      badgeIcon?: ReactNode;
      badgeIconBackgroundColor?: keyof typeof colors;
      radius?: Radius;
      size?: Size;
    }
    
    ...
    
    interface AvatarImageProps
     {
      imageUrl?: string;
      altText: string;
      radius?: Radius;
      size?: Size;
    }
    
    ...
    
    interface AvatarBadgeIconProps
    {
      badgeIcon?: ReactNode;
      badgeIconBackgroundColor?: keyof typeof colors;
      size?: Size;
    }
    
    ...
    
    interface AvatarBaseProps {
      imageUrl?: string;
      altText: string;
      badgeIcon?: ReactNode;
      badgeIconBackgroundColor?: keyof typeof colors;
    }
    
    ...
    
    interface AvatarGroupProps
     {
      previewAvatars?: AvatarBaseProps[];
      avatarTotalCount?: number;
      size?: Size;
      radius?: Radius;
    }
    
    ...
    
    export const Avatar = Object.assign(AvatarMain, {
      Image: AvatarImage,
      BadgeIcon: AvatarBadgeIcon,
    });
    
    export function AvatarGroup...

     

     

     

     속성들의 이해를 돕기 위해 스토리북에 작성했던 설명도 첨부한다.

    // ...
    component: Avatar,
    argTypes: {
        imageUrl: {
          control: 'text',
          description: '아바타 이미지의 URL입니다.',
          table: {
            category: 'Avatar.Image',
          },
        },
        altText: {
          description: '접근성을 위해 사용되는 아바타 이미지의 대체 텍스트입니다.',
          table: {
            category: 'Avatar.Image',
            defaultValue: {
              summary: '"유저 프로필 사진"',
            },
          },
        },
        radius: {
          table: {
            category: 'Avatar.Image',
            defaultValue: {
              summary: '"full"',
            },
          },
        },
        badgeIcon: {
          description: '아바타에 배지로 표시되는 아이콘입니다.',
          table: {
            category: 'Avatar.BadgeIcon',
          },
        },
        badgeIconBackgroundColor: {
          description: '배지 아이콘의 배경색을 지정합니다.',
          table: {
            category: 'Avatar.BadgeIcon',
          },
        },
        size: {
          options: ['tiny', 'small', 'medium', 'large'],
          table: {
            category: ['Avatar.Image', 'Avatar.BadgeIcon'],
            defaultValue: {
              summary: '"medium"',
            },
          },
        },
    },
    // ...
    component: AvatarGroup,
    argTypes: {
        previewAvatars: {
          control: 'array',
          description: '유저에게 보여지는 아바타 그룹의 전체 구성원 중 일부입니다.',
        },
        avatarTotalCount: {
          control: 'number',
          description: '그룹에 속해있는 모든 구성원들의 수 입니다.',
        },
        size: {
          control: 'radio',
          options: ['tiny', 'small', 'medium', 'large'],
          description: '아바타 그룹의 size를 설정합니다.',
          table: {
            defaultValue: { summary: '"medium"' },
          },
        },
        radius: {
          control: 'radio',
          options: ['none', 'small', 'medium', 'large', 'full'],
          description: '아바타 그룹의 radius를 설정합니다.',
          table: {
            defaultValue: { summary: '"full"' },
          },
        },
    }

     

    UI 구현기

    Avatar UI

      AvatarImage와 AvatarBadgeIcon은 size 타입을 공유하긴 하나 스타일은 공유하지 않기에 별도로 만들었다.

    // image
    variants: {
        size: {
          tiny: 'w-6 h-6',
          small: 'w-9 h-9',
          medium: 'w-14 h-14',
          large: 'w-20 h-20',
        },
        ...
    }
    
    // badgeIcon 
     variants: {
          size: {
            tiny: 'w-3 h-3',
            small: 'w-5 h-5',
            medium: 'w-7 h-7',
            large: 'w-10 h-10',
          },
          ...
    }

     

     

    AvatarGroup UI

     previewAvatars를 렌더링 하며 첫 번째 요소가 아니라면 margin-left를 조금씩 주었으며, z-index도 증가하도록 속성을 주었다.

     

    {previewAvatars.map((avatars, idx) => (
            <Avatar
              key={idx}
              size={size}
              imageUrl={avatars.imageUrl}
              altText={avatars.altText}
              radius={radius}
              badgeIcon={avatars.badgeIcon}
              badgeIconBackgroundColor={avatars.badgeIconBackgroundColor}
              className={`${idx !== 0 && '-ml-2'} z-${idx}`}
            />
    ))}

     

     

     초과 숫자 UI를 보여주기 위한 별도의 flag 변수를 만들었다.

    const showCount =
        avatarTotalCount && avatarTotalCount - previewAvatars.length > 0
          ? true
          : false;
          
          
     // 후에 리뷰받아 아래로 수정
     const showCount =
        !! avatarTotalCount && avatarTotalCount - previewAvatars.length > 0;

     

    그룹 전체의 size가 작아지거나 커지면 초과 숫자 UI도 크기를 따라가야 하는데 기존에 만든 Typography 컴포넌트의 사이즈를 사용해보고자 매핑하는 변수를 만들었다.

    const typographyFontSizes: Record<Size, TypographyProps['fontSize']> = {
        tiny: 'xs',
        small: 'base',
        medium: 'xl',
        large: '3xl',
      };

     

    초과 숫자 UI의 경우는 처음에 z-index를 `z-${avatarTotalCount}`로 주고 싶었는데 기본적으로 6개의 숫자 0,10,20,30,40,50와 auto만 제공하여 여기에 포함되지 않는다면 tailwind.config.js에 Customizing 해야 해 현시점에서는 z-20가 적절해 보여 별도로 주게 되었다(50은 너무 높아보여서...🫨).

    {showCount && (
            <div
              className={`${avatarVariants({
                size,
                radius,
              })} z-20 -ml-2 flex shrink-0 cursor-default items-center justify-center border-2 border-white bg-gray-600`}
            >
              <Typography
                fontSize={typographyFontSizes[size]}
                fontWeight='medium'
                color='white'
              >
                +{avatarTotalCount - previewAvatars.length}
              </Typography>
            </div>
          )}

     

     

    Avatar Component 구현결과
    Avatar Component 구현결과

     

    AvatarGroup Component 구현결과
    AvatarGroup Component 구현결과

     

     

    회고

    • UI 라이브러리를 참고할 때
      • 대중적 속성을 빠르게 파악하는 데 도움이 되나, 이에 매몰되어서는 안 된다.
      • 우리만의 컴포넌트는 우리 사이트, 우리 데이터를 보고 파악해야 한다. UI 라이브러리를 참고하는 만큼 우리 사이트도 참고하자.

     

    • 스토리북에 속성에 대한 설명을 작성하는 것도 중요하지만, 그것보다 더 중요한 것은 변수명을 잘 짓는 것이다.
      • src -> imageUrl : image의 url이라는 것을 명시적으로 알 수 있음.
      • avatars -> previewAvatars : 보여주기 위한 avatars라는 것을 알 수 있음.

     

    • UI 구현 시에도 개발자 도구를 활발하게 사용하자.

     

    • 동료들의 피드백을 받으며 시야가 넓어지고 이해도가 깊어지는 느낌이 들었다.
      • 내가 맡고 있는 컴포넌트의 이해도는 내가 제일 높아야한다는 것을 잊지 말자. 

     

     

    참고자료

    https://ant.design/components/avatar

    https://vercel.com/geist/avatar

    https://ui.shadcn.com/docs/components/avatar

    https://www.radix-ui.com/primitives/docs/components/avatar

    https://tailwindcss.com/

    https://fe-developers.kakaoent.com/2022/220731-composition-component/

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼