[React + Typescript + emotion] RangeSlider 컴포넌트 제작

2024. 8. 15. 18:23

프로젝트 내에 필터 설정에 키, 나이의 범위를 지정하는 부분에 양방향 RangeSlider가 필요해서 제작하게 되었다.

input Range, Range Slider 등등 다양하게 불리는 거 같다.

참고가 많이 되었던 블로그! 감사합니다.
*참고 - https://velog.io/@forest_xox/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%96%91%EB%B0%A9%ED%96%A5-Range-%EB%A7%8C%EB%93%A4%EA%B8%B0


우선 props로 받아올 값에는

1. 기준이 될 슬라이더의 최소, 최댓값(min, max)
2. 내가 선택한 현재의 최소, 최댓값(currentMin, currentMax)
3. 값 변경 시 호출될 콜백함수(handleValueChange)
가 필요하다 생각했다.

 

여기서 step은 slider가 움직일 때마다 +- 될 값이다.

나는 사용할 rangeSlider들이 다 1씩 움직일 거라 우선 고정시켜 놨지만, 추후 더 많이 쓰고 변경되면 props로 받아야 할 것 같다.

import { useEffect, useState } from "react";
import * as S from "./RangeSlider.styled";

interface RangeSliderProps {
  currentMinValue?: number;
  currentMaxValue?: number;
  min: number;
  max: number;
  handleValueChange?: (minValue: number, maxValue: number) => void;
}
const RangeSlider = ({
  currentMinValue,
  currentMaxValue,
  min,
  max,
  handleValueChange,
}: RangeSliderProps) => {
  const [minValue, setMinValue] = useState(currentMinValue ?? min);
  const [maxValue, setMaxValue] = useState(currentMaxValue ?? max);
  const [minPercent, setMinPercent] = useState(0);
  const [maxPercent, setMaxPercent] = useState(100);
  const step = 1;

 

슬라이더 값 변경 시 호출될 함수들을 작성해 줬다.

  const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setMinValue(parseInt(e.target.value));
  };

  const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setMaxValue(parseInt(e.target.value));
  };

 

양방향 슬라이더다 보니 최솟값과 최댓값의 차이가 step 보다 작을 경우가 되지 않게 하는 함수도 만들었다.

(현재 maxValue가 작아지면 minValue = maxValue-1인 채로 같이 줄어들게 되어있다.)

정상적으로 최소 최대값의 차이가 step 보다 크면 그대로 minValue, maxValue에 넣고, 

그게 아닐 경우 css에 필요한 percentage를 계산하게 했다.

  const updatePercentages = () => {
    if (maxValue - minValue < step) {
      setMaxValue(minValue + step);
      setMinValue(maxValue - step);
    } else {
      const range = max - min;
      setMinPercent(((minValue - min) / range) * 100);
      setMaxPercent(100 - ((max - maxValue) / range) * 100);
    }
  };

  useEffect(() => {
    updatePercentages();
    if (handleValueChange) {
      handleValueChange(minValue, maxValue);
    }
  }, [minValue, maxValue]);

 

return

 return (
    <S.SliderContainer>
      <S.SliderWrapper>
        <S.SliderRangeInner left={minPercent} width={maxPercent - minPercent} />
        <S.RangeWrapper>
          <S.Range
            type="range"
            min={min}
            max={max - step}
            step={step}
            value={minValue}
            onChange={(e) => handleMinChange(e)}
          />
          <S.Range
            type="range"
            min={min + step}
            max={max}
            step={step}
            value={maxValue}
            onChange={(e) => handleMaxChange(e)}
          />
        </S.RangeWrapper>
      </S.SliderWrapper>
    </S.SliderContainer>
  );
};

export default RangeSlider;

 

css

 

SliderRangeInner의 left, width에 계산한 percent값들을 넣어준다.

import { css } from "@emotion/react";
import styled from "@emotion/styled";

export const SliderContainer = styled.div`
  width: 320px;
  height: 24px;
  display: flex;
  align-items: center;
  align-self: center;
`;
export const SliderWrapper = styled.div`
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: 16px;
  background-color: #ddd;
`;

export const SliderRangeInner = styled.div<{ left: number; width: number }>`
  ${({ left, width, theme }) => css`
    position: absolute;
    left: ${left}%;
    width: ${width}%;
    background: ${theme.colors.navy900};
    height: 4px;
  `}
`;

export const RangeWrapper = styled.div`
  position: relative;
  height: 4px;
`;

export const Range = styled.input`
  ${({ theme }) => css`
    position: absolute;
    top: -10px;
    width: 100%;
    -webkit-appearance: none;
    background: none;
    pointer-events: none;

    &::-webkit-slider-thumb {
      height: 24px;
      width: 24px;
      border-radius: 50%;
      border: 1.5px solid ${theme.colors.navy900};
      background-color: ${theme.colors.white};
      -webkit-appearance: none;
      pointer-events: auto;
    }
  `}
`;

 

  &::-webkit-slider-thumb 이 부분을 잘 생각하지 못했는데 블로그에서 많이 참고했다.

 

BELATED ARTICLES

more