메이슨리

메이슨리 (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

더 알아보기 (Learn more)