컴포넌트/필드 컴포넌트

DoubleSlider (더블슬라이더)

VanillaFront 2026. 9. 13. 23:38

Va.DoubleSlider — 두 개의 thumb으로 범위(min-max)를 선택하는 슬라이더

Va.Slider가 단일 값을 조절한다면, Va.DoubleSlider는 두 개의 thumb으로 값 범위를 지정하는 컴포넌트. 낮은 값 thumb + 높은 값 thumb이 있고, 두 thumb 사이가 "선택된 범위"로 강조 표시됩니다. 가격 범위 필터·나이 범위·시간대 범위 같은 UI에 정확히 어울려요.

  • 클래스: Va.DoubleSlider  va_slider.js:322
  • short name: doubleSlider
  • 상속: Va.Div (PureField 아님!)
  • 파일: va_slider.js (별도 파일 — import 필요)
  • isContainer: true


1. 시작하기 전 — 별도 파일 import

import Va from '../../lib/va.js';
import '../../lib/va_slider.js';       // ← Slider 계열 모두 이 파일에

2. 기본 사용

{
    tagName: 'doubleSlider',
    lowValue: 30,
    highValue: 70,
    min: 0,
    max: 100,
    step: 1
}
  • 두 개의 thumb — 낮은 값(30), 높은 값(70)
  • 두 thumb 사이 범위 강조 표시
  • 각 thumb 위에 값 텍스트 표시

3. Va.Slider / Va.DoubleSlider의 차이

항목Va.SliderVa.DoubleSlider

thumb 개수 1개 2개 (낮은 값 + 높은 값)
선택 대상 단일 값 범위 (from-to)
값 옵션 value lowValue + highValue
getValue() 단일 값 getMinValue() / getMaxValue() 분리
상속 PureField Div (필드 아님)
숫자 입력창 병기
접근성 role slider 별도 세팅 없음

한 줄 요약: "범위 필터를 슬라이더 UI로 표현하는 컴포넌트."


4. 주요 속성

범위 값

속성기본값설명

lowValue 30 낮은 값 thumb의 초기 위치
highValue 70 높은 값 thumb의 초기 위치
min 최솟값 (전체 rail의 시작)
max 최댓값 (전체 rail의 끝)
step 증감 단위

표시 옵션

속성설명

visibleCurrentValue 숫자 입력창 표시 (실질 활용 제한적)
visibleMinMax min/max 라벨 표시
thumbText thumb 위 값 텍스트 표시
inputWidth 숫자 입력창 폭

색상 커스터마이즈

속성설명

progressColor 선택된 범위 색상
railColor 빈 rail 색상

5. 배타 로직 — 두 thumb이 서로 넘지 못하도록

DoubleSlider의 내부에서 자동 처리하는 규칙:

// minRange가 maxRange를 넘어가려 하면 → maxRange 값으로 되돌림
if (Number(minRangeElement.value) > Number(maxRangeElement.value)) {
    lowValue = highValue;
    minRangeElement.value = maxRangeElement.value;
}

// maxRange가 minRange 아래로 내려가려 하면 → minRange 값으로 되돌림
if (Number(maxRangeElement.value) < Number(minRangeElement.value)) {
    highValue = lowValue;
    maxRangeElement.value = minRangeElement.value;
}

결과: 사용자가 실수로 낮은 값 thumb을 높은 값 thumb보다 오른쪽으로 드래그해도 서로 밀리지 않고 자동 맞닿음.


6. 이벤트

DoubleSlider는 자체 이벤트가 매우 적습니다. this.events에 명시 추가된 게 없고, 내부적으로 input 이벤트에서 update()만 호출.

실질적으로 개발자가 감지할 이벤트가 부족한 편입니다. 필요하면 component.minRangeElement, component.maxRangeElement에 직접 리스너를 붙여야 합니다:

mounted() {
    const ds = this.getRef('range');
    ds.minRangeElement.addEventListener('input', (evt) => {
        console.log('low:', ds.lowValue);
    });
    ds.maxRangeElement.addEventListener('input', (evt) => {
        console.log('high:', ds.highValue);
    });
}

⚠️ 표준 change/input 이벤트를 dispatch하는 코드가 명확히 없어, 실제 프로젝트에선 이 부분 확인 필수. 데모에서 실동작 검증 권장.


7. 메서드

값 조회·세팅

메서드설명

setMinValue(value) 낮은 값 thumb 위치 세팅
setMaxValue(value) 높은 값 thumb 위치 세팅
getMinValue() 낮은 값 반환
getMaxValue() 높은 값 반환

⚠️ 주의: 소스상 getMinValue() / getMaxValue()는 this.minValue / this.maxValue를 반환하는데, 이 필드는 setMinValue/setMaxValue 호출 시에만 세팅됩니다. 사용자가 thumb을 드래그하면 실제로는 this.lowValue/this.highValue가 갱신되니, 드래그 후 값을 조회하려면 component.lowValue / component.highValue에 직접 접근하는 게 정확합니다.

그 외

메서드설명

focus() / blur() 빈 구현 (동작 안 함)

8. 내부 구조

<div elname="element" class="va-double-slider" ...>
  <div elname="inner" class="double-slider-inner">
    <span elname="minValue" class="text">0</span>           ← min 라벨
    <div elname="sliderDiv" class="slider-div">
      <div elname="track" class="track"></div>              ← 전체 rail
      <div elname="range" class="range"                     ← 선택된 범위 강조
           style="left: 30%; width: 40%"></div>
      <input elname="minRange" type="range" class="min-range"> ← 낮은 값 slider
      <input elname="maxRange" type="range" class="max-range"> ← 높은 값 slider
      <div elname="highValueText" class="thumb">70</div>    ← 높은 값 텍스트
      <div elname="lowValueText" class="thumb">30</div>     ← 낮은 값 텍스트
    </div>
    <span elname="maxValue" class="text">100</span>         ← max 라벨
  </div>
</div>

핵심 트릭:

  • 두 개의 <input type="range">가 겹쳐 있음 — 각각 낮은 값과 높은 값을 담당
  • range div가 선택된 범위 강조 — 두 thumb 사이의 배경 채움
  • 값 텍스트 위치는 픽셀 단위로 계산  draw()에서 슬라이더 폭과 텍스트 폭 기준으로 픽셀 계산

9. mounted()의 500ms 지연 렌더링

mounted()에서 _setTimer('render', () => this.draw(), 500)으로 500ms 후 draw() 재실행합니다 (va_slider.js:421-423).

이유: 두 thumb의 텍스트 위치를 정확히 계산하려면 슬라이더 폭(offsetWidth)이 확정되어야 하는데, 초기 렌더 시엔 아직 레이아웃이 안 잡혀 있을 수 있음. 500ms 대기 후 정확한 위치로 다시 그림.

⚠️ 초기 렌더링이 살짝 튀어 보일 수 있음 — 처음엔 잘못된 위치, 500ms 후 정확한 위치. UX상 티가 크게 나진 않지만 인지하고 있어야 함.


10. 언제 쓰나

DoubleSlider가 맞을 때

  • 가격 범위 필터 (5만원 ~ 20만원)
  • 나이 범위 (20~35세)
  • 시간대 범위 (09:00~18:00 같은)
  • 날짜 범위 (숫자로 매핑 가능한 것)
  • 감각적 범위 선택 (정확한 값 두 개보다 대략적 범위가 더 자연스러운 상황)

다른 걸 쓸 때

  • 단일 값 조절 → Va.Slider
  • 정확한 두 숫자 입력 → Va.NumberField × 2
  • 날짜 범위 → Va.DateRangePicker (별도)
  • 시간 범위 → Va.TimeField × 2
  • 별점 → Va.StarSlider

11. 흔한 조합 예시

// 가격 범위 필터
{
    tagName: 'doubleSlider',
    lowValue: 50000,
    highValue: 200000,
    min: 0,
    max: 500000,
    step: 10000,
    progressColor: '#0f6cbd'
}

// 나이 범위
{
    tagName: 'doubleSlider',
    lowValue: 20,
    highValue: 35,
    min: 0,
    max: 100,
    step: 1
}

// 시간대 (0~24시)
{
    tagName: 'doubleSlider',
    lowValue: 9,
    highValue: 18,
    min: 0,
    max: 24,
    step: 1
}

// 커스텀 색상
{
    tagName: 'doubleSlider',
    lowValue: 30,
    highValue: 70,
    min: 0,
    max: 100,
    progressColor: 'var(--colorBrandStroke1)',
    railColor: '#e0e0e0'
}

12. 실전 예 — 가격 범위 상품 필터

class ProductFilter extends Va.View {
    mounted() {
        // DoubleSlider의 표준 이벤트가 부족하므로 직접 리스너
        const ds = this.getRef('price');
        const trigger = () => this.applyFilter();
        ds.minRangeElement.addEventListener('change', trigger);
        ds.maxRangeElement.addEventListener('change', trigger);
    }

    applyFilter() {
        const ds = this.getRef('price');
        const low  = Number(ds.lowValue);
        const high = Number(ds.highValue);

        this.getRef('priceLabel').setValue(
            `${low.toLocaleString()}원 ~ ${high.toLocaleString()}원`
        );

        ProductService.search(this, { minPrice: low, maxPrice: high },
            (view, ok, res) => {
                if (ok) view.getRef('grid').setData(res.data.list);
            }
        );
    }

    config() {
        return {
            tagName: 'page',
            tags: [{
                tagName: 'panel',
                tags: [
                    { tagName: 'h2', innerHTML: '가격 범위 필터' },
                    {
                        tagName: 'doubleSlider',
                        ref: 'price',
                        lowValue: 50000,
                        highValue: 200000,
                        min: 0,
                        max: 500000,
                        step: 10000
                    },
                    { tagName: 'div', ref: 'priceLabel', innerHTML: '' },
                    { tagName: 'grid', ref: 'grid', columns: [/* ... */] }
                ]
            }]
        };
    }
}

13. 알아두면 좋을 주의사항

  1. va_slider.js import 필수 — 없으면 인식 못 함.
  2. Va.Div 상속 — PureField/Field 계열 아님. getValue()/setValue() 관행 없음.
  3. 표준 이벤트 부족  change/input 재발화 코드 명확하지 않음. minRangeElement/maxRangeElement에 직접 리스너 부착 권장.
  4. getMinValue()/getMaxValue() 실사용 주의 — 사용자 드래그 후 값은 lowValue/highValue 필드에. getMinValue가 이 값을 반환하지 않을 수 있음. 실제 확인 필요.
  5. 초기 500ms 렌더링 지연 — thumb 텍스트 위치 계산 문제로. UX상 미세하게 튀어 보일 수 있음.
  6. focus()/blur()가 빈 구현 — 프로그램적 포커스 제어 안 됨.
  7. 두 thumb 배타 로직 자동 — 서로 넘어가지 않게 자동 맞닿음.
  8. 값은 문자열  <input type="range"> 특성. 계산 시 Number() 변환.
  9. 범위 강조 계산이 minVal / 100 * 100 하드코딩  va_slider.js:446-447. min/max가 0~100이 아닌 경우 강조 표시가 잘못될 수 있음. 확인 필수.
  10. 접근성  role="slider" 자동 부여 없음. 스크린리더 지원이 Slider보다 약함.
  11. 라벨 있는 폼 필드로 쓰려면 Va.DoubleSliderField — 별도 확인 필요.

14. doubleSlider vs 두 numberField vs slider × 2 선택

상황추천

범위 감각적 조절 (가격, 나이) doubleSlider
정확한 두 숫자 입력 numberField × 2
단일 값 감각 조절 slider
날짜 범위 dateRangePicker
시간대 범위 timeField × 2

"범위를 감각적으로 좁히는 UI가 필요하면 doubleSlider" — 명확한 사용처.


참고