목록
목록 (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는 시맨틱 DOMclassNames/styles와 Design Token으로 조정해요.grid는 Listy로 마이그레이션하는 것을 권장하지 않아요. 대신 Row / Col과 Card로 카드 레이아웃을 만드세요.