작성자는 쉐퍼드23의 Product Manager & Software Engineer로 재직하며 카페24 플랫폼을 대상으로 하는 Contextual Bandit 기반의 개인화 상품 추천 플러그인 PickHound의 개발 부문을 담당한 바가 있습니다. (See: 경력 기술서 - PickHound)
작성자는 플러그인 개발의 일환으로, 쇼핑몰 관리자가 상품 추천 관련한 설정을 하고 구독 플랜을 관리할 수 있는 어드민 대시보드를 개발하였습니다.
이번 글에서는 이 중 "Admin Dashboard Kit" 디 자인 시스템을 기반으로 어드민 대시보드 개발을 위한 디자인 시스템을 구현한 내용을 소개합니다.
Admin Dashboard Kit
Admin Dashboard Kit은 SaaS Design에서 제작한 어드민 대시보드를 위한 디자인 시스템으로, 커뮤니티 버전이 무료로 Figma File로 공개되어 있습니다. (See: Figma Admin Dashboard Kit)
작성자는 이 디자인 시스템을 참고하여, 본 플러그인의 어드민 대시보드를 개발했습니다.
Implementation
아래에서는 어드민 대시보드를 개발하면서 작성한 주요 컴포넌트 및 오브젝트의 작성 동기와 구현 내용을 기술합니다.
<Avater />
<Avatar />
컴포넌트는 대시보드의 상단에 위치한 사용자 정보를 표시하는 컴포넌트입니다. 이 컴포넌트는 여러 컴포넌트에서 재사용되지는 않지만, Figma Admin Dashboard Kit에서 Header Bar 영역에 사용하기도 하고, 디자인의 구색을 맞추는 데에 이러한 Header Bar 영역이 필요했기에 구현하였습니다.
Avatar
import React from "react";
import clsx from "clsx";
interface Props {
/**
* Avatar source.
* Should be Image URL.
*/
src?: string;
/**
* The size of the avatar.
* `size` decides border radius.
* Border radius is half of the `size`.
*/
size?: string;
/**
* Whether to apply ring or not. See [ring](https://tailwindcss.com/docs/ring-width#adding-a-ring)
*/
ring?: boolean;
}
const borderRadius = (size: string) => {
const scalar = Number.parseFloat(size);
const unit = size.replace(scalar.toString(), "");
return `${scalar / 2}${unit}`;
};
const defaultImageSrc = "/res/images/user.png";
/**
* Common avatar UI implementation.
*/
const Avatar: React.FC<Props> = ({
src = defaultImageSrc,
size = "44px",
ring,
}) => {
return (
<div
className={clsx([
"center overflow-hidden",
ring && "ring ring-offset-2 ring-light-gray",
])}
style={{ width: size, height: size, borderRadius: borderRadius(size) }}
>
<img
src={src}
onError={(e) => {
e.currentTarget.src = defaultImageSrc;
}}
alt="Avatar"
className="bg-cover bg-center"
style={{ width: size }}
/>
</div>
);
};
export default Avatar;
<Callout />
<Callout />
컴포넌트는 Figma Admin Dashboard Kit에서 직접적으로 사용되지는 않지만, 개발 중 사용자에게 간단히 메시지를 전달하는데 이용할 수 있는 메시지 박스가 필요해 구현하였습니다.
컬러링에 디자인 시스템의 색상 팔레트를 사용해 다른 컴포넌트와의 일관성을 유지하여 이질감 없는 UI를 구현하고자 하였습니다.
직관적인 컴포넌트 구현으로 아래와 같이 간단하게 활용이 가능했고, 어드민 대시보드 곳곳에서 유저에게 메시지를 전달하는 데에 유용하게 사용되었습니다.
<Callout appearance="info" truncate={50}>
nulla autem mollitia voluptatibus quas expedita dolorem in possimus totam quam
amet reprehenderit natus quos minima temporibus ex reprehenderit deleniti
ullam dolorum voluptatem voluptas assumenda perspiciatis culpa consequuntur
facere a
</Callout>
Callout
import React from "react";
import clsx from "clsx";
import Icon from "../icon";
import {
CalloutAppearance,
getBackgroundColor,
getIconName,
getTextColor,
} from "./utils";
interface Props {
/**
* @description
* 배너의 색깔 및 leading 아이콘의 종류를 결정합니다. 각 옵션의 색깔 및 아이콘은 아래와 같습니다.
*
* - info: 파란색 배경; [info icon](https://fonts.google.com/icons?icon.query=info&icon.style=Rounded)
* - success: 초록색 배경; [check_circle icon](https://fonts.google.com/icons?icon.query=check_circle&icon.style=Rounded)
* - warning: 주황색 배경; [warning icon](https://fonts.google.com/icons?icon.query=warning&icon.style=Rounded)
* - error: 빨간색 배경; [error icon](https://fonts.google.com/icons?icon.query=error&icon.style=Rounded)
*/
appearance: CalloutAppearance;
children?: React.ReactNode;
/**
* @description
* children의 type이 @type {string}일 경우 적용됩니다.
* children의 길이가 truncate를 초과할 경우 초과하는 문자열을 자르고 "..."을 붙입니다
*/
truncate?: number;
/**
* @description
* 배경색을 설정합니다.
* 본 프로퍼티는 appearance 프로퍼티로 주어지는 배경색을 덮어씁니다.
*/
backgroundColor?: string;
/**
* @description
* 텍스트 색을 설정합니다.
* 본 프로퍼티는 appearance 프로퍼티로 주어지는 텍스트 색을 덮어씁니다.
*/
textColor?: string;
className?: string;
}
/**
* @description
* 어떤 메시지를 강조하고 싶을때 사용하는 컴포넌트입니다.
*
* Banner라고도 불리우며, [Atlaskit의 배너](https://atlassian.design/components/banner/examples)
* 를 참고하면 쉽게 이해할 수 있을 것입니다.
*/
const Callout: React.FC<Props> = ({
children,
appearance,
truncate,
backgroundColor = getBackgroundColor(appearance),
textColor = getTextColor(appearance),
className,
}) => {
return (
<div
className={clsx([
"flex justify-start items-center p-5 w-full",
className,
])}
style={{ backgroundColor }}
>
<Icon
name={getIconName(appearance)}
fontVariationSettings={{ FILL: 1 }}
color={textColor}
/>
<div className="h-full w-5" />
{typeof children === "string" && truncate ? (
<span style={{ color: textColor }}>
{children.slice(0, truncate).concat("...")}
</span>
) : (
children
)}
</div>
);
};
export default Callout;
<Icon />
<Icon />
컴포넌트는 Figma Admin Dashboard Kit에서 활용하는 아이콘을 Google Icons로 구현할 적에 사용되었습니다. React
의 Function Component 패턴 을 이용해 아래와 같이 Google Icons를 편리하게 활용할 수 있도록 구현하였습니다.
<Icon name="home" />
Icon
import React from "react";
import clsx from "clsx";
import type { Maybe } from "@src/utils/types";
import { FontVariationSettings, formatFontVariationSettings } from "./utils";
interface Props {
/**
* 구글 아이콘의 이름.
*
* 구글 아이콘의 이름은 [구글 아이콘](https://fonts.google.com/icons?icon.style=Rounded)에서 아이콘을 선택하면
* 오른쪽에 표시되는 사이드바에서 "Inserting the icon" 부분의 span 태그의 innerText이다.
*/
name: string;
/**
* Unit이 포함된 아이콘의 크기이다. (e.g. px, em, rem, ...)
*/
size?: string;
color?: Maybe<string>;
className?: string;
/**
* [구글 아이콘](https://fonts.google.com/icons?icon.style=Rounded)에 들어가면
* 우측에 Customization이라는 이름을 가진 floating banner가 있는데,
* 여기에 있는 4개의 프로퍼티에 대응되는 값을 넣어주면 된다.
*/
fontVariationSettings?: FontVariationSettings;
}
/**
* @description
* [구글 아이콘](https://fonts.google.com/icons?icon.style=Rounded)의 Wrapper Component.
*/
const Icon: React.FC<Props> = ({
name,
size: fontSize,
color,
className,
fontVariationSettings,
}) => {
return (
<span
className={clsx(["material-symbols-rounded", className])}
style={{
fontSize,
color: color ?? undefined,
fontVariationSettings: formatFontVariationSettings(
fontVariationSettings,
),
}}
>
{name}
</span>
);
};
export default Icon;
<Loader />
<Loader />
컴포넌트는 Figma Admin Dashboard Kit에서 사용하지는 않지만, 어드민 대시보드 곳곳에서 로딩 중임을 표시하는 데에 사용되었습 니다.
API와 인증 Flow를 수행하기 위해 기다리거나 <React.Suspense />
컴포넌트의 fallback
등으로 여러 차례 사용되었습니다.
Loader
import colors from "@src/theme/colors";
import React from "react";
import { RotatingSquare } from "react-loader-spinner";
import clsx from "clsx";
import { useLoadingTextAnimation } from "@src/utils/hooks";
import Center from "../wrapper/center";
interface Props {
/**
* If true, the loader will be shown in full screen.
*
* It applies tailwindcss utility classes of `w-screen h-screen bg-cool-white` to the wrapper.
*/
fullScreen?: boolean;
/**
* If true, the loader will be shown in full width and height.
*
* It applies tailwindcss utility classes of `w-full h-full` to the wrapper.
*/
fill?: boolean;
/**
* Extra class names to apply to the wrapper.
*/
className?: string;
}
/**
* @description
* Loader is a component that shows a loading animation.
* This animation includes a rotating square
* and a text that says "Loading" with trailing dots keep changing their number.
*/
const Loader: React.FC<Props> = ({ fullScreen, fill, className }) => {
const animatedText = useLoadingTextAnimation();
return (
<Center
direction="vertical"
className={clsx([
fullScreen && "w-screen h-screen bg-cool-white",
fill && "w-full h-full",
className,
])}
fill
>
<RotatingSquare
ariaLabel="rotating-square"
visible
color={colors.deepblue}
strokeWidth="10"
/>
<div className="w-1 h-6" />
<h1 className="text-xl font-noto-sans-kr">{animatedText}</h1>
</Center>
);
};
export default Loader;
<NumberCard />
<NumberCard />
컴포넌트는 <RoundedCard />
를 활용해 구현한 컴포넌트로, Figma Admin Dashboard Kit에서 사용되는 숫자를 표시하는 카드를 구현한 것입니다.
작성자가 개발한 어드민 대시보드에서는 메인 페이지에서 사용자에게 통계 정보를 제공하는 데에 사용되었습니다.
컴포넌트의 사용처가 매우 특정한 케이스로 제한된 경우로, 이러한 특성으로 아래와 같이 몇 개의 프로퍼티만 정의해도 바로 컴포넌트 사용이 가능합니다.
<NumberCard title="Unresolved" value={60} />
NumberCard
import React from "react";
import clsx from "clsx";
import RoundedCard from "../rounded-card";
import Center from "../wrapper/center";
import Spinner from "../spinner";
interface Props {
/**
* 카드의 제목
*/
title: string;
/**
* 제목에 대한 수치
*/
value: number | string;
/**
* 수치를 로딩 중인지 표시할 때 사용
*/
loading?: boolean;
/**
* 컴포넌트 루트 html tag에 적용할 id
*/
id?: string;
/**
* 컴포넌트 루트 html tag에 적용할 className
*/
className?: string;
/**
* `title`의 크기를 조절할 때 사용.
* 'sm', 'lg' 중 하나를 선택할 수 있다.
*/
size?: "sm" | "lg";
/**
* `value`의 크기를 조절할 때 사용.
* 'sm', 'lg' 중 하나를 선택할 수 있다.
*/
valueSize?: "sm" | "lg";
/**
* 컴포넌트 루트 html tag에 적용할 ref.
* `React.Ref<HTMLDivElement>` 타입이다.
*/
ref?: React.Ref<HTMLDivElement>;
}
/**
* @description
* 대시보드 첫 화면에서 실적 관련 통계를 표시할 때 사용하고 있는 컴포넌트.
*
* `title`인 이름을 위에 크게 표시하고 `value`인 수치를 `title`보다는 작게 아래에 표시하는 {@link RoundedCard}이다.
*
* [Dashboard Kit 의 디자인](https://www.figma.com/file/iXf4Zaj8CiD06LlknHpIRs/Figma-Admin-Dashboard-UI-Kit-(Community)?node-id=0%3A1&t=cPcff0XRaQTeWALD-1)을 참고하여 제작하였다.
*/
const NumberCard: React.FC<Props> = ({
title,
value,
loading,
id,
className,
ref,
size = "lg",
valueSize = "lg",
}) => {
return (
<RoundedCard
ref={ref}
id={id}
className={clsx([
size === "lg" ? "w-[258px] h-[134px]" : "w-24 h-18",
className,
])}
>
<Center direction="vertical" fill>
<span
className={clsx([
"text-ash-gray font-bold transition group-hover:text-deep-blue",
size === "lg" ? "text-lg" : "text-xs",
size === "lg" && "mb-2",
])}
>
{title}
</span>
<span
className={clsx([
"text-deep-black font-bold transition group-hover:text-deep-blue",
size === "lg" && valueSize === "lg" ? "text-4xl" : "text-xl",
size === "lg" && "mt-2",
])}
>
{loading && <Spinner />}
{!loading && value.toString()}
</span>
</Center>
</RoundedCard>
);
};
export default NumberCard;
<Required />
<Required />
컴포넌트는 이름 그래도 어떤 필드가 Required 일 때를 나타내고자 할때 사용합니다. Figma Admin Dashboard Kit에서도 위와 같은 디자인을 사용하고 있습니다.
<Required />
컴포넌트는 아래와 같이 텍스트를 감싸는 형태로 간단하게 사용할 수 있습니다.
<Required>est distinctio</Required>
Required
import React from "react";
import clsx from "clsx";
interface Props {
className?: string;
children?: React.ReactNode;
}
/**
* @description
* Red asterisk aligned topleft for required fields
*/
const Required: React.FC<Props> = ({ children, className }) => {
return (
<div
className={clsx([
"text-deep-black flex justify-start items-start",
className,
])}
>
<strong className="text-md align-top text-hotsauce">*</strong>
{children}
</div>
);
};
export default Required;