VanillaFront 2026. 9. 10. 15:21

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 아이콘 클릭 시:

  1. 해당 항목을 _tagSelectedItems에서 제거
  2. 팝업 데이터 재구성 (해당 항목이 팝업 리스트에 다시 나타남)
  3. 재렌더링
  4. 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 = [...팝업에 표시할 것들...]                 // 원본에서 선택된 것을 뺀 것

동작 흐름:

  1. setData(data) → 원본을 _tagOriginalData에 저장, data에 복사
  2. 항목 선택 → _tagSelectedItems에 추가, _rebuildPopupData()로 팝업에서 제거
  3. 칩 X 클릭 → _tagSelectedItems에서 제거, _rebuildPopupData()로 팝업에 복귀
  4. _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. 알아두면 좋을 주의사항

  1. va_tag.js import 필수 — 사용 전 명시적 import 없으면 tagName:'tag' 인식 못 함.
  2. getValue() 반환은 배열 또는 문자열  getValueType: 'string'이면 "'a','b','c'" 형태의 SQL IN 절 스타일 문자열. 기본은 배열.
  3. select 이벤트 인자 개수가 상황따라 다름 — 팝업 선택 시 7개, X 클릭 시 3개. 콜백에서 인자 방어 필요.
  4. 자유 태그 생성 안 됨 — Combobox처럼 사전 정의된 data에서만 선택. 사용자가 새 태그를 타이핑으로 만드는 기능은 없음.
  5. 필드 자체는 편집 불가  tabindex="-1". 텍스트 타이핑도 안 됨. 팝업으로만 조작.
  6. 칩 UI는 커스터마이즈 불가 — 하드코딩. template은 팝업 항목만 담당.
  7. _tagOriginalData는 setData 때만 갱신 — 이후 원본 배열 수정해도 반영 안 됨. 재-setData() 필요.
  8. setValue(null) / setValue('')로 전체 초기화 — 명시적 리셋.
  9. 가상 스크롤 자동 — Combobox와 동일 구조로 대량 데이터도 부드러움.
  10. 팝업 상태머신 참여 — 다른 팝업과 자동 상호 배타.
  11. 칩 순서 — 사용자가 선택한 순서대로 나열 (원본 데이터 순서 아님).
  12. 칩 X 클릭 시 팝업이 자동으로 안 열림 — 항목이 다시 팝업 데이터로 복귀할 뿐, 팝업 자체는 그대로.
  13. 필드 폭보다 칩이 많으면 wrap — flex-wrap으로 여러 줄이 됨. 폭 제한이 있으면 미리 계산 필요.
  14. selectedData 반환은 참조  getSelectedData() 반환 배열을 직접 수정하면 내부 상태 오염 가능. 필요하면 복사 후 사용.