개발 기술/사소하지만 놓치기 쉬운 개발 지식

[JS] RangeSlider를 구현하는 법(feat. 중첩 range input)

by GicoMomg 2024. 7. 14.

1. 들어가기 전에…

범위 슬라이더(range slider)는 사용자가 특정 범위 내에서 최소, 최대값을 선택할 수 있는 UI 요소이다.
이번 포스팅에서는 두 개의 input을 이용해 범위 슬라이더를 구현하는 방법에 대해 알아보았다!
이 포스팅을 다 읽고 나면 아래와 같은 형태의 범위 슬라이드를 구현할 수 있다 🙂




2. 어떻게 구현할 수 있을까?

1) HTML 구조

💡 먼저, 범위 슬라이더를 구성할 기본 HTML 구조를 정의해야한다.
구조는 두 개의 input 요소와 진행 상태를 표시하는 요소, 두 개의 핸들로 구성된다.

(1) 코드보기

<div class="range-slider-container">

  <div class="slider-track"> 
    <div class="progress"></div>
  </div>

  <label>
    <span class="handle min"></span>
    <input type="range" class="min-range range-input">
  </label>

  <label>
    <span class="handle max"></span>
    <input type="range" class="max-range range-input">
  </label>
</div>
요소 역할
range-slider-container - 최상위 컨테이너
slider-track - 슬라이더의 트랙 또는 배경 역할
progress - 슬라이더에서 선택된 범위를 시각적으로 나타내는 요소
- min-range 및 max-range 입력 값에 따라 너비나 위치가 변경됨
handle min - 슬라이더의 최소값을 설정하는 핸들(손잡이)
- 사용자가 드래그하여 범위의 최솟값 설정 가능함
- min-range input 요소와 연결됨
handle max - 슬라이더의 최대 값을 설정하는 핸들(손잡이)
- 사용자가 드래그하여 범위의 최댓값을 설정 가능함
- max-range 입력 요소와 연결됨
min-range - 범위의 최솟값을 설정하는 range input 요소
max-range - 범위의 최댓값을 설정하는 range input 요소



2) CSS 스타일링

💡 CSS의 경우, CSS 변수로 값의 최소, 최대, 그리고 슬라이더의 스타일을 선언한다.
이렇게 선언된 CSS변수는 JS에서 슬라이더 값을 계산할 때 쓰인다.

(1) CSS 변수 선언

:root {
  /* 범위 값 */
  --min-value: 10;
  --max-value: 200;
  --range-step: 5;
}
  • min-valuemax-value는 슬라이더의 최소 및 최대 값이다.
  • range-step은 슬라이드 한 스탭당 움직일 값이다.

:root {
    /* 슬라이더 및 진행 막대 스타일 */
  --range-height: 8px;
  --slider-width: 350px;
  --slider-bg: #e7eaf6;
  --progress-color: #a2a8d3;
}
  • range-height는 슬라이더의 높이값이다. 자식요소의 높이도 동일하게 지정된다.
  • slider-width는 슬라이더의 너비로, 자식요소의 너비도 동일하게 지정된다.
  • slider-bg는 슬라이더의 배경색이며, progress-color는 (선택한 범위를 나타내는)진행 막대의 색상이다.

:root {
 /* 핸들 스타일 */
  --handle-size: 20px;
  --handle-color: #38598b;

 /* 핸들 그림자 스타일 */
  --handle-effect-size: 5px;
  --handle-hover-color: rgba(17, 63, 103, 0.29);
  --handle-active-color: rgba(17, 63, 103, 0.49);
}
  • handle-size는 슬라이드 핸들의 크기이며, handle-color는 핸들의 색상이다.
  • handle-effect-size는 핸들에 마우스 호버했을 때 보여줄 핸들 그림자 크기이다. 핸들 그림자의 경우, 스타일링을 위해 넣은 요소라 없어도 무방하다.
  • handle-hover-color는 핸들을 호버했을 때 그림자 색상이며, handle-active-color는 핸들이 활성화됐을 때 그림자 색상이다.



(2) 슬라이더 트랙 스타일링하기

.range-slider-container {
  position: relative;
  .slider-track {
    position: relative;
    width: var(--slider-width);
    height: var(--range-height);
    border-radius: 4px;
    background-color: var(--slider-bg);
  }
}
  • .slider-track은 범위 슬라이더의 트랙 역할을 한다.
  • css변수(slider-width, range-height)로 트랙의 너비와 높이를 지정한다.
  • 그리고 css 변수(slider-bg)로 트랙의 배경색을 지정해준다.



(3) 진행 막대 스타일링하기

.slider-track .progress {
  height: 100%;
  position: absolute;
  left: 0;
  right: 0;
  border-radius: 5px;
  background-color: var(--progress-color);
}
  • .progress는 범위 슬라이더에서 현재 선택된 범위를 나타낸다.
  • 진행 막대의 높이는 100%로 지정해, 슬라이더 트랙(.slider-track) 높이와 동일하게 설정한다.
  • 그리고 현재 선택된 범위에 따라 요소의 위치가 변해야하므로 positionabsolute로 지정해야한다. 그러면 기존 slider-track 요소와 겹치게 된다.
  • leftright0으로 지정해, 진행막대의 초기 위치를 슬라이더의 왼쪽과 오른쪽 끝으로 맞춘다.
  • var(--progress-color)로 진행 막대의 색상을 설정한다.



(4) 슬라이더 입력을 담당하는 input 스타일링하기

.range-slider-container .range-input {
  margin: 0;
  top: 0;
  left: 0;
  position: absolute;
  width: 100%;
  height: var(--range-height);

  /** input range의 기본 스타일 제거 */
  background: none;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;

  /** input range을 컨트롤할 수 없게 */
  pointer-events: none;
}
  • RangeSlider의 구현 핵심은 type=rangeinput 2개를 겹쳐서 사용하는 것이다.
  • input의 위치를 absolute로 지정해, 슬라이드 트랙에 겹칠 수 있게 배치한다.
  • 우리는 지정된 핸들로만 범위를 조정하길 원하므로, input range에 포인트로 상호작용하기 못하도록 pointer-events: none를 지정한다.

.range-slider-container .range-input {
    &::-webkit-slider-thumb {
    height: var(--handle-size);
    width: var(--handle-size);
    border-radius: 50%;
    background: transparent; /* 배경색을 투명으로 지정해, 시각적으로 숨기기! */
    pointer-events: auto;
    -webkit-appearance: none;
    cursor: pointer;
  }

  &::-moz-range-thumb {
    height: var(--handle-size);
    width: var(--handle-size);
    border: none;
    border-radius: 50%;
    background: transparent; /* 배경색을 투명으로 지정해, 시각적으로 숨기기! */
    pointer-events: auto;
    -moz-appearance: none;
    box-shadow: 0 0 6px rgba(0, 0, 0, 0.05);
  }
}
  • 기존 range input의 핸들(slider-thumb)대신 커스텀 핸들(.handle)을 쓰기 위해, 기존 range input의 핸들을 시각적으로 숨긴다.



(5) 슬라이더 핸들 스타일링하기

.range-slider-container .handle {
  width: var(--handle-size);
  height: var(--handle-size);
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background-color: var(--handle-color);
}
  • 직접 조작할 커스텀 핸들을 스타일링한다.
  • CSS 변수(handle-size)로 핸들의 너비 높이를 지정한다.
  • 현재 슬라이드 최소, 최대값 위치에 핸들이 위치해야하므로 positionabsolute로 지정한다.
  • 핸들은 슬라이드 트랙 기준, 수직 중앙 위치하기 위해 transform: translate(-50%, -50%)를 지정한다.
  • CSS 변수(handle-color)로 핸들의 색상을 지정한다.




3) JavaScript 코드

💡 RangeSlider 클래스는 HTML 슬라이더의 동작을 제어한다.
이 클래스는 CSS 변수값으로 슬라이더의 최소, 최대 값 및 스텝 크기를 설정하고, 슬라이더 값을 업데이트한다.

  • 전체코드는 아래와 같으며, 이제 각 코드별 역할을 설명하겠다.
class RangeSlider {
  // (1) 초기화하기
  constructor() {
    this.constants = {
      MAX_VALUE: this.getGlobalCssValue('--max-value'),
      MIN_VALUE: this.getGlobalCssValue('--min-value'),
      RANGE_STEP: this.getGlobalCssValue('--range-step'),
      HANDLE_SIZE: this.getGlobalCssValue('--handle-size'),
      get RANGE() {
        return this.MAX_VALUE - this.MIN_VALUE;
      }
    };

    this.elements = {
      progress: document.querySelector('.progress'),
      minRange: document.querySelector('.min-range'),
      maxRange: document.querySelector('.max-range'),
      handles: document.querySelectorAll('.handle')
    };

    this.elements.minRange.addEventListener("input", (e) => {
      this.setStartValue(+e.target.value);
    });

    this.elements.maxRange.addEventListener("input", (e) => {
      this.setEndValue(+e.target.value);
    });
  }

  // (2) CSS 변수값 가져오기
  getGlobalCssValue(key) {
    const property = getComputedStyle(document.documentElement).getPropertyValue(key);
    return property ? parseFloat(property) : 0;
  }

  // (3) 초기값 설정하기
  init({ min, max }) {
    const { MIN_VALUE, MAX_VALUE, RANGE_STEP } = this.constants;
    const { minRange, maxRange } = this.elements;

    minRange.min = maxRange.min = MIN_VALUE;
    minRange.max = maxRange.max = MAX_VALUE;
    minRange.step = maxRange.step = RANGE_STEP;

    minRange.value = min; 
    maxRange.value = max;

    this.setStartValue(min);
    this.setEndValue(max);
  }

  // (4) 핸들 위치 계산하기
  setHandlePos(range, handle) {
    const { MIN_VALUE, RANGE, HANDLE_SIZE } = this.constants;
    const percentage = (range.value - MIN_VALUE) / RANGE;
    const offset = HANDLE_SIZE / 2 - HANDLE_SIZE * percentage;
    const left = `calc(${percentage * 100}% + ${offset}px)`;
    handle.style.left = left;
  }

  // (5) 최솟값 계산하기
  setStartValue(v) {
    const { minRange, maxRange, progress, handles } = this.elements;
    if (v >= +maxRange.value) {
      v = +maxRange.value - this.constants.RANGE_STEP;
      minRange.value = v;
    }
    const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
    progress.style.left = `${(value / this.constants.RANGE) * 100}%`;
    this.setHandlePos(minRange, handles[0]);
  }

  // (6) 최댓값 계산하기
  setEndValue(v) {
    const { minRange, maxRange, progress, handles } = this.elements;
    if (v <= +minRange.value) {
      v = +minRange.value + this.constants.RANGE_STEP;
      maxRange.value = v;
    }
    const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
    progress.style.right = `${100 - (value / this.constants.RANGE) * 100}%`;
    this.setHandlePos(maxRange, handles[1]);
  }

  // (5) 최솟값 계산하기
  getCurrStep(v) {
    return (v - this.constants.MIN_VALUE) / this.constants.RANGE_STEP;
  }
}
// 사용 예시
const slider = new RangeSlider();

slider.init({ min: 10, max: 150 });



(1) constructor, 값 초기화하기

constructor() {
  // (a)
  this.constants = {
    MAX_VALUE: this.getGlobalCssValue('--max-value'),
    MIN_VALUE: this.getGlobalCssValue('--min-value'),
    RANGE_STEP: this.getGlobalCssValue('--range-step'),
    HANDLE_SIZE: this.getGlobalCssValue('--handle-size'),
    get RANGE() {
      return this.MAX_VALUE - this.MIN_VALUE;
    }
  };

  // (b)
  this.elements = {
    progress: document.querySelector('.progress'),
    minRange: document.querySelector('.min-range'),
    maxRange: document.querySelector('.max-range'),
    handles: document.querySelectorAll('.handle')
  };

  // (c)
  this.elements.minRange.addEventListener("input", (e) => {
    this.setStartValue(+e.target.value);
  });
  this.elements.maxRange.addEventListener("input", (e) => {
    this.setEndValue(+e.target.value);
  });
}
  • constructor은 RangeSlider 클래스에서 사용할 변수값을 선언하고 이벤트를 등록한다.
번호 코드 설명
(a) 상수 초기화 - CSS 변수에서 값을 가져와 constants 객체에 저장한다.
ex) 슬라이더의 최대값, 최소값, 단계, 핸들 크기 등
(b) DOM 요소 초기화 - 슬라이더와 관련된 DOM 요소를 elements 객체에 저장한다.
(c) 이벤트 리스너 등록 - minRange와 maxRange 요소에 input 이벤트 리스너를 등록한다.
- minRange와 maxRange 요소의 값이 변경될 때 setStartValue와 setEndValue를 호출해 범위값을 업데이트한다.



(2) getGlobalCssValue, CSS 변수 값 가져오는 함수

getGlobalCssValue(key) {
  // (a)
  const property = getComputedStyle(document.documentElement).getPropertyValue(key);

  // (b)
  return property ? parseFloat(property) : 0;
}
  • getGlobalCssValue 함수는 CSS에서 선언한 CSS 변수값에 접근한다.
번호 코드 설명
(a) CSS 변수 접근하기 - 주어진 CSS 변수(key)의 값을 가져와 숫자로 변환하여 반환한다.
(b) 값 리턴하기 - 해당하는 CSS 변수가 없으면 0을 반환한다.



(3) init, 초기값을 지정하는 함수

init({ min, max }) {
  const { MIN_VALUE, MAX_VALUE, RANGE_STEP } = this.constants;
  const { minRange, maxRange } = this.elements;

  minRange.min = maxRange.min = MIN_VALUE;
  minRange.max = maxRange.max = MAX_VALUE;
  minRange.step = maxRange.step = RANGE_STEP;

  minRange.value = min;
  maxRange.value = max;

  this.setStartValue(min);
  this.setEndValue(max);
}
  • init 함수는 주어진 min, max값에 따라 슬라이드의 초기값을 설정한다.



(4) setHandlePos, 핸들의 위치를 조정하는 함수

setHandlePos(range, handle) {
  const { MIN_VALUE, RANGE, HANDLE_SIZE } = this.constants;

  // (a)
  const percentage = (range.value - MIN_VALUE) / RANGE;

  // (b)
  const offset = HANDLE_SIZE / 2 - HANDLE_SIZE * percentage;  
  const left = `calc(${percentage * 100}% + ${offset}px)`; 
  handle.style.left = left;
}
  • setHandlePos 함수는 range 값을 기반으로, 핸들 요소(handle)의 위치를 설정한다.
코드 번호 코드 설명
(a) 위치 퍼센트 계산 range 값이 전체 범위에서 몇 퍼센트에 해당하는지 계산한다.
(b) 핸들 위치 조정 핸들 크기(offset)를 고려해, 핸들의 위치(left )를 결정한다.



(5) setStartValue, 슬라이드 최솟값을 설정하는 함수

setStartValue(v) {
  const { minRange, maxRange, progress, handles } = this.elements;

  // (a)
  if (v >= +maxRange.value) {
    v = +maxRange.value - this.constants.RANGE_STEP;
    minRange.value = v;
  }

  // (b)
  const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
  progress.style.left = `${(value / this.constants.RANGE) * 100}%`;
  this.setHandlePos(minRange, handles[0]);
}
  • setStartValue()는 주어진 v 값을 기반으로 슬라이더의 최솟값을 설정한다.
코드 번호 코드 설명
(a) 최솟값 조정 최소값이 최대값보다 크거나 같으면, 최소값을 최대값보다 작게 조정한다.
(b) 핸들 위치 조정 최소값 핸들의 위치를 재설정한다.



(6) setEndValue, 슬라이드 최댓값을 설정하는 함수

setEndValue(v) {
  const { minRange, maxRange, progress, handles } = this.elements;

  // (a)
  if (v <= +minRange.value) {
    v = +minRange.value + this.constants.RANGE_STEP;
    maxRange.value = v;
  }

  // (b)
  const value = this.getCurrStep(v) * this.constants.RANGE_STEP;
  progress.style.right = `${100 - (value / this.constants.RANGE) * 100}%`;
  this.setHandlePos(maxRange, handles[1]);
}
  • setStartValue()는 주어진 v 값을 기반으로 슬라이더의 최댓값을 설정한다.
코드 번호 코드 설명
(a) 최댓값 조정 최대값이 최소값보다 작거나 같으면, 최대값을 최소값보다 크게 조정한다.
(b) 핸들 위치 조정 최대값 핸들의 위치를 재설정한다.



💡 일련의 과정을 거치면, 최소&최대값을 지정할 수 있는 RangeSlider(범위 슬라이드)가 완성된다.

See the Pen RangeSlider by KumJungMin (@kumjungmin) on CodePen.




3. 마치며…

이번 시간에는 두 개의 range input을 겹치는 방식으로, RangeSlider(범위 슬라이더)를 구현해보았다. 전반적인 설정값은 CSS 변수로 선언해 이 값에 따라 슬라이더 스타일은 물론, 슬라이드 기본값을 지정할 수 있었다.

스크립트 단에는 범위 슬라이드용 클래스를 만들어 constructor는 초기값을 지정하고, init은 슬라이더의 초기 값을 설정할 수 있었다.
그리고 Input 이벤트가 발생할 때, 입력값에 따라 슬라이드의 핸들 위치를 조정했다.

현재는 순수 JS로 구현을 했지만, Vue, React로 구현할 시 constructor에 선언한 변수는 props나 반응형 변수로 지정하면 확장성 있게 재구현할 수 있을 것이다 🙂

반응형

댓글