[React/TS] Range Slider 구현하기

예구·2023년 8월 28일
0

특화PJT

목록 보기
8/8
post-custom-banner

구현한 모습

1. range slider를 구현한 이유

프로젝트 당시에 rsuite라는 라이브러리를 사용해서 range slider를 구현했다. rsute를 사용한 이유는 짧은 시간 내에 내가 원하는 디자인으로 구현하기에 괜찮은 라이브러리라고 생각했기 때문이다.

하지만 rsuite 라이브러리를 설치하고 나서 메인화면의 font가 깨지는 일이 발생했고, range slider 하나 때문에 rsuite 라이브러리를 써야 한다는 게 불필요하다고 판단했다.

따라서 리팩토링 기간 중에 range slider를 직접 구현하기로 결정했다.


참고로 rsuite 라이브러리를 사용했을 때 모습은 아래와 같았다.

rsuite를 사용한 모습



2. 전체적인 구조 및 구성

현재 리팩토링 중인 프로젝트는 TypeScriptReact, Recoil, styled-components을 사용했다.


앞으로 나올 파일의 구조는 아래와 같다.

📦recoil
 ┗ 📜SurveyState.ts
  • SurveyState.ts: recoil을 이용해서 취향설문의 다섯가지 질문에 대한 상태를 관리하는 파일

📦question3
 ┣ 📜index.tsx
 ┗ 📜styles.ts
  • index.tsx: ThirdQuestion 컴포넌트를 포함하고 있고, range slider와 해당하는 label을 렌더링하는 파일
  • styles.ts: styled-components를 이용하여 ThirdQuestion 컴포넌트에 대한 스타일을 정의하는 파일



3. SurveyState.ts

Recoil의 atom을 사용하여 취향설문의 다섯가지 질문에 대한 상태를 관리하는 파일이다. range slider를 사용한 부분은 세 번째 설문이라서 해당하는 부분의 코드는 아래와 같다.

// recoil/SurveyState.ts

import { atom } from "recoil";

...

export const thirdState = atom<string>({
  key: "thirdState",
  default: "",
});

...
  • thirdState: 세 번째 질문에 대한 상태를 관리
  • 빈 문자열("")을 기본값으로 함



4. question3/styles.ts

styled-components 라이브러리를 사용해서 취향설문의 세 번째 질문에 사용되는 컴포넌트의 스타일을 정의하는 파일이다.

우선 전체 코드는 아래와 같다.

import styled from "styled-components";
import * as colors from "@styles/Colors";

// slider와 label이 포함된 컨테이너의 스타일 정의
export const SliderContainer = styled.div`
  border-radius: 3px;
  display: block;
  flex-direction: row;
  align-items: center;
  background-color: ${colors.white};
  margin-top: 16px;
  padding: 20px 70px 16px 70px;
`;

// slider의 스타일 정의
export const Slider = styled.input`
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 14px;
  border-radius: 10px;
  background: ${(props) => {
    const percentage = ((Number(props.value) - 1) / 4) * 100;
    return `linear-gradient(to right, ${colors.mainColor} 0%, ${colors.mainColor} ${percentage}%, ${colors.grey[100]} ${percentage}%, ${colors.grey[100]} 100%)`;
  }};

  &:focus {
    outline: none;
  }

  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 28px;
    height: 28px;
    background-color: ${colors.white};
    border: 1px solid ${colors.grey[300]};
    border-radius: 50%;
    cursor: pointer;
  }

  &::-moz-range-thumb {
    width: 28px;
    height: 28px;
    background-color: ${colors.white};
    border: 1px solid ${colors.grey[300]};
    border-radius: 50%;
    cursor: pointer;
  }
`;

// 각 label의 컨테이너 스타일 정의
export const Labels = styled.div`
  display: flex;
  justify-content: space-between;
  padding-top: 10px;
`;

// 각 label 스타일 정의
export const Label = styled.span`
  position: relative;
  cursor: pointer;
  display: inline-block;
  text-align: center;
  width: 40px;
  font-family: "SUIT";
  font-style: normal;
  font-size: 16px;
  line-height: 1.5;
`;

4개의 컴포넌트 중 Slider 컴포넌트는 input 태그를 사용하여 사용자에게 시간을 선택할 수 있도록 한다.
Slider 컴포넌트에서 background 프로퍼티가 특히 중요한데, 사용자가 현재 선택한 값에 따라 slider의 배경색이 바뀌어야 하기 때문이다.

 background: ${(props) => {
    const percentage = ((Number(props.value) - 1) / 4) * 100;
    return `linear-gradient(to right, ${colors.mainColor} 0%, ${colors.mainColor} ${percentage}%, ${colors.grey[100]} ${percentage}%, ${colors.grey[100]} 100%)`;
  • slider의 배경색을 설정하는 코드
  • props.value에 따라 슬라이더의 배경색이 선형 그라데이션으로 변경됨
    • props.value가 3이라면 slider의 60% 부분이 colors.mainColor로 채워지고 나머지 40%는 colors.grey[100]으로 채워짐

추가적으로 알아야 할 것은 다음과 같다.

  • -webkit-appearance: none;appearance: none;는 브라우저의 기본 스타일을 제거함
  • &:focus { outline: none; }: slider가 포커스될 때 아웃라인 제거
  • &::-webkit-slider-thumb: 웹킷 기반의 브라우저(Chrome, Safari)에서 slider의 thumb의 스타일을 적용
  • &::-moz-range-thumb: 모질라 기반의 브라우저(Firefox)에서 slider의 thumb의 스타일을 적용



5. question3/index.tsx

세 번째 취향설문을 사용자가 slider로 응답할 수 있도록 구현한 코드다.


interface ThirdQuestionProps {
  isOpen: boolean;
}

const ThirdQuestion = ({ isOpen }: ThirdQuestionProps) => {
  ...
};
  • 모든 취향설문은 Accordion 안에 있기 때문에 Accordion이 열렸는지, 닫혔는지가 중요함
  • 따라서 isOpen을 props로 받고 있음

const [mark, setMark] = useRecoilState(thirdState);

useEffect(() => {
  if (isOpen && mark === "") {
    setMark("1");
  }
}, [setMark, isOpen, mark]);
  • useRecoilState를 사용하여 사용자가 선택한 mark의 상태를 관리
  • useEffect를 통해 isOpentrue이고 mark가 빈 문자열일 경우 mark를 "1"로 설정
    • 즉, 취향설문 페이지가 렌더링되고 처음으로 세 번째 취향설문이 있는 accordion을 열었을 때, thirdState 값을 "1"로 수정함

const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  setMark(event.target.value);
};
  • slider의 값이 변경될 때 호출되며, event.target.value를 사용하여 mark 상태 업데이트

const handleChange = (val: number) => {
  setMark(val.toString());
};
  • label이 클릭될 때 호출되며, 클릭된 label 값을 사용하여 mark 상태 업데이트

const labels = Array.from({ length: 5 }, (_, i) => i + 1).map((val) => (
  <style.Label key={val} onClick={() => handleChange(val)}>
    {val}시간
  </style.Label>
));
  • 1부터 5까지의 label을 동적으로 생성
  • 클릭 시 handleChange()를 호출하여 mark 상태 변경

전체 코드는 아래와 같다.

import React, { useEffect } from "react";
import * as style from "./styles";
import { useRecoilState } from "recoil";
import { thirdState } from "@recoil/SurveyState";

interface ThirdQuestionProps {
  isOpen: boolean;
}

const ThirdQuestion = ({ isOpen }: ThirdQuestionProps) => {
  const [mark, setMark] = useRecoilState(thirdState);

  useEffect(() => {
    if (isOpen && mark === "") {
      setMark("1");
    }
  }, [setMark, isOpen, mark]);

  const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setMark(event.target.value);
  };

  const handleChange = (val: number) => {
    setMark(val.toString());
  };

  const labels = Array.from({ length: 5 }, (_, i) => i + 1).map((val) => (
    <style.Label key={val} onClick={() => handleChange(val)}>
      {val}시간
    </style.Label>
  ));

  return (
    <style.SliderContainer>
      <style.Slider
        type="range"
        min={1}
        max={5}
        value={parseInt(mark) || 1}
        onChange={handleSliderChange}
      />
      <style.Labels>{labels}</style.Labels>
    </style.SliderContainer>
  );
};

export default ThirdQuestion;



6. 마무리

range slider를 직접 구현하면서 프로젝트 당시에는 구현하지 못했던 것을 지금은 구현할 수 있게 된 모습이 뿌듯하다. 원래는 모듈화를 하려고 했으나 한 번만 사용하는데 모듈화를 하는 것은 불필요하다고 판단했다. 프로젝트에서 컴포넌트의 쓰임을 보고 모듈화 여부를 결정하는 게 좋은 방향성이라는 것을 배웠다.

profile
우당탕탕 FE 성장기
post-custom-banner

0개의 댓글