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
드롭다운의 헤더를 커스터마이징할 수 있어요. 슬롯으로 내용을 커스터마이징해요.
드롭다운 푸터 (Footer 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}`,
}))
원격 검색 (Remote search)
키워드를 입력하고 서버에서 데이터를 검색해요. 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)
- Select 컴포넌트 — 선택
- Slider 컴포넌트 — 슬라이더
- Table 컴포넌트 — 테이블 (가상화)
- Element Plus 시작하기 — 프로젝트 설정