DefinitionList | CoUI
본문으로 건너뛰기
LogoCoUI

DefinitionList

용어와 설명을 한 쌍으로 묶어 읽는 순서까지 문서에 남기는 정의 목록 컴포넌트

목차

용어(term)와 그 설명(description)을 한 쌍으로 묶어 나열하는 컴포넌트입니다. 값만 놓고는 뜻이 서지 않는 정보 — 사업자등록번호, 식별자, 날짜, 금액, 스펙 항목 — 에 각자의 라벨을 붙여, 쌍 관계가 시각 배치가 아니라 문서 구조에 남습니다.

기본 사용법 (Basic Usage)#

// 기본 — 용어 위, 설명 아래 (stacked)
DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
    CoreDefinitionEntry(term: '대표', description: '한동우'),
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
  ],
)

// 두 열로 — 설명이 같은 모서리에서 시작하고 자릿수가 맞는다
DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '공급가액', description: '1,200,000'),
    CoreDefinitionEntry(term: '부가세', description: '120,000'),
  ],
  layout: CoreDefinitionLayout.columns,
  numeric: true,
)

// 설명이 링크·주소 블록처럼 텍스트 한 줄이 아닐 때
DefinitionList(
  items: [
    CoreDefinitionEntry.rich(
      term: '이메일',
      description: Link(href: 'mailto:cody@cocode.im', child: Text('cody@cocode.im')),
    ),
  ],
)
// 기본 — 용어 위, 설명 아래 (stacked)
DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
    CoreDefinitionEntry(term: '대표', description: '한동우'),
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
  ],
)

// 두 열로 — 설명이 같은 모서리에서 시작하고 자릿수가 맞는다
DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '공급가액', description: '1,200,000'),
    CoreDefinitionEntry(term: '부가세', description: '120,000'),
  ],
  layout: CoreDefinitionLayout.columns,
  numeric: true,
)

// 설명이 링크·주소 블록처럼 텍스트 한 줄이 아닐 때
DefinitionList(
  items: [
    CoreDefinitionEntry.rich(
      term: '이메일',
      description: Link(href: 'mailto:cody@cocode.im', child: Text('cody@cocode.im')),
    ),
  ],
)

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 푸터의 사업자 정보(상호 · 대표 · 사업자등록번호 · 통신판매업신고번호)처럼 라벨 없이는 읽히지 않는 값을 나열할 때
  • 주문 상세, 배송 정보, 결제 내역처럼 "항목명 → 값" 구조가 반복될 때
  • 제품 스펙 시트나 메타데이터 패널처럼 키·값 쌍이 목록을 이룰 때
  • 값이 숫자라서 자릿수가 세로로 맞아야 할 때 (layout: columns + numeric: true)

대신 다른 컴포넌트를 사용하세요:

  • Table: 행이 여러 열의 값을 갖는 2차원 데이터일 때. 정의 목록은 항상 한 용어에 한 설명입니다.
  • DescriptionCard: 같은 라벨·값 목록에 카드 표면·제목·행 강조·하단 액션까지 필요할 때. 표면 없이 용어/설명 관계만 나열하면 DefinitionList 입니다.
  • Stat: 하나의 지표를 크게 강조해 보여줄 때.
  • TextBlock: 라벨 없는 산문 문단일 때.

Props / Parameters#

속성타입기본값설명
items List<CoreDefinitionEntry<W>> 필수 읽는 순서대로 나열할 용어 / 설명 쌍. W 는 Flutter Widget, Web Component
layout CoreDefinitionLayout stacked 용어를 설명에 대해 어떻게 배치할지 ( inline / stacked / columns ). 시각 전용 — 읽히는 순서는 세 값 모두 같다
numeric bool false 설명을 고정폭 숫자(tabular figures)로 그린다. 자릿수가 세로로 맞아야 하는 등록번호·금액·날짜용. 글자도 의미도 바꾸지 않는 순수 타이포그래피 설정
mono bool false 설명을 Geist Mono( code-sm , 12px · 21px 줄 상자)로 그린다. 식별자·해시·코드용. numeric 과 함께 쓸 수 있다
definitionListStyle CoreDefinitionListStyle? null 모든 간격·타이포그래피·용어 열 폭이 지나는 단일 슬롯 (아래 표 참고)

항목 타입 — CoreDefinitionEntry#

term 은 언제나 String 입니다. 스크린 리더가 값보다 먼저 읽는 라벨이고, 임의 마크업을 허용하면 텍스트가 없는 아이콘도 용어가 될 수 있어 타입 자체가 그 경우를 거절합니다.

생성자설명
CoreDefinitionEntry(term:, description:) description 이 평범한 String 인 경우. 대부분 이쪽
CoreDefinitionEntry.rich(term:, description:) description 이 링크·주소 블록처럼 위젯 / 컴포넌트인 경우

스타일 시스템 (Style System)#

CoreDefinitionListStyle 필드#

필드타입설명
entrySpacing double? Distance between one term / description pair and the next, in logical pixels. Maps to row-gap in CoreDefinitionLayout.columns , to the <dt> 's leading block margin in stacked , and to the <dt> 's leading inline margin in inline . The default is defaultsByLayout[layout].entrySpacing — 14 stacked, 24 in columns, 16 inline. It differs per layout, so there is no single scalar default.
pairSpacing double? Distance between a term and its own description, in logical pixels. Maps to column-gap in CoreDefinitionLayout.columns , and to the <dd> 's leading block / inline margin in stacked / inline . The default is defaultsByLayout[layout].pairSpacing — 2 stacked, 8 in columns and inline. It differs per layout, so there is no single scalar default.
termTextStyle CoreTextStyle? <dt> typography override. Layered over the layout row of [defaultsByLayout] — a caller who sets only fontWeight keeps the default role and colour. The default differs per layout ( label-small stacked, label-medium in columns and inline), so there is no single scalar default.
descriptionTextStyle CoreTextStyle? <dd> typography override. Layered over the layout row of [defaultsByLayout] (or [defaultMonoDescriptionTextStyle] when the list is mono ) on the same terms as [termTextStyle].
monoDescriptionTextStyle CoreTextStyle? <dd> typography override that applies only when the list is mono . Layered over [defaultMonoDescriptionTextStyle] and then over any [descriptionTextStyle], so a theme can recolour every description while a mono list alone changes its code weight.
termWidth double? Fixed width of the term column, in logical pixels — only CoreDefinitionLayout.columns reads it; stacked and inline ignore it. No default* by design — absence means max-content : the column is as wide as its widest term, and a long term squeezes the description column. Setting a width holds the column there, so descriptions start at the same edge across lists whose terms differ.

두 간격이 축 이름(row-gap / column-gap)이 아니라 관계 이름을 갖는 이유는 어느 축에 얹히는지가 layout 마다 다르기 때문입니다. stacked 에서는 용어와 자기 설명도 세로로 쌓이고 이웃한 두 쌍도 세로로 쌓이므로, 축으로 이름 지으면 두 거리를 구별할 수 없습니다. entrySpacing 은 쌍과 쌍 사이, pairSpacing 은 용어와 자기 설명 사이이며 — 묶여 보이는 근거가 근접성이라 pairSpacing 이 언제나 더 작아야 합니다.

빠른 오버라이드 (Chain)#

이미 만든 DefinitionList 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class DefinitionListChainExample extends StatelessWidget {
  const DefinitionListChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const DefinitionList(
      items: [
        CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
        CoreDefinitionEntry(term: '대표', description: '한동우'),
        CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
      ],
      layout: CoreDefinitionLayout.columns,
      numeric: true,
    ).withStyle(
      const CoreDefinitionListStyle(
        entrySpacing: CoreSpace.space24,
        pairSpacing: CoreSpace.space16,
        termTextStyle: CoreTextStyle.token(
          CoreTextStyles.labelMedium,
          color: CoreColor.token(CoreColors.primary),
        ),
        descriptionTextStyle: CoreTextStyle.token(
          CoreTextStyles.bodyLarge,
          color: CoreColor.token(CoreColors.onSurface),
        ),
      ),
    );
  }
}
class DefinitionListChainExample extends StatelessComponent {
  const DefinitionListChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return const DefinitionList(
      items: [
        CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
        CoreDefinitionEntry(term: '대표', description: '한동우'),
        CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
      ],
      layout: CoreDefinitionLayout.columns,
      numeric: true,
    ).withStyle(
      const CoreDefinitionListStyle(
        entrySpacing: CoreSpace.space24,
        pairSpacing: CoreSpace.space16,
        termTextStyle: CoreTextStyle.token(
          CoreTextStyles.labelMedium,
          color: CoreColor.token(CoreColors.primary),
        ),
        descriptionTextStyle: CoreTextStyle.token(
          CoreTextStyles.bodyLarge,
          color: CoreColor.token(CoreColors.onSurface),
        ),
      ),
    );
  }
}

변형 (Variants)#

Default (stacked)#

용어 위, 설명 아래. 어떤 폭에서도 넘치지 않는 유일한 배치라서 기본값입니다 — 긴 용어는 열을 넓히는 대신 본문 폭 안에서 줄바꿈합니다.

DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
    CoreDefinitionEntry(term: '대표', description: '한동우'),
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
  ],
)

Columns#

두 열(max-content / 나머지 전부). 모든 설명이 같은 모서리에서 시작하므로 두 번째 열에 쌓인 숫자가 세로로 정렬됩니다 — 자릿수까지 맞추려면 numeric: true 를 함께 씁니다.

용어 열은 기본이 max-content 라 줄어들지 않습니다. 긴 용어는 줄바꿈하는 대신 설명 열을 좁히므로, 용어가 길거나 컨테이너가 좁으면 stacked 를 쓰세요. termWidth 를 주면 용어 열이 그 폭(px)에 고정되어, 용어가 다른 여러 목록의 설명이 같은 모서리에서 시작합니다.

DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
    CoreDefinitionEntry(term: '대표', description: '한동우'),
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
  ],
  layout: CoreDefinitionLayout.columns,
  numeric: true,
)

Inline#

용어와 설명이 한 줄에 이어 붙고, 폭이 모자라면 쌍 단위로 다음 줄에 넘어갑니다. 짧은 값이 짧게 이어지는 푸터 한 줄 같은 곳에 씁니다.

한 쌍이 통째로 하나의 wrap 항목이라, 용어만 줄 끝에 남고 값이 다음 줄로 떨어지는 일은 생기지 않습니다.

DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
    CoreDefinitionEntry(term: '대표', description: '한동우'),
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
  ],
  layout: CoreDefinitionLayout.inline,
)

동작 스펙 (Behavior)#

인터랙션#

  • 표시 전용입니다. 제스처도 컨트롤러도 상태도 없습니다 — 정적 렌더링에서 그대로 읽혀야 하는 내용이기 때문입니다.
  • 인터랙션이 필요한 값(링크·버튼)은 CoreDefinitionEntry.rich 로 넣습니다. 그 서브트리가 자기 동작을 그대로 유지합니다.

레이아웃#

  • layout 은 상자만 옮깁니다. 세 값 모두 같은 쌍 구조를 내보내므로 스크린 리더가 읽는 순서는 바뀌지 않습니다.
  • 쌍 사이 간격(entrySpacing)은 첫 쌍에는 붙지 않습니다 — 첫 쌍은 목록을 여는 것이지 무언가를 뒤따르는 것이 아니라서, 목록 맨 위에 아무도 요청하지 않은 빈 띠가 생기지 않습니다.
  • columns 에서 용어와 설명은 baseline 으로 정렬됩니다. mono 설명은 code-sm 의 21px 줄 상자를 쓰고 용어는 label-medium 의 줄 상자를 쓰므로(18.2px), 다른 정렬은 그 차이만큼 어긋납니다.

타이포그래피#

  • stacked 의 용어는 설명보다 한 단 작습니다 — 용어는 label-small 12/500, 설명은 label-medium 13/500 입니다. columns · inline 은 둘 다 label-medium 13/500 이고, 나란히 선 위치(열 · 앞뒤 순서)가 둘을 가르며 용어의 흐린 색이 그 구분을 보강합니다.
  • 간격도 배치마다 다릅니다 — 쌍 안은 stacked 2 · columns / inline 8, 쌍 사이는 stacked 14 · columns 24 · inline 16 입니다.
  • numeric: true 는 고정폭 숫자만 켭니다. 글자도 의미도 바꾸지 않습니다.

사용 가이드라인 (Usage Guidelines)#

✅ Do#

값에는 항상 그 값을 설명하는 용어를 붙인다

DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
    CoreDefinitionEntry(term: '통신판매업신고번호', description: '2019-서울강남-01234'),
  ],
)

두 번호를 라벨 없이 나열하면 듣는 사람은 숫자만 듣고 어느 대장에서 조회해야 하는지 알 수 없습니다.


❌ Don't#

용어와 값을 한 문자열에 밀어 넣지 않는다

// ❌ 쌍 관계가 문서에 남지 않는다 — 한 줄의 텍스트일 뿐
Text('사업자등록번호 152-81-04005 · 법인등록번호 110111-0923034')

가운뎃점으로 나눈 한 줄은 스크린 리더에게 끊기지 않는 숫자 나열로 읽힙니다. 바로 이 상황을 없애려고 있는 컴포넌트입니다.

✅ Do#

숫자를 세로로 비교해야 하면 columns + numeric

DefinitionList(
  items: const [
    CoreDefinitionEntry(term: '공급가액', description: '1,200,000'),
    CoreDefinitionEntry(term: '부가세', description: '120,000'),
    CoreDefinitionEntry(term: '합계', description: '1,320,000'),
  ],
  layout: CoreDefinitionLayout.columns,
  numeric: true,
)

columns 가 설명의 시작 모서리를 맞추고, numeric 이 자릿수를 맞춥니다. 둘은 짝입니다 — 한쪽만 쓰면 눈이 자릿수를 세야 합니다.


❌ Don't#

긴 용어에 columns 를 쓰지 않는다

// ❌ 용어 열이 줄어들지 않아 설명 열이 짓눌린다
DefinitionList(
  items: const [
    CoreDefinitionEntry(
      term: '전자상거래 등에서의 소비자보호에 관한 법률에 따른 통신판매업신고번호',
      description: '2019-서울강남-01234',
    ),
  ],
  layout: CoreDefinitionLayout.columns,
)

용어 열은 기본이 max-content 라 줄어들지 않습니다. 용어가 길거나 컨테이너가 좁으면 stacked 를 쓰세요 — 어떤 폭에서도 넘치지 않는 유일한 배치입니다.

접근성 (Accessibility)#

이 컴포넌트가 존재하는 이유가 접근성입니다. 라벨 없는 값의 나열("152-81-04005", "110111-0923034")은 붙일 데 없는 숫자로 읽히고, 각 쌍이 자기 라벨을 들고 있으면 그렇지 않습니다.

역할#

Web 은 네이티브 <dl> / <dt> / <dd> 구조를 씁니다. Flutter 는 설명 노드에 용어·값을 실어 쌍 관계를 전달합니다. 표시 목록 자체는 버튼이나 선택 컨트롤이 아닙니다.

키보드#

목록 자신이 처리하는 키나 tab stop 은 없습니다. CoreDefinitionEntry.rich 로 넣은 링크·버튼은 자기 키보드 동작을 그대로 유지합니다.

스크린 리더#

  • Web: 루트가 언제나 <dl> 이고 직계 자식은 <dt> / <dd> 뿐입니다 — 사이에 래퍼 <div> 를 두지 않습니다. 스펙은 <div> 그룹핑을 허용하지만 그 <div> 를 따라가다 멈추는 보조기술이 실제로 있고, 이 컴포넌트는 바로 그 독자를 위해 존재합니다. 세 레이아웃 모두 래퍼가 아니라 <dl> 자신의 grid / flex 로 만들어집니다.
  • Flutter: <dl> 에 해당하는 위젯이 없으므로 쌍 관계를 접근성 API 로 진술합니다. 용어는 ExcludeSemantics 안에서 그려져 자기 노드를 만들지 않고, 설명의 노드가 용어를 label 로, 값을 value 로 싣습니다. 그래서 한 쌍당 한 번 — "사업자등록번호, 152-81-04005" — 으로 읽히며 용어가 두 번 발음되지 않습니다.
  • rich 설명: 링크·주소 블록처럼 자기 동작을 가진 서브트리는 시맨틱을 그대로 유지합니다. 노드가 하나면 용어가 그 노드에 이름으로 붙어 "이메일 cody@cocode.im, 버튼" 처럼 한 번에 읽히고, 여러 개면 용어가 이름인 그룹 아래 각자 남습니다 — 두 동작이 하나로 뭉개지지 않습니다.

읽는 순서#

layout 은 상자만 옮깁니다. inline · stacked · columns 세 값 모두 같은 쌍 구조를 내보내므로 발음되는 순서는 동일합니다.

대비#

용어와 설명은 크기(stacked) 또는 위치(columns · inline)로 먼저 구분되고 색은 그 구분을 보강할 뿐입니다 (WCAG 1.4.1). 용어 색을 바꿀 때도 이 구분은 남겨 두세요.

포커스 관리#

포커스 이동·복귀·트랩은 없습니다. 레이아웃을 바꿔도 자식의 선언 순서와 포커스 동작을 유지합니다.

알려진 제약#

Flutter 에는 네이티브 정의 목록 역할이 없어 Web 의 <dl> 구조와 같은 역할 어휘로 읽히지는 않습니다. rich 콘텐츠의 이름과 동작은 호출자가 제공해야 합니다. 자동 live region 이 아니므로 외부에서 값이 바뀐 사실을 별도로 통보하지 않습니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명DefinitionListDefinitionList
루트 Column / Table / Wrap (레이아웃별) 언제나 <dl>
쌍 관계 ExcludeSemantics (용어) + Semantics(label:, value:) (설명) <dt> / <dd> 요소 자체
stacked Column(spacing: entrySpacing) 안의 Column(spacing: pairSpacing) <dl> 이 flex-col , 간격은 <dt> / <dd> 의 leading margin
columns Table ( IntrinsicColumnWidth 또는 termWidth 의 FixedColumnWidth / FlexColumnWidth , baseline 정렬) <dl> 이 grid-template-columns: max-content 1fr ( termWidth 가 있으면 <rem> 1fr ) + row-gap / column-gap
inline Wrap 안의 Row (한 쌍 = wrap 항목 하나) <dl> 이 flex-wrap , 간격은 논리 축( margin-inline-start )이라 rtl 에서 뒤집힌다
numeric FontFeature.tabularFigures() tabular-nums (font-variant-numeric)
mono code-sm 역할의 설명 글자 <dd> 의 text-code-sm 클래스
브라우저 기본값 해당 없음 <dd> 의 UA margin-inline-start: 40px 를 리셋한다 — grid / flex 에서도 살아남아 열 정렬을 무너뜨리기 때문
  • Table: 행이 여러 열의 값을 갖는 2차원 데이터용. 정의 목록은 한 용어에 한 설명입니다.
  • DescriptionCard: 제목·본문·액션을 갖춘 카드 표면이 필요할 때.
  • TextBlock: 라벨 없는 산문 문단.
  • Stat: 하나의 지표를 크게 강조할 때.

조합 예제#

// 푸터의 사업자 정보 — Divider 아래 DefinitionList 한 벌
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Divider(),
    Gap(size: CoreSpace.space16),
    DefinitionList(
      items: const [
        CoreDefinitionEntry(term: '상호', description: '주식회사 코코드'),
        CoreDefinitionEntry(term: '대표', description: '한동우'),
        CoreDefinitionEntry(term: '사업자등록번호', description: '152-81-04005'),
        CoreDefinitionEntry(term: '통신판매업신고번호', description: '2019-서울강남-01234'),
      ],
      layout: CoreDefinitionLayout.columns,
      numeric: true,
    ),
  ],
)