스켈레톤

스켈레톤 (Skeleton)

리소스가 로딩되는 동안 자리 표시자(placeholder) 역할을 해주는 컴포넌트입니다. 데이터가 오기 전에 레이아웃을 미리 잡아 화면 흔들림을 줄여줘요.

출처: 문서

본문

언제 사용하나요 (When To Use)

  • 리소스를 불러오는 데 오래 걸릴 때
  • List나 Card처럼 컴포넌트가 많은 정보를 담고 있을 때
  • 데이터를 처음 로딩할 때만 동작합니다
  • 어떤 상황에서는 Spin으로 대체할 수도 있지만, 더 나은 사용자 경험을 제공할 수 있어요

예제 (Examples)

기본 (Basic)

가장 간단한 Skeleton 사용법입니다.

import React from 'react';
import { Skeleton } from 'antd';

const App: React.FC = () => <Skeleton />;

export default App;

복합 조합 (Complex combination)

아바타와 여러 문단이 결합된 복합 형태입니다.

import React from 'react';
import { Skeleton } from 'antd';

const App: React.FC = () => <Skeleton avatar paragraph={{ rows: 4 }} />;

export default App;

활성 애니메이션 (Active Animation)

활성(active) 애니메이션을 표시합니다.

import React from 'react';
import { Skeleton } from 'antd';

const App: React.FC = () => <Skeleton active />;

export default App;

Button/Avatar/Input/Image/Node

Skeleton Button, Avatar, Input, Image, Node입니다.

import React, { useState } from 'react';
import { DotChartOutlined } from '@ant-design/icons';
import type { RadioChangeEvent } from 'antd';
import { Divider, Flex, Form, Radio, Skeleton, Space, Switch } from 'antd';

type SizeType = 'large' | 'medium' | 'small';
type ButtonShapeType = 'circle' | 'square' | 'round' | 'default';
type AvatarShapeType = 'circle' | 'square';

const App: React.FC = () => {
  const [active, setActive] = useState(false);
  const [block, setBlock] = useState(false);
  const [size, setSize] = useState<SizeType>('medium');
  const [buttonShape, setButtonShape] = useState<ButtonShapeType>('default');
  const [avatarShape, setAvatarShape] = useState<AvatarShapeType>('circle');

  const handleActiveChange = (checked: boolean) => {
    setActive(checked);
  };

  const handleBlockChange = (checked: boolean) => {
    setBlock(checked);
  };

  const handleSizeChange = (e: RadioChangeEvent) => {
    setSize(e.target.value);
  };

  const handleShapeButton = (e: RadioChangeEvent) => {
    setButtonShape(e.target.value);
  };

  const handleAvatarShape = (e: RadioChangeEvent) => {
    setAvatarShape(e.target.value);
  };

  return (
    <Flex gap="medium" vertical>
      <Space>
        <Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
        <Skeleton.Avatar active={active} size={size} shape={avatarShape} />
        <Skeleton.Input active={active} size={size} />
      </Space>
      <Skeleton.Button active={active} size={size} shape={buttonShape} block={block} />
      <Skeleton.Input active={active} size={size} block={block} />
      <Space>
        <Skeleton.Image active={active} />
        <Skeleton.Node active={active} style={{ width: 160 }} />
        <Skeleton.Node active={active}>
          <DotChartOutlined style={{ fontSize: 40, color: '#bfbfbf' }} />
        </Skeleton.Node>
      </Space>
      <Divider />
      <Form layout="inline" style={{ margin: '16px 0' }}>
        <Space size={16} wrap>
          <Form.Item label="Active">
            <Switch checked={active} onChange={handleActiveChange} />
          </Form.Item>
          <Form.Item label="Button and Input Block">
            <Switch checked={block} onChange={handleBlockChange} />
          </Form.Item>
          <Form.Item label="Size">
            <Radio.Group value={size} onChange={handleSizeChange}>
              <Radio.Button value="large">Large</Radio.Button>
              <Radio.Button value="medium">Medium</Radio.Button>
              <Radio.Button value="small">Small</Radio.Button>
            </Radio.Group>
          </Form.Item>
          <Form.Item label="Button Shape">
            <Radio.Group value={buttonShape} onChange={handleShapeButton}>
              <Radio.Button value="default">Default</Radio.Button>
              <Radio.Button value="square">Square</Radio.Button>
              <Radio.Button value="round">Round</Radio.Button>
              <Radio.Button value="circle">Circle</Radio.Button>
            </Radio.Group>
          </Form.Item>
          <Form.Item label="Avatar Shape">
            <Radio.Group value={avatarShape} onChange={handleAvatarShape}>
              <Radio.Button value="square">Square</Radio.Button>
              <Radio.Button value="circle">Circle</Radio.Button>
            </Radio.Group>
          </Form.Item>
        </Space>
      </Form>
    </Flex>
  );
};

export default App;

하위 컴포넌트 포함 (Contains sub component)

Skeleton이 하위 컴포넌트를 포함하고 있습니다.

import React, { useState } from 'react';
import { Button, Skeleton, Space } from 'antd';

const App: React.FC = () => {
  const [loading, setLoading] = useState<boolean>(false);

  const showSkeleton = () => {
    setLoading(true);
    setTimeout(() => {
      setLoading(false);
    }, 3000);
  };

  return (
    <Space vertical style={{ width: '100%' }} size={16}>
      <Skeleton loading={loading}>
        <h4 style={{ marginBottom: 16 }}>Ant Design, a design language</h4>
        <p>
          We supply a series of design principles, practical patterns and high quality design
          resources (Sketch and Axure), to help people create their product prototypes beautifully
          and efficiently.
        </p>
      </Skeleton>
      <Button onClick={showSkeleton} disabled={loading}>
        Show Skeleton
      </Button>
    </Space>
  );
};

export default App;

리스트 (List)

리스트 컴포넌트에서 Skeleton을 사용합니다.

import React, { useState } from 'react';
import type Icon from '@ant-design/icons';
import { LikeOutlined, MessageOutlined, StarOutlined } from '@ant-design/icons';
import { Avatar, List, Skeleton, Switch } from 'antd';

interface IconTextProps {
  icon: typeof Icon;
  text: React.ReactNode;
}

const listData = Array.from({ length: 3 }).map((_, i) => ({
  href: 'https://ant.design',
  title: `ant design part ${i + 1}`,
  avatar: `https://api.dicebear.com/10.x/lorelei/svg?seed=${i}`,
  description:
    'Ant Design, a design language for background applications, is refined by Ant UED Team.',
  content:
    'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
}));

const IconText: React.FC<IconTextProps> = ({ icon, text }) => (
  <>
    {React.createElement(icon, { style: { marginInlineEnd: 8 } })}
    {text}
  </>
);

const App: React.FC = () => {
  const [loading, setLoading] = useState(true);

  const onChange = (checked: boolean) => {
    setLoading(!checked);
  };

  return (
    <>
      <Switch checked={!loading} onChange={onChange} style={{ marginBottom: 16 }} />
      <List
        itemLayout="vertical"
        size="large"
        dataSource={listData}
        renderItem={(item) => (
          <List.Item
            key={item.title}
            actions={
              !loading
                ? [
                    <IconText icon={StarOutlined} text="156" key="list-vertical-star-o" />,
                    <IconText icon={LikeOutlined} text="156" key="list-vertical-like-o" />,
                    <IconText icon={MessageOutlined} text="2" key="list-vertical-message" />,
                  ]
                : undefined
            }
            extra={
              !loading && (
                <img
                  draggable={false}
                  width={272}
                  alt="logo"
                  src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
                />
              )
            }
          >
            <Skeleton loading={loading} active avatar>
              <List.Item.Meta
                avatar={<Avatar src={item.avatar} />}
                title={<a href={item.href}>{item.title}</a>}
                description={item.description}
              />
              {item.content}
            </Skeleton>
          </List.Item>
        )}
      />
    </>
  );
};

export default App;

커스텀 시맨틱 DOM 스타일링 (Custom semantic dom styling)

classNames와 styles에 객체 또는 함수를 넘겨서 Skeleton의 시맨틱 DOM 스타일을 커스터마이즈할 수 있습니다.

import React from 'react';
import { Flex, Skeleton } from 'antd';
import type { GetProp, SkeletonProps } from 'antd';
import { createStaticStyles } from 'antd-style';

const classnames = createStaticStyles(({ css }) => ({
  root: css`
    border-radius: 10px;
    padding: 12px;
  `,
  header: css`
    margin-bottom: 12px;
  `,
}));

const paragraphStyles = createStaticStyles(({ css }) => ({
  paragraph: css`
    & > li {
      background-color: rgba(229, 243, 254, 0.5);
    }
  `,
}));

const styles: SkeletonProps['styles'] = {
  avatar: {
    border: '1px solid #aaa',
  },
  title: {
    border: '1px solid #aaa',
  },
};

const stylesFn: SkeletonProps['styles'] = (info): GetProp<SkeletonProps, 'styles', 'Return'> => {
  if (info.props.active) {
    return {
      root: {
        border: '1px solid rgba(229, 243, 254, 0.3)',
      },
      title: {
        backgroundColor: 'rgba(229, 243, 254, 0.5)',
        height: 20,
        borderRadius: 20,
      },
    };
  }
  return {};
};

const App: React.FC = () => {
  return (
    <Flex gap="medium">
      <Skeleton classNames={classnames} styles={styles} avatar paragraph={false} />
      <Skeleton
        classNames={{ ...classnames, paragraph: paragraphStyles.paragraph }}
        styles={stylesFn}
        active
      />
    </Flex>
  );
};

export default App;

API

Common props ref:Common props

공통 API (Common API)

Skeleton

Property Description Type Default Version Global Config
active Show animation effect boolean false ×
avatar Show avatar placeholder boolean | SkeletonAvatar false ×
loading Display the skeleton when true boolean - ×
paragraph Show paragraph placeholder boolean | SkeletonParagraphProps true ×
round Show paragraph and title radius when true boolean false ×
title Show title placeholder boolean | SkeletonTitleProps true ×

SkeletonTitleProps

Property Description Type Default
width Set the width of title number | string -

SkeletonParagraphProps

Property Description Type Default
rows Set the row count of paragraph number -
width Set the width of paragraph. When width is an Array, it can set the width of each row. Otherwise only set the last row width number | string | Array<number | string> -

Skeleton.Avatar

Property Description Type Default
active Show animation effect, only valid when used avatar independently boolean false
shape Set the shape of avatar circle | square circle
size Set the size of avatar number | large | medium | small medium

Skeleton.Button

Property Description Type Default Version
active Show animation effect boolean false
block Option to fit button width to its parent width boolean false 4.17.0
shape Set the shape of button circle | round | square | default -
size Set the size of button large | medium | small medium

Skeleton.Input

Property Description Type Default
active Show animation effect boolean false
size Set the size of input large | medium | small medium

시맨틱 DOM (Semantic DOM)

Skeleton

https://ant.design/components/skeleton/semantic.md

Skeleton.Element

https://ant.design/components/skeleton/semantic_element.md

디자인 토큰 (Design Token)

컴포넌트 토큰 (Component Token - Skeleton)

Token Name Description Type Default Value
blockRadius Border radius of skeleton number 4
gradientFromColor Start color of gradient string rgba(0,0,0,0.06)
gradientToColor End color of gradient string rgba(0,0,0,0.15)
paragraphLiHeight Line height of paragraph skeleton number 16
paragraphMarginTop Margin top of paragraph skeleton number 28
titleHeight Height of title skeleton string | number 16

글로벌 토큰 (Global Token)

Token Name Description Type Default Value
borderRadiusSM SM size border radius, used in small size components, such as Button, Input, Select and other input components in small size number
controlHeight The height of the basic controls such as buttons and input boxes in Ant Design number
controlHeightLG LG component height number
controlHeightSM SM component height number
controlHeightXS XS component height number
marginSM Control the margin of an element, with a medium-small size. number
padding Control the padding of the element. number

더 알아보기 (Learn more)