목록

목록 (List)

하나의 주제에 관련된 콘텐츠를 표시하는 데 쓰는 목록 컴포넌트예요. 다양한 타입과 크기의 여러 요소로 구성할 수 있어요.

참고: List 컴포넌트는 더 이상 사용되지 않아요(deprecated). 다음 메이저 버전에서 제거될 예정이니 Listy를 사용해 주세요.

출처: 문서

본문

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

  • 하나의 주제에 관련된 콘텐츠를 표시할 때 사용해요. 콘텐츠는 타입과 크기가 다양한 여러 요소로 구성될 수 있어요.

:::warning{title=Deprecated Notice} List 컴포넌트는 더 이상 사용되지 않아요. 다음 메이저 버전에서 제거될 예정이에요. 대신 Listy를 사용해 주세요. List에서 어떻게 마이그레이션하나요?를 참고해요. :::

예시 (Examples)

단순 목록 (Simple list)

Ant Design은 기본 목록 크기뿐 아니라 large와 small 크기도 지원해요.

large나 small 목록을 원하면 size 속성을 각각 large나 small로 설정해요. 기본 크기의 목록을 원하면 size 속성을 생략해요.

header와 footer 속성을 설정해 목록의 헤더와 푸터를 커스터마이즈해요.

import React from 'react';
import { Divider, List, Typography } from 'antd';

const data = [
  'Racing car sprays burning fuel into crowd.',
  'Japanese princess to wed commoner.',
  'Australian walks 100km after outback crash.',
  'Man charged over missing wedding girl.',
  'Los Angeles battles huge wildfires.',
];

const App: React.FC = () => (
  <>
    <Divider titlePlacement="start">Default Size</Divider>
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={data}
      renderItem={(item) => (
        <List.Item>
          <Typography.Text mark>[ITEM]</Typography.Text> {item}
        </List.Item>
      )}
    />
    <Divider titlePlacement="start">Small Size</Divider>
    <List
      size="small"
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={data}
      renderItem={(item) => <List.Item>{item}</List.Item>}
    />
    <Divider titlePlacement="start">Large Size</Divider>
    <List
      size="large"
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={data}
      renderItem={(item) => <List.Item>{item}</List.Item>}
    />
  </>
);

export default App;

기본 목록 (Basic list)

기본 목록이에요.

import React from 'react';
import { Avatar, List } from 'antd';

const data = [
  {
    title: 'Ant Design Title 1',
  },
  {
    title: 'Ant Design Title 2',
  },
  {
    title: 'Ant Design Title 3',
  },
  {
    title: 'Ant Design Title 4',
  },
];

const App: React.FC = () => (
  <List
    itemLayout="horizontal"
    dataSource={data}
    renderItem={(item, index) => (
      <List.Item>
        <List.Item.Meta
          avatar={<Avatar src={`https://api.dicebear.com/10.x/lorelei/svg?seed=${index}`} />}
          title={<a href="https://ant.design">{item.title}</a>}
          description="Ant Design, a design language for background applications, is refined by Ant UED Team"
        />
      </List.Item>
    )}
  />
);

export default App;

더 불러오기 (Load more)

loadMore 속성으로 더 불러오기 목록을 만들어요.

import React, { useEffect, useState } from 'react';
import { Avatar, Button, List, Skeleton } from 'antd';
import { createStyles } from 'antd-style';

const useStyles = createStyles((props) => {
  const { css } = props;
  return {
    loadmoreList: css`
      min-height: 350px;
    `,
  };
});

interface DataType {
  gender?: string;
  name?: string;
  email?: string;
  avatar?: string;
  loading: boolean;
}

const PAGE_SIZE = 3;

const App: React.FC = () => {
  const { styles } = useStyles();

  const [initLoading, setInitLoading] = useState(true);
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<DataType[]>([]);
  const [list, setList] = useState<DataType[]>([]);
  const [page, setPage] = useState(1);

  const fetchData = (currentPage: number) => {
    const fakeDataUrl = `https://660d2bd96ddfa2943b33731c.mockapi.io/api/users?page=${currentPage}&limit=${PAGE_SIZE}`;
    return fetch(fakeDataUrl)
      .then((res) => res.json())
      .catch(() => {
        console.log('fetch mock data failed');
        return [];
      });
  };

  useEffect(() => {
    fetchData(page).then((res) => {
      const results = Array.isArray(res) ? res : [];
      setInitLoading(false);
      setData(results);
      setList(results);
    });
  }, []);

  const onLoadMore: React.MouseEventHandler<HTMLElement> = () => {
    setLoading(true);
    setList(data.concat(Array.from({ length: PAGE_SIZE }).map(() => ({ loading: true }))));
    const nextPage = page + 1;
    setPage(nextPage);
    fetchData(nextPage).then((res) => {
      const results = Array.isArray(res) ? res : [];
      const newData = data.concat(results);
      setData(newData);
      setList(newData);
      setLoading(false);
      // Resetting window's offsetTop so as to display react-virtualized demo underfloor.
      // In real scene, you can using public method of react-virtualized:
      // https://stackoverflow.com/questions/46700726/how-to-use-public-method-updateposition-of-react-virtualized
      window.dispatchEvent(new Event('resize'));
    });
  };

  const loadMore =
    !initLoading && !loading ? (
      <div style={{ textAlign: 'center', marginTop: 12, height: 32, lineHeight: '32px' }}>
        <Button onClick={onLoadMore}>loading more</Button>
      </div>
    ) : null;

  return (
    <List
      className={styles.loadmoreList}
      loading={initLoading}
      itemLayout="horizontal"
      loadMore={loadMore}
      dataSource={list}
      renderItem={(item) => (
        <List.Item actions={[<a key="loadmore-edit">edit</a>, <a key="loadmore-more">more</a>]}>
          <Skeleton avatar title={false} loading={item.loading} active>
            <List.Item.Meta
              avatar={<Avatar src={item.avatar} />}
              title={<a href="https://ant.design">{item.name}</a>}
              description="Ant Design, a design language for background applications, is refined by Ant UED Team"
            />
            <div>content</div>
          </Skeleton>
        </List.Item>
      )}
    />
  );
};

export default App;

세로 (Vertical)

itemLayout 속성을 vertical로 설정해 세로 목록을 만들어요.

import React from 'react';
import { LikeOutlined, MessageOutlined, StarOutlined } from '@ant-design/icons';
import { Avatar, List, Space } from 'antd';

const data = Array.from({ length: 23 }).map((_, i) => ({
  href: 'https://ant.design',
  title: `ant design part ${i}`,
  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 = ({ icon, text }: { icon: React.FC; text: string }) => (
  <Space>
    {React.createElement(icon)}
    {text}
  </Space>
);

const App: React.FC = () => (
  <List
    itemLayout="vertical"
    size="large"
    pagination={{
      onChange: (page) => {
        console.log(page);
      },
      pageSize: 3,
    }}
    dataSource={data}
    footer={
      <div>
        <b>ant design</b> footer part
      </div>
    }
    renderItem={(item) => (
      <List.Item
        key={item.title}
        actions={[
          <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" />,
        ]}
        extra={
          <img
            draggable={false}
            width={272}
            alt="logo"
            src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
          />
        }
      >
        <List.Item.Meta
          avatar={<Avatar src={item.avatar} />}
          title={<a href={item.href}>{item.title}</a>}
          description={item.description}
        />
        {item.content}
      </List.Item>
    )}
  />
);

export default App;

페이지네이션 설정 (Pagination Settings)

List의 페이지네이션은 pagination 속성으로 사용하고 설정할 수 있어요.

import React, { useState } from 'react';
import { Avatar, List, Radio, Space } from 'antd';

type PaginationPosition = 'top' | 'bottom' | 'both';

type PaginationAlign = 'start' | 'center' | 'end';

const data = [
  {
    title: 'Ant Design Title 1',
  },
  {
    title: 'Ant Design Title 2',
  },
  {
    title: 'Ant Design Title 3',
  },
  {
    title: 'Ant Design Title 4',
  },
];

const positionOptions = ['top', 'bottom', 'both'];

const alignOptions = ['start', 'center', 'end'];

const App: React.FC = () => {
  const [position, setPosition] = useState<PaginationPosition>('bottom');
  const [align, setAlign] = useState<PaginationAlign>('center');

  return (
    <>
      <Space vertical style={{ marginBottom: '20px' }} size="medium">
        <Space>
          <span>Pagination Position:</span>
          <Radio.Group
            optionType="button"
            value={position}
            onChange={(e) => {
              setPosition(e.target.value);
            }}
          >
            {positionOptions.map((item) => (
              <Radio.Button key={item} value={item}>
                {item}
              </Radio.Button>
            ))}
          </Radio.Group>
        </Space>
        <Space>
          <span>Pagination Align:</span>
          <Radio.Group
            optionType="button"
            value={align}
            onChange={(e) => {
              setAlign(e.target.value);
            }}
          >
            {alignOptions.map((item) => (
              <Radio.Button key={item} value={item}>
                {item}
              </Radio.Button>
            ))}
          </Radio.Group>
        </Space>
      </Space>
      <List
        pagination={{ position, align }}
        dataSource={data}
        renderItem={(item, index) => (
          <List.Item>
            <List.Item.Meta
              avatar={<Avatar src={`https://api.dicebear.com/10.x/lorelei/svg?seed=${index}`} />}
              title={<a href="https://ant.design">{item.title}</a>}
              description="Ant Design, a design language for background applications, is refined by Ant UED Team"
            />
          </List.Item>
        )}
      />
    </>
  );
};

export default App;

그리드 (Grid)

List의 grid 속성을 설정해 그리드 레이아웃을 만들어요.

import React from 'react';
import { Card, List } from 'antd';

const data = [
  {
    title: 'Title 1',
  },
  {
    title: 'Title 2',
  },
  {
    title: 'Title 3',
  },
  {
    title: 'Title 4',
  },
];

const App: React.FC = () => (
  <List
    grid={{ gutter: 16, column: 4 }}
    dataSource={data}
    renderItem={(item) => (
      <List.Item>
        <Card title={item.title}>Card content</Card>
      </List.Item>
    )}
  />
);

export default App;

반응형 그리드 목록 (Responsive grid list)

반응형 그리드 목록이에요. size 속성은 Layout Grid와 같아요.

import React from 'react';
import { Card, List } from 'antd';

const data = [
  {
    title: 'Title 1',
  },
  {
    title: 'Title 2',
  },
  {
    title: 'Title 3',
  },
  {
    title: 'Title 4',
  },
  {
    title: 'Title 5',
  },
  {
    title: 'Title 6',
  },
];

const App: React.FC = () => (
  <List
    grid={{
      gutter: 16,
      xs: 1,
      sm: 2,
      md: 4,
      lg: 4,
      xl: 6,
      xxl: 3,
    }}
    dataSource={data}
    renderItem={(item) => (
      <List.Item>
        <Card title={item.title}>Card content</Card>
      </List.Item>
    )}
  />
);

export default App;

스크롤 로딩 (Scrolling loaded)

react-infinite-scroll-component로 무한 로드하는 예시예요.

import React, { useEffect, useState } from 'react';
import { Avatar, Divider, List, Skeleton } from 'antd';
import InfiniteScroll from 'react-infinite-scroll-component';

interface DataType {
  gender?: string;
  name?: string;
  email?: string;
  avatar?: string;
  id?: string;
}

const App: React.FC = () => {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<DataType[]>([]);
  const [page, setPage] = useState(1);

  const loadMoreData = () => {
    if (loading) {
      return;
    }
    setLoading(true);
    fetch(`https://660d2bd96ddfa2943b33731c.mockapi.io/api/users/?page=${page}&limit=10`)
      .then((res) => res.json())
      .then((res) => {
        const results = Array.isArray(res) ? res : [];
        setData([...data, ...results]);
        setLoading(false);
        setPage(page + 1);
      })
      .catch(() => {
        setLoading(false);
      });
  };

  useEffect(() => {
    loadMoreData();
  }, []);

  return (
    <div
      id="scrollableDiv"
      style={{
        height: 400,
        overflow: 'auto',
        padding: '0 16px',
        border: '1px solid rgba(140, 140, 140, 0.35)',
      }}
    >
      <InfiniteScroll
        dataLength={data.length}
        next={loadMoreData}
        hasMore={data.length < 50}
        loader={<Skeleton avatar paragraph={{ rows: 1 }} active />}
        endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
        scrollableTarget="scrollableDiv"
      >
        <List
          dataSource={data}
          renderItem={(item) => (
            <List.Item key={item.email}>
              <List.Item.Meta
                avatar={<Avatar src={item.avatar} />}
                title={<a href="https://ant.design">{item.name}</a>}
                description={item.email}
              />
              <div>Content</div>
            </List.Item>
          )}
        />
      </InfiniteScroll>
    </div>
  );
};

export default App;

드래그 정렬 (Drag sorting)

components를 사용해 List를 dnd-kit과 연동해 드래그 정렬 기능을 구현해요.

import React, { useState } from 'react';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
  arrayMove,
  SortableContext,
  useSortable,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { List } from 'antd';
import type { GetProps } from 'antd';

const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => {
  const { itemKey, style, children, ...rest } = props;

  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
    id: itemKey,
  });

  const listStyle: React.CSSProperties = {
    ...style,
    transform: CSS.Translate.toString(transform),
    transition,
    cursor: 'move',
    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
  };

  return (
    <List.Item {...rest} ref={setNodeRef} style={listStyle}>
      <div {...attributes} {...listeners}>
        {children}
      </div>
    </List.Item>
  );
};

const App: React.FC = () => {
  const [data, setData] = useState([
    { key: 1, content: 'Racing car sprays burning fuel into crowd.' },
    { key: 2, content: 'Japanese princess to wed commoner.' },
    { key: 3, content: 'Australian walks 100km after outback crash.' },
    { key: 4, content: 'Man charged over missing wedding girl.' },
    { key: 5, content: 'Los Angeles battles huge wildfires.' },
  ]);

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        // https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
        distance: 1,
      },
    }),
  );

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (!active || !over) {
      return;
    }
    if (active.id !== over.id) {
      setData((prev) => {
        const activeIndex = prev.findIndex((i) => i.key === active.id);
        const overIndex = prev.findIndex((i) => i.key === over.id);
        return arrayMove(prev, activeIndex, overIndex);
      });
    }
  };

  return (
    <DndContext
      sensors={sensors}
      modifiers={[restrictToVerticalAxis]}
      onDragEnd={onDragEnd}
      id="list-drag-sorting"
    >
      <SortableContext items={data.map((item) => item.key)} strategy={verticalListSortingStrategy}>
        <List
          dataSource={data}
          renderItem={(item) => (
            <SortableListItem key={item.key} itemKey={item.key}>
              {item.key} {item.content}
            </SortableListItem>
          )}
        />
      </SortableContext>
    </DndContext>
  );
};

export default App;

핸들이 있는 드래그 정렬 (Drag sorting with handler)

대안으로 dnd-kit의 핸들로 드래그 정렬을 구현할 수도 있어요.

import React, { createContext, useContext, useMemo, useState } from 'react';
import { HolderOutlined } from '@ant-design/icons';
import type { DragEndEvent, DraggableAttributes } from '@dnd-kit/core';
import { DndContext } from '@dnd-kit/core';
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
  arrayMove,
  SortableContext,
  useSortable,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, List } from 'antd';
import type { GetProps } from 'antd';

interface SortableListItemContextProps {
  setActivatorNodeRef?: (element: HTMLElement | null) => void;
  listeners?: SyntheticListenerMap;
  attributes?: DraggableAttributes;
}

const SortableListItemContext = createContext<SortableListItemContextProps>({});

const DragHandle: React.FC = () => {
  const { setActivatorNodeRef, listeners, attributes } = useContext(SortableListItemContext);
  return (
    <Button
      type="text"
      size="small"
      icon={<HolderOutlined />}
      style={{ cursor: 'move' }}
      ref={setActivatorNodeRef}
      {...attributes}
      {...listeners}
    />
  );
};

const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => {
  const { itemKey, style, ...rest } = props;

  const {
    attributes,
    listeners,
    setNodeRef,
    setActivatorNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: itemKey });

  const listStyle: React.CSSProperties = {
    ...style,
    transform: CSS.Translate.toString(transform),
    transition,
    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
  };

  const memoizedValue = useMemo<SortableListItemContextProps>(
    () => ({ setActivatorNodeRef, listeners, attributes }),
    [setActivatorNodeRef, listeners, attributes],
  );

  return (
    <SortableListItemContext.Provider value={memoizedValue}>
      <List.Item {...rest} ref={setNodeRef} style={listStyle} />
    </SortableListItemContext.Provider>
  );
};

const App: React.FC = () => {
  const [data, setData] = useState([
    { key: 1, content: 'Racing car sprays burning fuel into crowd.' },
    { key: 2, content: 'Japanese princess to wed commoner.' },
    { key: 3, content: 'Australian walks 100km after outback crash.' },
    { key: 4, content: 'Man charged over missing wedding girl.' },
    { key: 5, content: 'Los Angeles battles huge wildfires.' },
  ]);

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (!active || !over) {
      return;
    }
    if (active.id !== over.id) {
      setData((prevState) => {
        const activeIndex = prevState.findIndex((i) => i.key === active.id);
        const overIndex = prevState.findIndex((i) => i.key === over.id);
        return arrayMove(prevState, activeIndex, overIndex);
      });
    }
  };

  return (
    <DndContext
      modifiers={[restrictToVerticalAxis]}
      onDragEnd={onDragEnd}
      id="list-drag-sorting-handler"
    >
      <SortableContext items={data.map((item) => item.key)} strategy={verticalListSortingStrategy}>
        <List
          dataSource={data}
          renderItem={(item) => (
            <SortableListItem key={item.key} itemKey={item.key}>
              <DragHandle /> {item.key} {item.content}
            </SortableListItem>
          )}
        />
      </SortableContext>
    </DndContext>
  );
};

export default App;

그리드 드래그 정렬 (Grid Drag sorting)

커스텀 컴포넌트로 List를 dnd-kit과 연동해 그리드 레이아웃의 드래그 정렬 기능을 구현해요.

import React, { useState } from 'react';
import type { DragEndEvent } from '@dnd-kit/core';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Card, List } from 'antd';
import type { GetProps } from 'antd';

const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => {
  const { itemKey, style, ...rest } = props;

  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
    id: itemKey,
  });

  const listStyle: React.CSSProperties = {
    ...style,
    transform: CSS.Translate.toString(transform),
    transition,
    cursor: 'move',
    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
  };

  return <List.Item {...rest} ref={setNodeRef} style={listStyle} {...attributes} {...listeners} />;
};

const App: React.FC = () => {
  const [data, setData] = useState([
    { key: 1, title: 'Title 1' },
    { key: 2, title: 'Title 2' },
    { key: 3, title: 'Title 3' },
    { key: 4, title: 'Title 4' },
    { key: 5, title: 'Title 5' },
    { key: 6, title: 'Title 6' },
  ]);

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        // https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
        distance: 1,
      },
    }),
  );

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (!active || !over) {
      return;
    }
    if (active.id !== over.id) {
      setData((prev) => {
        const activeIndex = prev.findIndex((i) => i.key === active.id);
        const overIndex = prev.findIndex((i) => i.key === over.id);
        return arrayMove(prev, activeIndex, overIndex);
      });
    }
  };

  return (
    <DndContext sensors={sensors} onDragEnd={onDragEnd} id="list-grid-drag-sorting">
      <SortableContext items={data.map((item) => item.key)}>
        <List
          grid={{ gutter: 16, column: 4 }}
          dataSource={data}
          renderItem={(item) => (
            <SortableListItem key={item.key} itemKey={item.key}>
              <Card title={item.title}>Card content</Card>
            </SortableListItem>
          )}
        />
      </SortableContext>
    </DndContext>
  );
};

export default App;

핸들이 있는 그리드 드래그 정렬 (Grid Drag sorting with handler)

커스텀 컴포넌트와 드래그 핸들로 List를 dnd-kit과 연동해 그리드 레이아웃의 드래그 정렬 기능을 구현해요.

import React, { createContext, useContext, useMemo, useState } from 'react';
import { HolderOutlined } from '@ant-design/icons';
import type { DragEndEvent, DraggableAttributes } from '@dnd-kit/core';
import { DndContext } from '@dnd-kit/core';
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Card, List } from 'antd';
import type { GetProps } from 'antd';

interface SortableListItemContextProps {
  setActivatorNodeRef?: (element: HTMLElement | null) => void;
  listeners?: SyntheticListenerMap;
  attributes?: DraggableAttributes;
}

const SortableListItemContext = createContext<SortableListItemContextProps>({});

const DragHandle: React.FC = () => {
  const { setActivatorNodeRef, listeners, attributes } = useContext(SortableListItemContext);
  return (
    <Button
      type="text"
      size="small"
      icon={<HolderOutlined />}
      style={{ cursor: 'move' }}
      ref={setActivatorNodeRef}
      {...attributes}
      {...listeners}
    />
  );
};

const SortableListItem: React.FC<GetProps<typeof List.Item> & { itemKey: number }> = (props) => {
  const { itemKey, style, ...rest } = props;

  const {
    attributes,
    listeners,
    setNodeRef,
    setActivatorNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: itemKey });

  const listStyle: React.CSSProperties = {
    ...style,
    transform: CSS.Translate.toString(transform),
    transition,
    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
  };

  const memoizedValue = useMemo<SortableListItemContextProps>(
    () => ({ setActivatorNodeRef, listeners, attributes }),
    [setActivatorNodeRef, listeners, attributes],
  );

  return (
    <SortableListItemContext.Provider value={memoizedValue}>
      <List.Item {...rest} ref={setNodeRef} style={listStyle} />
    </SortableListItemContext.Provider>
  );
};

const App: React.FC = () => {
  const [data, setData] = useState([
    { key: 1, title: 'Title 1' },
    { key: 2, title: 'Title 2' },
    { key: 3, title: 'Title 3' },
    { key: 4, title: 'Title 4' },
    { key: 5, title: 'Title 5' },
    { key: 6, title: 'Title 6' },
  ]);

  const onDragEnd = ({ active, over }: DragEndEvent) => {
    if (!active || !over) {
      return;
    }
    if (active.id !== over.id) {
      setData((prevState) => {
        const activeIndex = prevState.findIndex((i) => i.key === active.id);
        const overIndex = prevState.findIndex((i) => i.key === over.id);
        return arrayMove(prevState, activeIndex, overIndex);
      });
    }
  };

  return (
    <DndContext onDragEnd={onDragEnd} id="list-grid-drag-sorting-handler">
      <SortableContext items={data.map((i) => i.key)}>
        <List
          grid={{ gutter: 16, column: 4 }}
          dataSource={data}
          renderItem={(item) => (
            <SortableListItem key={item.key} itemKey={item.key}>
              <Card
                title={
                  <>
                    <DragHandle />
                    {item.title}
                  </>
                }
              >
                Card content
              </Card>
            </SortableListItem>
          )}
        />
      </SortableContext>
    </DndContext>
  );
};

export default App;

가상 목록 (virtual list)

@rc-component/virtual-list을 사용한 무한·가상화 목록 예시예요.

import React, { useEffect, useState } from 'react';
import VirtualList from '@rc-component/virtual-list';
import { Avatar, List, message } from 'antd';

interface UserItem {
  email: string;
  gender: string;
  name: string;
  avatar: string;
}

const CONTAINER_HEIGHT = 400;
const PAGE_SIZE = 20;

const App: React.FC = () => {
  const [messageApi, contextHolder] = message.useMessage();
  const [data, setData] = useState<UserItem[]>([]);
  const [page, setPage] = useState(1);

  const appendData = (showMessage = true) => {
    const fakeDataUrl = `https://660d2bd96ddfa2943b33731c.mockapi.io/api/users/?page=${page}&limit=${PAGE_SIZE}`;
    fetch(fakeDataUrl)
      .then((res) => res.json())
      .then((body) => {
        const results = Array.isArray(body) ? body : [];
        setData(data.concat(results));
        setPage(page + 1);
        showMessage && messageApi.success(`${results.length} more items loaded!`);
      })
      .catch(() => {
        console.log('fetch mock data failed');
      });
  };

  useEffect(() => {
    appendData(false);
  }, []);

  const onScroll = (e: React.UIEvent<HTMLElement, UIEvent>) => {
    // Refer to: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#problems_and_solutions
    if (
      Math.abs(e.currentTarget.scrollHeight - e.currentTarget.scrollTop - CONTAINER_HEIGHT) <= 1
    ) {
      appendData();
    }
  };

  return (
    <>
      {contextHolder}
      <List>
        <VirtualList
          data={data}
          height={CONTAINER_HEIGHT}
          itemHeight={47}
          itemKey="email"
          onScroll={onScroll}
        >
          {(item: UserItem) => (
            <List.Item key={item.email}>
              <List.Item.Meta
                avatar={<Avatar src={item.avatar} />}
                title={<a href="https://ant.design">{item.name}</a>}
                description={item.email}
              />
              <div>Content</div>
            </List.Item>
          )}
        </VirtualList>
      </List>
    </>
  );
};

export default App;

API

공통 props는 Common props를 참고해요.

List

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version) 글로벌 설정
bordered 목록 주변에 테두리 렌더링 여부 boolean false ×
dataSource 목록의 데이터 배열 any[] - ×
footer 목록 푸터 렌더러 ReactNode - ×
grid 목록의 그리드 타입. grid를 {gutter: 16, column: 4}처럼 설정할 수 있어요. object - ×
header 목록 헤더 렌더러 ReactNode - ×
itemLayout 목록의 레이아웃 horizontal | vertical horizontal ×
loading 목록 콘텐츠를 가져오는 동안 로딩 표시기를 보여 줘요. boolean | SpinProps (more) false ×
loadMore 더 불러오기 콘텐츠 표시 ReactNode - ×
locale 빈 텍스트를 포함한 i18n 텍스트 object {emptyText: No Data} ×
pagination 페이지네이션 설정. false로 설정해 숨겨요. boolean | object false ×
renderItem dataSource 사용 시 목록 항목 커스터마이즈 (item: T, index: number) => ReactNode - ×
rowKey 항목의 고유 값이에요. React.Key 타입 고유 값을 가진 항목의 key이거나, 항목을 받아 React.Key를 반환하는 함수일 수 있어요. keyof T | (item: T) => React.Key "key" ×
size 목록 크기 default | large | small default ×
split 목록 항목 아래 구분선 렌더링 여부 boolean true ×

pagination

페이지네이션 속성이에요.

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default)
position Pagination의 위치 지정 top | bottom | both bottom
align Pagination의 정렬 지정 start | center | end end

페이지네이션에 대한 더 자세한 내용은 Pagination 문서를 확인해 주세요.

List grid props

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
column 그리드의 열 number -
gutter 그리드 사이 간격 number 0
xs <576px 그리드 열 number -
sm ≥576px 그리드 열 number -
md ≥768px 그리드 열 number -
lg ≥992px 그리드 열 number -
xl ≥1200px 그리드 열 number -
xxl ≥1600px 그리드 열 number -
xxxl ≥1920px 그리드 열 number - 6.3.0

List.Item

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version) 글로벌 설정
actions 목록 항목의 동작 콘텐츠예요. itemLayout이 vertical이면 아래쪽에, 아니면 맨 오른쪽에 표시돼요. Array<ReactNode> - ×
classNames 시맨틱 구조 className Record<actions | extra, string> - 5.18.0 5.18.0
extra 목록 항목의 추가 콘텐츠예요. itemLayout이 vertical이면 오른쪽에, 아니면 맨 오른쪽에 표시돼요. ReactNode - ×
styles 시맨틱 DOM 스타일 Record<actions | extra, CSSProperties> - 5.18.0 5.18.0

List.Item.Meta

속성 (Property) 설명 (Description) 타입 (Type) 기본값 (Default) 버전 (Version)
avatar 목록 항목의 아바타 ReactNode -
description 목록 항목의 설명 ReactNode -
title 목록 항목의 제목 ReactNode -

시맨틱 DOM (Semantic DOM)

시맨틱 DOM 구조는 https://ant.design/components/list/semantic.md 에서 확인할 수 있어요.

디자인 토큰 (Design Token)

컴포넌트 토큰 (List) (Component Token)

토큰 이름 (Token Name) 설명 (Description) 타입 (Type) 기본값 (Default Value)
avatarMarginRight 아바타의 오른쪽 여백 MarginRight<string | number> | undefined 16
contentWidth 콘텐츠의 너비 string | number 220
descriptionFontSize 설명의 글자 크기 number 14
emptyTextPadding 빈 텍스트의 패딩 Padding<string | number> | undefined 16
footerBg 푸터의 배경색 string transparent
headerBg 헤더의 배경색 string transparent
itemPadding 항목의 패딩 string 12px 0
itemPaddingLG 큰 항목의 패딩 string 16px 24px
itemPaddingSM 작은 항목의 패딩 string 8px 16px
metaMarginBottom meta의 아래쪽 여백 MarginBottom<string | number> | undefined 16
titleMarginBottom 제목의 아래쪽 여백 MarginBottom<string | number> | undefined 12

글로벌 토큰 (Global Token)

토큰 이름 (Token Name) 설명 (Description) 타입 (Type) 기본값 (Default Value)
borderRadiusLG LG 크기 테두리 반경이에요. Card, Modal 등 큰 테두리 반경을 가진 컴포넌트에 사용돼요. number
colorBorder 기본 테두리 색이에요. 폼 구분선, 카드 구분선처럼 서로 다른 요소를 구분하는 데 사용돼요. string
colorPrimary 브랜드 색은 제품의 특성과 커뮤니케이션을 반영하는 가장 직접적인 시각 요소 중 하나예요. 브랜드 색을 선택하면 자동으로 완전한 색 팔레트가 생성되고 유효한 디자인 시맨틱이 부여돼요. string
colorSplit 구분자 색으로 사용돼요. colorBorderSecondary와 같은 색이지만 투명도를 가져요. string
colorText W3C 표준을 따르는 기본 텍스트 색이에요. 가장 어두운 중성색이기도 해요. string
colorTextDescription 텍스트 설명의 글자 색을 제어해요. string
colorTextDisabled 비활성 상태의 텍스트 색을 제어해요. string
controlHeight Ant Design에서 버튼, 입력 상자 같은 기본 컨트롤의 높이 number
controlHeightLG LG 컴포넌트 높이 number
fontFamily Ant Design의 글꼴은 시스템의 기본 인터페이스 글꼴을 우선시하고, 화면 표시에 적합한 대체 글꼴 라이브러리를 제공해 플랫폼과 브라우저에 따라 가독성을 유지하며 친근하고 안정적이며 전문적인 특성을 반영해요. string
fontSize 디자인 시스템에서 가장 널리 쓰이는 글자 크기로, 여기서 텍스트 그라데이션이 파생돼요. number
fontSizeLG 큰 글자 크기 number
fontSizeSM 작은 글자 크기 number
lineHeight 텍스트의 줄 높이예요. number
lineHeightLG 큰 텍스트의 줄 높이예요. number
lineType 기본 컴포넌트의 테두리 스타일 string
lineWidth 기본 컴포넌트의 테두리 두께 number
margin 중간 크기의 요소 여백을 제어해요. number
marginLG 큰 크기의 요소 여백을 제어해요. number
marginSM 중간-작은 크기의 요소 여백을 제어해요. number
marginXXL 가장 큰 크기의 요소 여백을 제어해요. number
marginXXS 가장 작은 크기의 요소 여백을 제어해요. number
motionDurationSlow 동작 속도, 느린 속도예요. 큰 요소의 애니메이션 상호작용에 사용돼요. string
padding 요소의 패딩을 제어해요. number
paddingLG 요소의 큰 패딩을 제어해요. number
paddingSM 요소의 작은 패딩을 제어해요. number
paddingXS 요소의 아주 작은 패딩을 제어해요. number
screenMD 중간 화면의 화면 너비를 제어해요. number
screenSM 작은 화면의 화면 너비를 제어해요. number

FAQ {#faq}

더 이상 사용되지 않는 List 컴포넌트의 대체재가 있나요? {#faq-listy-replacement}

네 — [email protected]부터 사용할 수 있는 Listy를 사용해 주세요. List의 후속 컴포넌트로, 가상 스크롤, 스티키 그룹 헤더, 프로그래매틱 스크롤 같은 내장 기능을 갖추고 있어요. 유연한 커스텀 렌더링도 지원하며 다양한 시나리오의 목록 요구를 아우르는 것을 목표로 해요.

List에서 어떻게 마이그레이션하나요? {#faq-migrate-from-list}

목록은 시나리오마다 형태가 달라 일대일 마이그레이션 가이드를 주기는 어려워요. Listy의 커스텀 렌더링 덕분에 List의 프리셋 구조를 itemRender 안의 일반 JSX로 다시 구성할 수 있어요.

매핑은 다음과 같아요.

  • 데이터와 렌더링: dataSource는 items로, renderItem은 itemRender로 매핑돼요. rowKey는 의미를 유지하지만 Listy에서는 필수이며 더 이상 key 필드로 기본 설정되지 않아요. 기본 설정에 의존했다면 rowKey="key"를 명시적으로 전달해 주세요. 대용량 데이터셋에서는 서드파티 의존성이 필요 없어요. virtual을 켜고 height를 지정하면 가상 스크롤을 얻을 수 있어요.
  • 한 행 안의 프리셋 구조: List.Item, List.Item.Meta, actions, extra는 모두 itemRender 안에서 다시 구성할 수 있어요.
  • 목록 바깥의 구조: header와 footer는 Listy 바로 바깥에 두고, loading은 Spin으로 감싸며, pagination은 데이터를 직접 잘라 Pagination과 함께 items에 전달하고, loadMore는 무한 로딩 데모를 참고해요.
  • 스타일: bordered, split, size는 시맨틱 DOM classNames / styles와 Design Token으로 조정해요. grid는 Listy로 마이그레이션하는 것을 권장하지 않아요. 대신 Row / Col과 Card로 카드 레이아웃을 만드세요.

더 알아보기 (Learn more)