Va.Tag — 선택된 항목을 칩(chip)으로 표시하는 다중 선택 입력
Combobox처럼 팝업 리스트에서 항목을 선택하되, 선택된 것들이 필드 안에 X 버튼 붙은 칩(chip/pill)으로 표시되는 컴포넌트입니다. Gmail의 수신자 입력, GitHub의 라벨 선택, 태그 시스템 같은 UI에 정확히 맞습니다.
- 클래스: Va.Tag — va_tag.js:19
- short name: tag
- 상속: Va.PureField (Combobox와 형제)
- 파일: va_tag.js (별도 파일 — import 필요)
- isContainer: true
- 베이스 CSS: va-input (+ 팝업 va-menu-pop, 칩 va-tag-item)

1. 기본 사용
import '../../lib/va_tag.js'; // ← 필수
// config 안에서
{
tagName: 'tag',
key: 'key',
display: 'display',
data: [
{ key: 'js', display: 'JavaScript' },
{ key: 'ts', display: 'TypeScript' },
{ key: 'py', display: 'Python' },
{ key: 'go', display: 'Go' }
],
value: ['js', 'ts'], // 초기 선택된 태그 배열
onSelect: 'onTagChange'
}
렌더 결과: 필드 안에 "JavaScript ✕", "TypeScript ✕" 칩이 나열되고, 클릭 시 팝업에서 나머지 항목 선택 가능.
2. Va.Combobox / Va.Tag의 차이
Combobox와 소스가 거의 판박이입니다. 실제로 코드 대부분이 복사되어 나왔고, 차이는 "선택된 것을 어떻게 표시하는가" 하나에 집중됩니다.
항목Va.Combobox (multiSelect)Va.Tag
| 선택 표시 | 텍스트 필드에 콤마 나열 ("A, B, C") | 칩으로 나열 ([A✕] [B✕] [C✕]) |
| 개별 삭제 UI | ✕ (전체 다시 선택) | ✓ (칩의 X 버튼) |
| multiSelect 기본값 | false | true |
| 선택 후 팝업 | 자동 닫힘(single) / 유지(multi) | 선택된 항목만 팝업 리스트에서 제거되고 유지 |
| 파일 | va_component.js | va_tag.js (별도) |
| 선택 상태 저장 방식 | DataManager의 vaDataSelected 플래그 | _tagSelectedItems 별도 배열 |
| 필드 편집 | 팝업 이외에도 필드 자체 클릭·타이핑 | 필드 tabindex=-1, 편집 안 됨 |
한 줄 요약: "Combobox multi-select의 UI를 텍스트가 아닌 칩으로 바꾼 형태."
3. 주요 속성
Combobox와 거의 동일하지만 다중선택이 기본입니다.
데이터 관련 (Combobox와 공통)
속성기본값설명
| data | — | 항목 배열 |
| fakeData | — | 에디터 미리보기 데이터 |
| key | 'key' | 값 필드명 |
| display | 'display' | 표시 필드명 |
| displayType | 'display' | key / display / both — 칩 안 텍스트 형태 |
| template | 자동 생성 | 팝업 항목 렌더 템플릿 (칩 자체가 아님!) |
| dataMode | 'data' | 데이터 소스 모드 |
선택 동작
속성기본값설명
| value | — | 선택된 키 배열 (single 값 넘겨도 배열로 처리) |
| multiSelect | true | 기본이 true (Combobox는 false) |
| addCheckAll | — | "전체" 체크 |
| clickToSelect | true | 클릭 선택 |
팝업
속성기본값설명
| expanded | false | 팝업 초기 상태 |
| popWidth | 자동(필드 폭) | 팝업 폭 |
| popMaxHeight | '400px' | 팝업 최대 높이 |
반환 타입
속성설명
| getValueType | 'string'이면 getValue()가 "'a','b','c'" 형태 문자열 반환. 미지정 시 배열 |
PureField 상속
placeholder, readonly, disabled, size, appearance, stopPropagation 등 모두 유효.
4. 칩(chip) 렌더링 규칙
_renderTagItems() 로직 (va_tag.js:848-870):
<div elname="field"> ← flex, wrap, gap:3px
<span class="va-tag-item" data-tag-key="js">
<span>JavaScript</span>
<span class="icon xsmall ico_dismiss va-tag-close"></span> ← readonly/disabled면 X 없음
</span>
<span class="va-tag-item" data-tag-key="ts">
<span>TypeScript</span>
<span class="icon xsmall ico_dismiss va-tag-close"></span>
</span>
...
</div>
표시 텍스트는 displayType이 결정:
- 'display' (기본) → TypeScript
- 'key' → ts
- 'both' → ts: TypeScript
X 아이콘 클릭 시:
- 해당 항목을 _tagSelectedItems에서 제거
- 팝업 데이터 재구성 (해당 항목이 팝업 리스트에 다시 나타남)
- 재렌더링
- select 이벤트 dispatch
⚠️ 칩 자체는 커스터마이즈 불가 — template 옵션은 팝업 리스트 아이템 렌더링만 담당하지 칩 UI는 하드코딩입니다.
5. 이벤트
Combobox와 동일한 세트:
이벤트시그니처발생 시점
| select | (component, element, listItem, data, key, display, evt) | 팝업에서 선택 시 또는 칩 X로 제거 시 모두 발생 |
| beforePop / afterPop | 팝업 표시 | |
| expand / collapse | 팝업 확장 | |
| hidePop / pop | 팝업 관련 | |
| focus / blur | 표준 | |
| click / keydown / keyup | 표준 | |
| listItemClick / listItemContextmenu | 리스트 아이템 클릭 |
⚠️ X 클릭 시 dispatch되는 select 이벤트는 인자가 짧음 — dispatchEvent("select", this, this.element)만 호출 (va_tag.js:845). 팝업 선택 시와 인자 개수가 달라 콜백에서 방어 로직 필요.
6. 메서드
Combobox와 거의 동일하지만 값 처리 방식이 다릅니다.
값 관리
메서드설명
| getValue() | 선택된 키 배열 반환. getValueType: 'string'이면 "'a','b','c'" 형태 |
| setValue(value, ignoreBindParams) | 배열 또는 단일 값. null/"" 넘기면 전체 초기화 |
| getSelectedData() | 선택된 전체 데이터 객체 배열 반환 (_tagSelectedItems 반환) |
| getDisplay() / getDisplayAsText() | 선택된 항목의 표시 텍스트 |
| getValueByDataManager() | 내부용 — 현재 선택 상태로 value 재계산 |
데이터 CRUD (Combobox와 동일)
메서드설명
| setData(data) | 데이터 교체 (_tagSelectedItems 초기화됨) |
| getData() | 팝업 리스트 데이터 반환 (선택된 것은 제외됨) |
| addData / insertData / modifyData / removeData / moveData | 표준 CRUD |
| selectData(data) | 프로그램적으로 선택 |
| selectFirstData(eventOccur) | 첫 항목 선택 |
팝업 / 상태
메서드설명
| showPop() / hidePop() | 팝업 제어 |
| setDisabled(bool) / setReadOnly(bool) | 상태 |
| focus() / blur() | 포커스 |
7. 내부 데이터 관리 — 이중 배열 구조
Tag는 두 개의 데이터 배열을 유지합니다:
this._tagOriginalData = [...전체 원본 데이터...] // 불변
this._tagSelectedItems = [...선택된 것들...] // 칩으로 표시
this.data = [...팝업에 표시할 것들...] // 원본에서 선택된 것을 뺀 것
동작 흐름:
- setData(data) → 원본을 _tagOriginalData에 저장, data에 복사
- 항목 선택 → _tagSelectedItems에 추가, _rebuildPopupData()로 팝업에서 제거
- 칩 X 클릭 → _tagSelectedItems에서 제거, _rebuildPopupData()로 팝업에 복귀
- _renderTagItems()로 칩 재렌더링
결과: 팝업 리스트에는 아직 선택 안 된 것만 보이고, 필드 안 칩은 선택된 것만 보임 — 중복 선택 방지 UX.
8. 내부 구조
<div elname="element" class="va-input [size]..." tag-name="tag" field="true">
<div elname="fieldWrapper" class="field-wrapper">
<div elname="field" tabindex="-1"
style="display:flex; flex-wrap:wrap; align-items:center;
gap:3px; cursor:pointer; overflow:hidden; flex:1">
<span class="va-tag-item" data-tag-key="js">
<span>JavaScript</span>
<span class="icon xsmall ico_dismiss va-tag-close"></span>
</span>
<span class="va-tag-item" data-tag-key="ts">
<span>TypeScript</span>
<span class="icon xsmall ico_dismiss va-tag-close"></span>
</span>
</div>
<div class="focus-line"></div>
<div class="va-combobox-dropdown">▼</div>
</div>
<div elname="popDiv" class="menu-button-pop-div">
<div elname="pop" class="va-menu-pop" style="position:absolute; display:none">
<div elname="popInner" class="combobox-pop-div" style="overflow-y:auto">
<!-- 아직 선택 안 된 항목만 표시 -->
</div>
</div>
</div>
</div>
포인트:
- field가 <input>이 아니라 <div> — 칩들을 담기 위해
- field의 tabindex="-1" — 직접 편집 불가, 팝업으로만 조작
- flex-wrap으로 칩이 자연스럽게 여러 줄 배치
- 팝업 구조는 Combobox와 동일 (팝업 상태머신 참여)
9. Va.Tag vs Va.TagField vs 관련 컴포넌트
같은 파일에 라벨 포함 버전과 그리드 안 칩 컴포넌트들이 있습니다:
컴포넌트역할
| Va.Tag | 순수 태그 입력 (이 문서 대상) |
| Va.TagField | Tag + Field 래퍼 (라벨/검증/설명) |
| Va.Combobox (multiSelect) | 다중 선택이지만 텍스트 필드로 표시 |
| Va.Filterbox (multiSelect) | Combobox 다중 + 필터링 |
그리드 안 사용례 (ApiGridTag, ApiGridFlashTag 등):
- 그리드 셀에 여러 태그 표시
- 편집 모드로 진입 시 팝업 열림
- flashTag: 유저 정의 커스텀 태그 지원 형태
10. 언제 쓰나
Tag가 맞을 때
- 여러 개 선택 + 개별 삭제 UX가 필요한 필드
- 이메일 수신자, 초대자 목록, 참여자 지정
- 게시글 태그, 라벨 선택
- 필터 조건에 여러 값 지정 (개별 제거 가능)
- Gmail·GitHub 스타일 다중 선택 UI
다른 걸 쓸 때
- 콤마 나열 형태로 충분 → Va.Combobox + multiSelect: true
- 라벨과 함께 폼 필드로 → Va.TagField
- 타이핑으로 필터 필요 → Va.Filterbox (multiSelect)
- 사용자가 자유롭게 새 태그 생성해야 함 → 별도 커스텀 (Tag는 사전 정의된 데이터에서만 선택)
11. 흔한 조합 예시
// 표준 다중 태그
{
tagName: 'tag',
data: [
{ key: 'urgent', display: '긴급' },
{ key: 'bug', display: '버그' },
{ key: 'feature', display: '기능' },
{ key: 'docs', display: '문서' }
],
value: ['urgent', 'bug']
}
// 서버 전송용 문자열 반환
{
tagName: 'tag',
data: [...],
getValueType: 'string' // getValue() → "'a','b','c'" 형태
}
// 팝업 항목에 아이콘 (템플릿 커스텀)
{
tagName: 'tag',
template: {
tagName: 'listItem',
layout: 'ds-flex fd-row ai-center gap-s',
tags: [
{ tagName: 'i', innerHTML: '{icon}' },
{ tagName: 'span', innerHTML: '{display}' }
]
},
data: [
{ key: 'js', display: 'JavaScript', icon: '🟨' },
{ key: 'ts', display: 'TypeScript', icon: '🟦' }
]
}
// 읽기 전용 (X 버튼 안 나옴)
{
tagName: 'tag',
readonly: true,
data: [...],
value: ['a', 'b']
}
// key: display 둘 다 칩 안에 표시
{
tagName: 'tag',
displayType: 'both', // 칩이 "js: JavaScript" 형태로
data: [...]
}
12. 알아두면 좋을 주의사항
- va_tag.js import 필수 — 사용 전 명시적 import 없으면 tagName:'tag' 인식 못 함.
- getValue() 반환은 배열 또는 문자열 — getValueType: 'string'이면 "'a','b','c'" 형태의 SQL IN 절 스타일 문자열. 기본은 배열.
- select 이벤트 인자 개수가 상황따라 다름 — 팝업 선택 시 7개, X 클릭 시 3개. 콜백에서 인자 방어 필요.
- 자유 태그 생성 안 됨 — Combobox처럼 사전 정의된 data에서만 선택. 사용자가 새 태그를 타이핑으로 만드는 기능은 없음.
- 필드 자체는 편집 불가 — tabindex="-1". 텍스트 타이핑도 안 됨. 팝업으로만 조작.
- 칩 UI는 커스터마이즈 불가 — 하드코딩. template은 팝업 항목만 담당.
- _tagOriginalData는 setData 때만 갱신 — 이후 원본 배열 수정해도 반영 안 됨. 재-setData() 필요.
- setValue(null) / setValue('')로 전체 초기화 — 명시적 리셋.
- 가상 스크롤 자동 — Combobox와 동일 구조로 대량 데이터도 부드러움.
- 팝업 상태머신 참여 — 다른 팝업과 자동 상호 배타.
- 칩 순서 — 사용자가 선택한 순서대로 나열 (원본 데이터 순서 아님).
- 칩 X 클릭 시 팝업이 자동으로 안 열림 — 항목이 다시 팝업 데이터로 복귀할 뿐, 팝업 자체는 그대로.
- 필드 폭보다 칩이 많으면 wrap — flex-wrap으로 여러 줄이 됨. 폭 제한이 있으면 미리 계산 필요.
- selectedData 반환은 참조 — getSelectedData() 반환 배열을 직접 수정하면 내부 상태 오염 가능. 필요하면 복사 후 사용.
'컴포넌트 > 필드 컴포넌트' 카테고리의 다른 글
| DateField (날짜필드) (0) | 2026.09.10 |
|---|---|
| Filterbox (필터박스) (0) | 2026.09.10 |
| ComboboxField (콤보박스필드) (0) | 2026.09.10 |
| ColorField (색상필드) (0) | 2026.09.10 |
| ColorPicker(색상선택) (0) | 2026.09.10 |