Virtualized Select (가상화 셀렉트)

Virtualized Select (가상화 셀렉트)

수만 행의 데이터를 렌더링해야 할 때 브라우저 부담을 줄여 성능 문제를 해결하는 가상화 셀렉트 컴포넌트예요.

출처: 문서

본문

TIP 이 컴포넌트는 아직 테스트 중이에요. 버그나 문제를 발견하면 GitHub에 보고해서 수정할 수 있게 해주세요.

배경 (Background)

어떤 사용 사례에서는 단일 셀렉터가 수만 행의 데이터를 로드하게 될 수 있어요. 그 많은 데이터를 DOM에 렌더링하는 것은 브라우저에 부담이 되어 성능 문제를 일으킬 수 있어요. 더 나은 사용자와 개발자 경험을 위해 이 컴포넌트를 추가하기로 했어요.

기본 사용법 (Basic usage)

가장 단순한 셀렉터예요.

다중 선택 (Multi select)

태그가 있는 기본 다중 선택 셀렉터예요.

크기 (Sizes)

size 속성을 추가해서 Select-V2의 크기를 바꿔요. 기본 크기 외에 두 가지 옵션이 더 있어요: large, small.

import { ref } from 'vue'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

const value = ref()
const options = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`,
}))

.example-showcase .el-select-v2 {
  margin-right: 20px;
}

선택 항목이 너무 많을 때 추가 태그 숨기기 (Hide extra tags when the selected items are too many)

collapse-tags 속성을 사용해서 태그를 텍스트로 접을 수 있어요. collapse-tags-tooltip 속성으로 collapse 텍스트 위에 마우스를 올렸을 때 확인할 수 있어요.

import { ref } from 'vue'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

const value = ref([])
const value2 = ref([])
const value3 = ref([])
const options = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`,
}))

필터링 가능한 다중 선택 (Filterable multi-select)

옵션이 압도적으로 많을 때 filterable 옵션을 사용해서 원하는 옵션을 찾는 필터 기능을 활성화할 수 있어요.

셀렉터와 옵션 비활성화 (Disabled selector and select options)

셀렉터 자체나 옵션을 비활성화하도록 선택할 수 있어요.

옵션 그룹화 (Option Grouping)

데이터가 패턴을 충족하기만 하면 원하는 대로 옵션을 그룹화할 수 있어요.

지우기 가능한 셀렉터 (Clearable selector)

선택된 모든 옵션을 한 번에 지울 수 있고, 단일 선택에도 적용돼요.

커스텀 옵션 렌더러 (Customized option renderer)

팝업에서 옵션을 렌더링하는 자신만의 템플릿을 정의할 수 있어요.

드롭다운 헤더 (Header of the dropdown) 2.5.2

드롭다운의 헤더를 커스터마이징할 수 있어요. 슬롯으로 내용을 커스터마이징해요.

드롭다운의 푸터를 커스터마이징할 수 있어요. 슬롯으로 내용을 커스터마이징해요.

import { nextTick, ref } from 'vue'

import type { CheckboxValueType, SelectV2Instance } from 'element-plus'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
const select = ref()
const isAdding = ref(false)
const value = ref([])
const optionName = ref('')
const options = ref(
  Array.from({ length: 1000 }).map((_, idx) => ({
    value: `Option ${idx + 1}`,
    label: `${initials[idx % 10]}${idx}`,
  }))
)

const onAddOption = () => {
  isAdding.value = true
}

const onConfirm = () => {
  if (optionName.value) {
    options.value.push({
      label: optionName.value,
      value: optionName.value,
    })
    clear()
    nextTick(() => {
      select.value?.scrollTo(options.value.length - 1)
    })
  }
}

const clear = () => {
  optionName.value = ''
  isAdding.value = false
}

.select-footer {
  display: flex;
  flex-direction: column;

  .option-input {
    width: 100%;
    margin-bottom: 8px;
  }
}

옵션 만들기 (Create Option)

select 옵션에 없는 새 항목을 만들고 선택해요. allow-create 속성을 사용하면 사용자가 입력 박스에 입력해서 새 항목을 만들 수 있어요. allow-create가 동작하려면 filterable이 true여야 한다는 점에 유의하세요. 이 예제는 default-first-option도 보여줘요. 이 속성이 true로 설정되면 마우스나 화살표 키로 이동할 필요 없이 Enter 키를 눌러 현재 옵션 목록의 첫 번째 옵션을 선택할 수 있어요.

TIP allow-create를 사용할 때 :reserve-keyword="false"로 설정하는 게 좋아요.

import { ref } from 'vue'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

const value1 = ref([])
const value2 = ref()
const value3 = ref([])
const options = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`,
}))

키워드를 입력하고 서버에서 데이터를 검색해요. filterable과 remote의 값을 true로 설정해서 원격 검색을 활성화하고, remote-method를 전달해야 해요. remote-method는 입력 값이 변경될 때 호출되는 Function이고, 매개변수는 현재 입력 값이에요.

import { ref } from 'vue'

const states = [
  'Alabama',
  'Alaska',
  'Arizona',
  'Arkansas',
  'California',
  'Colorado',
  'Connecticut',
  'Delaware',
  'Florida',
  'Georgia',
  'Hawaii',
  'Idaho',
  'Illinois',
  'Indiana',
  'Iowa',
  'Kansas',
  'Kentucky',
  'Louisiana',
  'Maine',
  'Maryland',
  'Massachusetts',
  'Michigan',
  'Minnesota',
  'Mississippi',
  'Missouri',
  'Montana',
  'Nebraska',
  'Nevada',
  'New Hampshire',
  'New Jersey',
  'New Mexico',
  'New York',
  'North Carolina',
  'North Dakota',
  'Ohio',
  'Oklahoma',
  'Oregon',
  'Pennsylvania',
  'Rhode Island',
  'South Carolina',
  'South Dakota',
  'Tennessee',
  'Texas',
  'Utah',
  'Vermont',
  'Virginia',
  'Washington',
  'West Virginia',
  'Wisconsin',
  'Wyoming',
]
const list = states.map((item): ListItem => {
  return { value: `value:${item}`, label: `label:${item}` }
})

interface ListItem {
  value: string
  label: string
}

const value = ref([])
const options = ref([])
const loading = ref(false)

const remoteMethod = (query: string) => {
  if (query !== '') {
    loading.value = true
    setTimeout(() => {
      loading.value = false
      options.value = list.filter((item) => {
        return item.label.toLowerCase().includes(query.toLowerCase())
      })
    }, 200)
  } else {
    options.value = []
  }
}

value-key 속성 사용 (Use value-key attribute)

options.value가 객체일 때 value의 고유 식별 키 이름을 설정해야 해요.

TIP 2.4.0 이전에는 value-key가 선택된 객체의 고유 값이자 options의 value 별칭으로 모두 사용됐어요. 이제 value-key는 선택된 객체의 고유 값으로만 사용되고, options의 value 별칭은 props.value예요.

커스텀 옵션 별칭 (Aliases for custom options) 2.4.2

옵션 형식이 기본 형식과 다르면 props 속성으로 옵션의 별칭을 커스터마이징할 수 있어요.

커스텀 태그 (Custom Tag) 2.5.0

태그를 커스터마이징할 수 있어요. el-select의 슬롯에 커스텀 태그를 넣어요. collapse-tags, collapse-tags-tooltip, max-collapse-tags는 동작하지 않아요.

import { ref } from 'vue'

const value = ref([])
const colors = [
  {
    value: '#E63415',
    label: 'red',
  },
  {
    value: '#FF6600',
    label: 'orange',
  },
  {
    value: '#FFDE0A',
    label: 'yellow',
  },
  {
    value: '#1EC79D',
    label: 'green',
  },
  {
    value: '#14CCCC',
    label: 'cyan',
  },
  {
    value: '#4167F0',
    label: 'blue',
  },
  {
    value: '#6222C9',
    label: 'purple',
  },
]
colors.forEach((color) => {
  value.value.push(color.value)
})

.el-tag {
  border: none;
  aspect-ratio: 1;
}

커스텀 로딩 (Custom Loading) 2.5.2

로딩 내용을 재정의해요.

import { onMounted, ref } from 'vue'

interface ListItem {
  value: string
  label: string
}

const list = ref([])
const options = ref([])
const value = ref([])
const loading = ref(false)

onMounted(() => {
  list.value = states.map((item) => {
    return { value: `value:${item}`, label: `label:${item}` }
  })
})

const remoteMethod = (query: string) => {
  if (query) {
    loading.value = true
    setTimeout(() => {
      loading.value = false
      options.value = list.value.filter((item) => {
        return item.label.toLowerCase().includes(query.toLowerCase())
      })
    }, 3000)
  } else {
    options.value = []
  }
}

const states = [
  'Alabama',
  'Alaska',
  'Arizona',
  'Arkansas',
  'California',
  'Colorado',
  'Connecticut',
  'Delaware',
  'Florida',
  'Georgia',
  'Hawaii',
  'Idaho',
  'Illinois',
  'Indiana',
  'Iowa',
  'Kansas',
  'Kentucky',
  'Louisiana',
  'Maine',
  'Maryland',
  'Massachusetts',
  'Michigan',
  'Minnesota',
  'Mississippi',
  'Missouri',
  'Montana',
  'Nebraska',
  'Nevada',
  'New Hampshire',
  'New Jersey',
  'New Mexico',
  'New York',
  'North Carolina',
  'North Dakota',
  'Ohio',
  'Oklahoma',
  'Oregon',
  'Pennsylvania',
  'Rhode Island',
  'South Carolina',
  'South Dakota',
  'Tennessee',
  'Texas',
  'Utah',
  'Vermont',
  'Virginia',
  'Washington',
  'West Virginia',
  'Wisconsin',
  'Wyoming',
]

.el-select-dropdown__loading {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100px;
  font-size: 20px;
}

.circular {
  display: inline;
  height: 30px;
  width: 30px;
  animation: loading-rotate 2s linear infinite;
}
.path {
  animation: loading-dash 1.5s ease-in-out infinite;
  stroke-dasharray: 90, 150;
  stroke-dashoffset: 0;
  stroke-width: 2;
  stroke: var(--el-color-primary);
  stroke-linecap: round;
}
.loading-path .dot1 {
  transform: translate(3.75px, 3.75px);
  fill: var(--el-color-primary);
  animation: custom-spin-move 1s infinite linear alternate;
  opacity: 0.3;
}
.loading-path .dot2 {
  transform: translate(calc(100% - 3.75px), 3.75px);
  fill: var(--el-color-primary);
  animation: custom-spin-move 1s infinite linear alternate;
  opacity: 0.3;
  animation-delay: 0.4s;
}
.loading-path .dot3 {
  transform: translate(3.75px, calc(100% - 3.75px));
  fill: var(--el-color-primary);
  animation: custom-spin-move 1s infinite linear alternate;
  opacity: 0.3;
  animation-delay: 1.2s;
}
.loading-path .dot4 {
  transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
  fill: var(--el-color-primary);
  animation: custom-spin-move 1s infinite linear alternate;
  opacity: 0.3;
  animation-delay: 0.8s;
}
@keyframes loading-rotate {
  to {
    transform: rotate(360deg);
  }
}
@keyframes loading-dash {
  0% {
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -40px;
  }
  100% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -120px;
  }
}
@keyframes custom-spin-move {
  to {
    opacity: 1;
  }
}

빈 값 (Empty Values) 2.7.0

빈 문자열을 지원하고 싶다면 empty-values를 [null, undefined]로 설정해요. 지운 값을 null로 바꾸고 싶다면 value-on-clear를 null로 설정해요.

커스텀 라벨 (Custom Label) 2.7.4

라벨을 커스터마이징할 수 있어요.

import { ref } from 'vue'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
const value1 = ref('Option 1')
const value2 = ref(['Option 1'])
const options = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}`,
}))

커스텀 너비 (Custom Width) 2.9.2

드롭다운 박스의 너비는 기본적으로 label 값을 기준으로 계산돼요. default 슬롯으로 드롭다운 박스 옵션을 커스터마이징하면 옵션에 표시되는 텍스트가 label 값과 같지 않아 계산 오류가 날 수 있어요. 이 경우 fit-input-width 속성을 숫자로 설정해서 너비를 고정할 수 있어요.

import { ref } from 'vue'

const initials = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

const value = ref()
const options = Array.from({ length: 1000 }).map((_, idx) => ({
  value: `Option ${idx + 1}`,
  label: `${initials[idx % 10]}${idx}${'-'.repeat(Math.ceil(idx / 25))}`,
}))

API

Attributes

Name Description Type Default
model-value / v-model 바인딩 값 string / number / boolean / object / array —
options 옵션의 데이터, value와 label의 키는 props로 커스터마이징할 수 있어요 array —
props 2.4.2 설정 옵션, 아래 표 참고 object —
multiple 다중 선택 여부 boolean false
disabled 비활성화 여부 boolean false
value-key value의 고유 식별 키 이름, value가 객체일 때 필수 string value
size 컴포넌트의 크기 enum ''
clearable select를 지울 수 있는지 여부 boolean false
clear-icon 커스텀 지우기 아이콘 string / object CircleClose
collapse-tags 다중 선택 시 태그를 텍스트로 접을지 여부 boolean false
multiple-limit multiple이 true일 때 사용자가 선택할 수 있는 최대 옵션 수. 0이면 제한 없음 number 0
id 네이티브 input id string —
name select input의 name 속성 string —
effect tooltip 테마, 내장 테마: dark / light enum / string light
autocomplete select input의 autocomplete string off
placeholder placeholder string Please select
filterable Select 필터링 가능 여부 boolean false
allow-create 새 항목 생성 허용 여부. 사용하려면 filterable이 true여야 해요 boolean false
filter-method 커스텀 필터 메서드, 첫 번째 매개변수는 현재 입력 값. 사용하려면 filterable이 true여야 해요 Function —
loading Select가 서버에서 데이터를 로드하는지 여부 boolean false
loading-text 서버에서 데이터를 로드하는 동안 표시되는 텍스트, 기본은 'Loading' string —
reserve-keyword 필터된 옵션 선택 후 키워드 유지 여부 boolean true
default-first-option Enter 키로 첫 번째 일치 옵션 선택. filterable 또는 remote와 함께 사용 boolean false
no-match-text 필터링 쿼리와 일치하는 데이터가 없을 때 표시되는 텍스트, empty 슬롯도 사용 가능, 기본은 'No matching data' string —
no-data-text 옵션이 없을 때 표시되는 텍스트, empty 슬롯도 사용 가능 string No Data
popper-class Select 드롭다운과 태그 tooltip의 커스텀 클래스 이름 string / object ''
popper-style 2.11.0 Select 드롭다운과 태그 tooltip의 커스텀 스타일 string / object —
teleported select 드롭다운 텔레포트 여부, true면 append-to가 설정한 곳으로 텔레포트돼요 boolean true
append-to 2.8.8 select 드롭다운이 붙는 요소 CSSSelector / HTMLElement —
persistent select 드롭다운이 비활성이고 persistent가 false일 때 드롭다운이 파괴됨 boolean true
popper-options popper.js 매개변수 object (popper.js 문서 참고) {}
automatic-dropdown 필터링 불가 Select에서 입력 포커스 시 옵션 메뉴가 팝업될지 여부 boolean false
fit-input-width 2.9.2 드롭다운 너비가 입력과 같은지 여부, 숫자면 너비가 고정돼요 boolean / number true
suffix-icon 2.9.8 커스텀 접미 아이콘 컴포넌트 string / object ArrowDown
height 드롭다운 패널의 높이, 각 항목 34px number 274
item-height 드롭다운 항목의 높이 number 34
estimated-option-height 가상 목록 크기 모드 제어: undefined면 item-height의 고정 항목 높이 사용, 제공하면 동적 항목 크기와 이 값을 추정 항목 높이로 사용 number —
scrollbar-always-on 스크롤바를 항상 표시할지 여부 제어 boolean false
remote 서버에서 데이터를 검색할지 여부 boolean false
debounce 2.11.7 원격 검색 중 디바운스 지연(밀리초) number 300
remote-method 입력 값이 변경될 때 호출되는 함수. 매개변수는 현재 입력 값. 사용하려면 filterable이 true여야 해요 Function —
remote-show-suffix 2.11.9 원격 검색 방식에서 접미 아이콘 표시 boolean false
validate-event 폼 검증 트리거 여부 boolean true
offset 2.8.8 드롭다운의 오프셋 number 12
show-arrow 2.8.8 드롭다운에 화살표가 있는지 여부 boolean true
placement 드롭다운 위치 enum bottom-start
fallback-placements 2.5.6 드롭다운 popper.js의 가능한 위치 목록 array ['bottom-start', 'top-start', 'right', 'left']
collapse-tags-tooltip 2.3.0 collapse-tags 텍스트 위에 마우스를 올렸을 때 모든 선택 태그를 표시할지 여부. 사용하려면 collapse-tags가 true여야 해요 boolean false
tag-tooltip 2.13.3 collapse-tags tooltip의 설정 객체. 사용하려면 collapse-tags와 collapse-tags-tooltip이 true여야 해요 object {}
max-collapse-tags 2.3.0 표시할 최대 태그 수. 사용하려면 collapse-tags가 true여야 해요 number 1
tag-type 2.5.0 태그 타입 enum info
tag-effect 2.7.7 태그 효과 enum light
aria-label a11y 2.5.0 네이티브 input의 aria-label과 동일 string —
empty-values 2.7.0 컴포넌트의 빈 값, config-provider 참고 array —
value-on-clear 2.7.0 지우기 반환 값, config-provider 참고 string / number / boolean / Function —
popper-append-to-body deprecated popper 메뉴를 body에 붙일지 여부. popper 위치가 잘못되면 이 prop을 false로 설정해 볼 수 있어요 boolean false
tabindex 2.9.0 input의 tabindex string / number —

props

Attribute Description Type Default
value 노드 객체의 어떤 키를 노드의 value로 사용할지 지정 string value
label 노드 객체의 어떤 키를 노드의 label로 사용할지 지정 string label
options 노드 객체의 어떤 키를 노드의 children으로 사용할지 지정 string options
disabled 노드 객체의 어떤 키를 노드의 disabled로 사용할지 지정 string disabled

tag-tooltip 2.13.3

대체 메커니즘 (Fallback Mechanism) tag-tooltip의 속성은 다음 우선순위를 따르는 데요:

  • tag-tooltip 객체 내부에 명시적으로 정의된 필드.
  • el-select-v2에서 상속된 공유 props (예: effect, popper-class, popper-style, teleported, append-to, popper-options).
  • 기본 el-tooltip 컴포넌트의 기본값. 이렇게 하면 Select 드롭다운과 기본적으로 일관성을 유지하면서 태그의 특정 tooltip 동작을 재정의할 수 있어요.

사용자 지정 컨테이너 배치 (Custom Container Positioning) Tooltip을 커스텀 컨테이너에 추가할 때(append-to 속성) 정확한 배치를 위해 컨테이너에 position: relative 또는 position: absolute를 설정해야 해요. 또한 Tooltip이 경계를 넘는 것을 막아야 한다면 컨테이너에 overflow: hidden을 적용할 수 있어요.

Attribute Description Type Default
append-to tooltip CONTENT가 붙는 요소 CSSSelector / HTMLElement —
placement Tooltip의 위치 enum bottom
fallback-placements Tooltip popper.js의 가능한 위치 목록 array ['bottom', 'top', 'right', 'left']
effect Tooltip 테마, 내장 테마: dark / light enum / string —
popper-class Tooltip popper의 커스텀 클래스 이름 string —
popper-style Tooltip popper의 커스텀 스타일 string / object —
transition 애니메이션 이름 string —
teleported tooltip 내용 텔레포트 여부, true면 append-to가 설정한 곳으로 텔레포트돼요 boolean —
popper-options popper.js 매개변수 object (popper.js 문서 참고) —
show-after 나타나는 지연 시간(밀리초) number —
hide-after 사라지는 지연 시간(밀리초) number —
auto-close tooltip을 숨기기까지의 타임아웃(밀리초) number —
offset Tooltip의 오프셋 number —

Events

Name Description Type
change 선택된 값이 변경될 때 트리거, 매개변수는 현재 선택 값 Function
visible-change 드롭다운이 나타나거나 사라질 때 트리거, 나타나면 true, 아니면 false Function
remove-tag 다중 모드에서 태그가 제거될 때 트리거, 매개변수는 제거된 태그 값 Function
clear clearable Select에서 지우기 아이콘이 클릭될 때 트리거 Function
blur Input이 블러될 때 트리거 Function
focus Input이 포커스될 때 트리거 Function
end-reached 2.14.0 드롭다운 스크롤이 끝에 도달할 때 트리거 Function

Slots

Name Description Type
default 옵션 렌더러 —
header 2.5.2 드롭다운 상단의 내용 —
footer 2.5.2 드롭다운 하단의 내용 —
empty 옵션이 비어 있을 때의 내용 —
prefix input의 접두사 내용 —
tag 2.5.0 Select 태그로서의 내용, subTags 데이터, selectDisabled, deleteTag는 2.10.3에서 도입 object
loading 2.5.2 Select 로딩으로서의 내용 —
label 2.7.4 Select 라벨로서의 내용, index는 2.11.2에서 도입 object

Exposes

Name Description Type
focus Input 컴포넌트 포커스 Function
blur Input 컴포넌트 블러, 드롭다운 숨김 Function
selectedLabel 2.8.5 현재 선택된 라벨 가져오기 object

더 알아보기 (Learn more)