Home v-model을-활용하여-멀티-레인지-슬라이더-만들기
Post
Cancel

v-model을-활용하여-멀티-레인지-슬라이더-만들기


칵테일 검색 필터를 구현하던 중 도수의 범위를 지정하기 위해서 멀티 레인지 슬라이더가 필요했다. 몇번의 구글링 결과 도움이 되는 블로그를 찾았고 이를 기반으로 vue3에서 어떻게 멀티 레인지 슬라이더를 만들 수 있는지 정리해보았다.

참고한 블로그


1) 슬라이더 마크업

슬라이더는 track과 thumb로 구성되어 있다. 멀티 레인지를 만들기 위해서는 아래와 같은 input을 겹쳐 thumb를 2개로 만들어 구현할 수 있다.

image-20220730231617106


(1) 슬라이더를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
  <div class="cocktail-search-filter-slider">
    <!-- 진짜 슬라이더 -->
    <input
      type="range"
      id="slider-left"
      max="30"
      min="0"
      value=6
    />
    <input
      type="range"
      id="slider-right"
      max="30"
      min="0"
      value=15
    />

    <!-- 가짜 슬라이더 -->
    <div class="filter-slider">
      <div class="filter-slider-line"></div>
      <div class="filter-slider-range" :style="rangeStyle"></div>
      <div class="filter-slider-left" :style="leftThumbStyle"></div>
      <div class="filter-slider-right" :style="rightThumbStyle"></div>
    </div>
  </div>
</template>
  1. <input type="range">인 슬라이더 2개를 만든다. (진짜 슬라이더)
    • 왼쪽 thumb이 될 슬라이더
    • 오른쪽 thumb 될 슬라이더
  2. 사용자에게 보일 가짜 슬라이더를 만든다. (가짜 슬라이더)
  3. 슬라이더를 다 겹치게 한다.

아래처럼 진짜 슬라이드 2개와 가짜 슬라이더를 다 겹치게 한다.

image-20220730224131450


vue-multi-range-input-5


(2) 가짜 슬라이더만 보이게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
 <!-- 생략 --> 
</template>

<script setup lang="ts">
</script>

<style scoped lang="scss">
/* 진짜 슬라이더 */
.cocktail-search-filter-slider {
  @include flex(column);
  position: relative;
  width: 95%;
  input {
    position: absolute;
    z-index: 1;
    width: 100%;
    height: 8px;
    background-color: black;
    opacity: 0;
    appearance: none;
    pointer-events: none;

    /* thumb에만 설정을 한다. (크롬) */
    &::-webkit-slider-thumb {
      z-index: 10;
      width: 32px;
      height: 32px;
      background-color: black;
      cursor: pointer;
      appearance: none;
      pointer-events: all;
    }

    /*thumb에만 설정을 한다. (파이어 폭스) */
    &::-moz-range-thumb {
      z-index: 10;
      width: 32px;
      height: 32px;
      background-color: black;
      cursor: pointer;
      appearance: none;
      pointer-events: all;
    }
  }

</style>

✅ 진짜 슬라이더를 투명하게 하여 사용자가 볼 수 없지만 사용자가 진짜 slider의 thumb을 움직여 value를 수정할 수 있도록 한다.

  • appearance속성을 통해 수정한 CSS를 반영한다.

  • pointer-events를 통해 진짜 슬라이더가 포인트의 이벤트 대상이 되지 않도록 한다.

  • 그러나 thumb는 마우스를 통해 이동시켜야 하기 때문에 해당 부분만 포인트의 대상이 되도록 한다.

    • 크롬: ::-webkit-slider-thumb로 접근이 가능하다.

    • 파이어 폭스: ::-moz-range-thumb로 접근이 가능하다.


결국 아래처럼, 가짜 슬라이더만 보이는 상태이다 (그러나 진짜 Slider의 thumb은 이동시킬 수 있다.)

image-20220730224136373


2) 진짜 슬라이더의 value에 따라 가짜 슬라이더를 움직인다.

다음처럼 진짜 슬라이더의 thumb에 따라 가짜 슬라이더도 움직이게 해야 한다.

vue-multi-range-input-3


(1) v-model을 통해 진짜 슬라이더의 value를 가져온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
  <div class="cocktail-search-filter-slider">
    <!-- 진짜 슬라이더 -->
    <input
      type="range"
      id="slider-left"
      max="30"
      min="0"
      v-model.number="sliderLeftValue"
    />
    <input
      type="range"
      id="slider-right"
      max="30"
      min="0"
      v-model.number="sliderRightValue"
    />

    <!-- 가짜 슬라이더 -->
    <div class="filter-slider">
      <div class="filter-slider-line"></div>
      <div class="filter-slider-range"></div>
      <div class="filter-slider-left"></div>
      <div class="filter-slider-right"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

// v-model로 연결한 값
const sliderLeftValue = ref(6);
const sliderRightValue = ref(15);
   
// sliderLeftValue의 값이 변하면 함수를 실행한다.
watch(sliderLeftValue, () => {
  console.log(sliderLeftValue.value);
});
</script>

이렇게 vue의 v-model을 활용하여 진짜 슬라이더의 value에 간단하게 접근할 수 있다. 접근한 값을 가지고 가짜 슬라이더의 thumb를 움직이면 된다.


image-20220730203145430


(2) 진짜 슬라이더의 value에 따라 가짜 슬라이더가 움직인다.

진짜 슬라이더의 value에 따라 가짜 슬라이더의 left, width값을 수정하여 가짜 슬라이더의 thumb와 thumb간의 범위를 변경할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<template>
  <div class="cocktail-search-filter-slider">
    <!-- 진짜 슬라이더 -->
    <input
      type="range"
      id="slider-left"
      max="30"
      min="0"
      v-model.number="sliderLeftValue"
    />
    <input
      type="range"
      id="slider-right"
      max="30"
      min="0"
      v-model.number="sliderRightValue"
    />

    <!-- 보이는 슬라이더 -->
    <div class="filter-slider">
      <div class="filter-slider-line"></div>
        
       <!-- 스타일 바인딩 --> 
      <div class="filter-slider-range" :style="rangeStyle"></div>
      <div class="filter-slider-left" :style="leftThumbStyle"></div>
      <div class="filter-slider-right" :style="rightThumbStyle"></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, watch, computed } from "vue";

// 진짜 Slider thumb의 초기 위치
const sliderLeftValue = ref(6);
const sliderRightValue = ref(15);

// 가짜 Slider thumb의 초기 위치 계산
const leftThumbStyle = reactive({
  left: `${(sliderLeftValue.value / 30) * 100}%`,
});

const rightThumbStyle = reactive({
  left: `${(sliderRightValue.value / 30) * 100}%`,
});

const rangeStyle = reactive({
  left: `${(sliderLeftValue.value / 30) * 100}%`,
  width: `${((sliderRightValue.value - sliderLeftValue.value) / 30) * 100}%`,
});


// 왼쪽 SliderValue값이 변할 때 실행된다.
watch(sliderLeftValue, () => {
  // 값 변경: 가짜 슬라이더의 왼쪽 thumb이동, range 이동 및 너비 변경
  leftThumbStyle.left = `${(sliderLeftValue.value / 30) * 100}%`;  
  rangeStyle.left = `${(sliderLeftValue.value / 30) * 100}%`;
  rangeStyle.width = `${
    ((sliderRightValue.value - sliderLeftValue.value) / 30) * 100
  }%`;
});

// 오른쪽 SliderValue값이 변할 때 실행된다.
watch(sliderRightValue, () => {
  // 값 변경: 가짜 슬라이더의 오른쪽 thumb이동, range 너비 변경
  rightThumbStyle.left = `${(sliderRightValue.value / 30) * 100}%`;   
  rangeStyle.width = `${
    ((sliderRightValue.value - sliderLeftValue.value) / 30) * 100
  }%`;
});
</script>

✅ 진짜 슬라이더의 value에 따라 가짜 슬라이더의 스타일 속성 값이 변한다.

  • 가짜 슬라이더의 thumb의 위치가 변한다.
    • 스타일 속성의 left의 값에 변화를 준다. 위치 변화
  • 가짜 슬라이더 사이의 범위를 나타내는 range의 위치와 너비가 변한다.
    • 스타일 속성의 left의 값에 변화를 준다. 위치 변화
    • 스타일 속성의 width의 값에 변화를 준다. 너비 변화


(3) 범위 제한을 통해 thumb끼리 서로 엇갈리지 않도록 한다.

그러나, 위와 같이 구현을 끝낼 경우 아래와 같은 문제가 발생한다.😅


vue-multi-range-input-1


그래서 thumb이 서로의 영역을 침범하지 않도록 범위를 제한해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<!-- 생략 -->
</template>
<script>
// 생략..

// 왼쪽 Slider Value값이 변할 때 실행된다.
watch(sliderLeftValue, () => {
  // 왼쪽 thumb의 맥시멈
  if (sliderLeftValue.value > 27) {
    sliderLeftValue.value = 27;
  }

  // 서로의 영역을 침범하지 않기 위한, 범위 제한
  if (sliderRightValue.value <= sliderLeftValue.value + 3) {
    sliderLeftValue.value = sliderRightValue.value - 3;
  }

  // 값 변경
  leftThumbStyle.left = `${(sliderLeftValue.value / 30) * 100}%`;
  rangeStyle.left = `${(sliderLeftValue.value / 30) * 100}%`;
  rangeStyle.width = `${
    ((sliderRightValue.value - sliderLeftValue.value) / 30) * 100
  }%`;
});

// 오른쪽 Slider Value값이 변할 때 실행된다.
watch(sliderRightValue, () => {
  // 오른쪽 thumb의 미니멈
  if (sliderRightValue.value < 3) {
    sliderRightValue.value = 3;
  }

  // 서로의 영역을 침범하지 않기 위한, 범위 제한
  if (sliderRightValue.value - 3 <= sliderLeftValue.value) {
    sliderRightValue.value = sliderLeftValue.value + 3;
  }

  // 값 변경
  rightThumbStyle.left = `${(sliderRightValue.value / 30) * 100}%`;
  rangeStyle.width = `${
    ((sliderRightValue.value - sliderLeftValue.value) / 30) * 100
  }%`;
});
</script>

이제는 서로 영역을 침범하지 않는 것을 확인할 수 있다.


vue-multi-range-input-2


3) 로컬스토리지에 value를 저장하고 불러온다.

앞서 말했듯이, 이는 필터를 구현하기 위해 필요한 기능이다. (칵테일을 검색할 때, 도수 범위를 설정하여 칵테일 검색에 도움을 주기 위한 기능)

이런 필터는 다시 웹사이트에 방문했을 때도 기록이 남아있어 사용자가 재사용한다면 편리한 기능이라고 생각했기 때문에 사용자가 지난번에 설정했던 값을 로컬 스토리지에 저장해서 불러오도록 했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script setup lang="ts">
import { ref, reactive, watch, computed } from "vue";
import { useStore } from "vuex";
const store = useStore();

// 진짜 슬라이더 value
const sliderValue = computed(
  () => store.getters["cocktailSearch/getSearchFilterAlcoholStrength"]
);
const sliderLeftValue = ref(sliderValue.value[0]);
const sliderRightValue = ref(sliderValue.value[1]);

// 진짜 슬라이더 값이 변경될 때, vuex에 값을 저장한다.
// 디바운스 사용하여 과도한 함수 실행을 막는다.
let debounce: ReturnType<typeof setTimeout>;
watch([sliderLeftValue, sliderRightValue], () => {
  if (debounce) {
    clearTimeout(debounce);
  }
  debounce = setTimeout(
    () =>
      store.dispatch("cocktailSearch/changeFilterAlcoholStrength", [
        sliderLeftValue.value,
        sliderRightValue.value,
      ]),
    200
  );
});

// 생략
</script>
  • 로컬 스토리지에 value가 저장되어 있으면 vuex로 가져온다. 없으면 기본 값을 설정한다.
  • 초기 value 값을 vuex에서 가져오고, value가 변경 될 때 vuex에 값을 저장한다. (debounce 활용)


This post is licensed under CC BY 4.0 by the author.