메이슨리
메이슨리 (Masonry)
높이가 각기 다른 콘텐츠를 벽돌처럼 쌓는 메이슨리(masonry) 레이아웃 컴포넌트예요. 이미지나 카드처럼 높이가 제각각인 요소를 보기 좋게 배열해요.
출처: 문서
본문
높이가 다른 콘텐츠를 표시하기 위한 메이슨리 레이아웃 컴포넌트예요.
언제 사용하나요 (When To Use)
- 높이가 불규칙한 이미지나 카드를 표시할 때 사용해요.
- 콘텐츠를 열 단위로 고르게 분배해야 할 때 사용해요.
- 열 개수를 반응형으로 조절해야 할 때 사용해요.
예시 (Examples)
기본 (Basic)
기본 사용법이에요. columns로 열 개수를, gutter로 간격을 설정해요.
import React from 'react';
import { Card, Masonry } from 'antd';
import type { MasonryProps } from 'antd';
type MasonryItemType = NonNullable<MasonryProps<number>['items']>[number];
const heights = [150, 50, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 60, 50, 80].map(
(height, index) => {
const item: MasonryItemType = {
key: `item-${index}`,
data: height,
};
if (index === 4) {
item.children = (
<Card
size="small"
cover={
<img
alt="food"
src="https://images.unsplash.com/photo-1491961865842-98f7befd1a60?w=523&auto=format"
/>
}
>
<Card.Meta title="I'm Special" description="Let's have a meal" />
</Card>
);
}
return item;
},
);
const App: React.FC = () => (
<Masonry
columns={4}
gutter={16}
items={heights}
itemRender={({ data, index }) => (
<Card size="small" style={{ height: data }}>
{index + 1}
</Card>
)}
/>
);
export default App;
반응형 (Responsive)
반응형 레이아웃은 다양한 화면 너비에 맞춰 조정돼요. columns로 브레이크포인트별 열 개수를, gutter로 항목 사이 간격을 조정해요.
import React from 'react';
import { Card, Masonry } from 'antd';
const heights = [120, 55, 85, 160, 95, 140, 75, 110, 65, 130, 90, 145, 55, 100, 80];
const App: React.FC = () => {
const items = heights.map((height, index) => ({
key: `item-${index}`,
data: height,
index,
}));
return (
<Masonry
columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
gutter={{ xs: 8, sm: 12, md: 16 }}
items={items}
itemRender={(item) => (
<Card size="small" style={{ height: item.data }}>
{item.index + 1}
</Card>
)}
/>
);
};
export default App;
이미지 (Image)
이미지가 로드될 때 높이를 동적으로 조정해요.
import React from 'react';
import { Masonry } from 'antd';
const imageList = [
'https://images.unsplash.com/photo-1510001618818-4b4e3d86bf0f',
'https://images.unsplash.com/photo-1507513319174-e556268bb244',
'https://images.unsplash.com/photo-1474181487882-5abf3f0ba6c2',
'https://images.unsplash.com/photo-1492778297155-7be4c83960c7',
'https://images.unsplash.com/photo-1508062878650-88b52897f298',
'https://images.unsplash.com/photo-1506158278516-d720e72406fc',
'https://images.unsplash.com/photo-1552203274-e3c7bd771d26',
'https://images.unsplash.com/photo-1528163186890-de9b86b54b51',
'https://images.unsplash.com/photo-1727423304224-6d2fd99b864c',
'https://images.unsplash.com/photo-1675090391405-432434e23595',
'https://images.unsplash.com/photo-1554196967-97a8602084d9',
'https://images.unsplash.com/photo-1491961865842-98f7befd1a60',
'https://images.unsplash.com/photo-1721728613411-d56d2ddda959',
'https://images.unsplash.com/photo-1731901245099-20ac7f85dbaa',
'https://images.unsplash.com/photo-1617694455303-59af55af7e58',
'https://images.unsplash.com/photo-1709198165282-1dab551df890',
];
const App = () => (
<Masonry
columns={4}
gutter={16}
items={imageList.map((img, index) => ({
key: `item-${index}`,
data: img,
}))}
itemRender={({ data }) => (
<img src={`${data}?w=523&auto=format`} alt="sample" style={{ width: '100%' }} />
)}
/>
);
export default App;
동적 업데이트 (Dynamic)
메이슨리 레이아웃이 동적으로 업데이트되는 모습을 보여 줘요. item.column으로 항목을 제자리에 유지할 수 있어요.
import React, { useState } from 'react';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Flex, Masonry, theme } from 'antd';
const heights = [150, 50, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 70, 50, 80];
type ItemType = {
key: number;
column?: number;
data: number;
};
const Update: React.FC = () => {
const { token } = theme.useToken();
const [items, setItems] = useState<ItemType[]>(() =>
heights.map((height, index) => ({
key: index,
column: index % 4,
data: height,
})),
);
const removeItem = (removeKey: React.Key) => {
setItems((prevItems) => prevItems.filter(({ key }) => key !== removeKey));
};
const addItem = () => {
setItems((prevItems) => [
...prevItems,
{
key: prevItems.length ? prevItems[prevItems.length - 1].key + 1 : 0,
data: Math.floor(Math.random() * 100) + 50,
},
]);
};
return (
<Flex vertical gap={16}>
<Masonry
columns={4}
gutter={16}
items={items}
itemRender={({ data, key }) => (
<Card size="small" style={{ height: data }}>
{Number(key) + 1}
<Button
style={{
position: 'absolute',
insetBlockStart: token.paddingSM,
insetInlineEnd: token.paddingSM,
}}
size="small"
icon={<CloseOutlined />}
onClick={() => removeItem(key)}
/>
</Card>
)}
onLayoutChange={(sortedItems) => {
setItems((prevItems) =>
prevItems.map((item) => {
const matchItem = sortedItems.find((sortedItem) => sortedItem.key === item.key);
return matchItem
? {
...item,
column: matchItem.column,
}
: item;
}),
);
}}
/>
<Button block onClick={addItem}>
Add Item
</Button>
</Flex>
);
};
export default Update;
시맨틱 DOM 스타일링 (Custom semantic dom styling)
classNames와 styles로 객체나 함수를 전달해 Masonry의 시맨틱 DOM 스타일을 커스터마이즈할 수 있어요.
import React from 'react';
import { Card, Divider, Flex, Masonry, Typography } from 'antd';
import type { GetProp, MasonryProps } from 'antd';
import { createStaticStyles } from 'antd-style';
import type { MasonryItemType } from 'antd/es/masonry/MasonryItem';
const { Title } = Typography;
const classNames = createStaticStyles(({ css }) => ({
root: css`
border: 1px solid #d9d9d9;
border-radius: 8px;
padding: 16px;
height: 260px;
background-color: #fafafa;
`,
item: css`
transform: scale(0.98);
transition: transform 0.2s ease;
border-radius: 12px;
border: 1px solid #ccc;
overflow: hidden;
`,
}));
const items = [120, 80, 100, 60, 140, 90, 110, 70].map<MasonryItemType<number>>(
(height, index) => ({
key: `item-${index}`,
data: height,
}),
);
const styles: MasonryProps['styles'] = {
root: {
borderRadius: 12,
padding: 20,
height: 260,
backgroundColor: 'rgba(250,250,250,0.5)',
},
item: {
transform: 'scale(0.98)',
transition: 'transform 0.2s ease',
border: '1px solid #ccc',
},
};
const stylesFn: MasonryProps['styles'] = (info): GetProp<MasonryProps, 'styles', 'Return'> => {
const { props } = info;
return {
root: {
border: `2px solid ${typeof props.columns === 'number' && props.columns > 2 ? '#1890ff' : '#52c41a'}`,
padding: 20,
height: 280,
backgroundColor: 'rgba(240,248,255,.6)',
},
item: {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #1890ff',
},
};
};
const App: React.FC = () => {
const sharedProps: MasonryProps = {
classNames,
itemRender: ({ data, index }) => (
<Card size="small" style={{ height: data }}>
{index + 1}
</Card>
),
};
return (
<Flex vertical gap={24}>
<div>
<Title level={4}>classNames and styles Object</Title>
<Masonry columns={4} gutter={16} items={items} {...sharedProps} styles={styles} />
</div>
<Divider />
<div>
<Title level={4}>classNames and styles Function</Title>
<Masonry
columns={3}
gutter={12}
items={items.slice(0, 6)}
{...sharedProps}
styles={stylesFn}
/>
</div>
</Flex>
);
};
export default App;
API
공통 props는 Common props를 참고해요.
Masonry
| 속성 (Property) | 설명 (Description) | 타입 (Type) | 기본값 (Default) | 버전 (Version) | 글로벌 설정 |
|---|---|---|---|---|---|
| classNames | 컴포넌트 내부의 각 시맨틱 구조에 대한 class를 지정해요. 객체 또는 함수를 지원해요. | Record<SemanticDOM, string> | (info: { props })=> Record<SemanticDOM, string> | - | 6.0.0 | 6.0.0 |
| columns | 열 개수예요. 고정값이거나 반응형 설정일 수 있어요. | number | { xs?: number; sm?: number; md?: number } |
3 |
× | |
| fresh | 자식 항목의 크기 변화를 계속 모니터링할지 여부예요. | boolean |
false |
× | |
| gutter | 간격이에요. 고정값, 반응형 설정, 가로·세로 간격 설정일 수 있어요. | Gap | [Gap, Gap] | 0 |
× | |
| items | 메이슨리 항목 | MasonryItem[] | - | × | |
| itemRender | 커스텀 항목 렌더링 함수 | (item: MasonryItem) => React.ReactNode |
- | × | |
| styles | 컴포넌트 내부의 각 시맨틱 구조에 대한 인라인 스타일을 지정해요. 객체 또는 함수를 지원해요. | Record<SemanticDOM, CSSProperties> | (info: { props })=> Record<SemanticDOM, CSSProperties> | - | 6.0.0 | 6.0.0 |
| onLayoutChange | 열 정렬 변경 콜백 | ({ key: React.Key; column: number }[]) => void |
- | × |
MasonryItem
| 속성 (Property) | 설명 (Description) | 타입 (Type) | 기본값 (Default Value) |
|---|---|---|---|
| children | 커스텀 표시 콘텐츠로, itemRender보다 우선해요. |
React.ReactNode |
- |
| column | 항목이 속한 열을 지정해요. | number |
- |
| data | 커스텀 데이터 저장소 | T |
- |
| height | 항목의 높이 | number |
- |
| key | 항목의 고유 식별자 | string | number |
- |
Gap
Gap은 항목 사이의 간격을 나타내요. 고정값이거나 반응형 설정일 수 있어요.
type Gap = undefined | number | Partial<Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number>>;
시맨틱 DOM (Semantic DOM)
시맨틱 DOM 구조는 https://ant.design/components/masonry/semantic.md 에서 확인할 수 있어요.
디자인 토큰 (Design Token)
글로벌 토큰 (Global Token)
| 토큰 이름 (Token Name) | 설명 (Description) | 타입 (Type) | 기본값 (Default Value) |
|---|---|---|---|
| motionDurationFast | 동작 속도, 빠른 속도예요. 작은 요소의 애니메이션 상호작용에 사용돼요. | string | |
| motionDurationSlow | 동작 속도, 느린 속도예요. 큰 요소의 애니메이션 상호작용에 사용돼요. | string | |
| motionEaseOut | 미리 정의된 모션 곡선이에요. | string |