프로젝트 당시에 rsuite
라는 라이브러리를 사용해서 range slider
를 구현했다. rsute
를 사용한 이유는 짧은 시간 내에 내가 원하는 디자인으로 구현하기에 괜찮은 라이브러리라고 생각했기 때문이다.
하지만 rsuite
라이브러리를 설치하고 나서 메인화면의 font가 깨지는 일이 발생했고, range slider
하나 때문에 rsuite
라이브러리를 써야 한다는 게 불필요하다고 판단했다.
따라서 리팩토링 기간 중에 range slider
를 직접 구현하기로 결정했다.
참고로 rsuite
라이브러리를 사용했을 때 모습은 아래와 같았다.
현재 리팩토링 중인 프로젝트는 TypeScript
와 React
, 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
컴포넌트에 대한 스타일을 정의하는 파일Recoil
의 atom을 사용하여 취향설문의 다섯가지 질문에 대한 상태를 관리하는 파일이다. range slider
를 사용한 부분은 세 번째 설문이라서 해당하는 부분의 코드는 아래와 같다.
// recoil/SurveyState.ts
import { atom } from "recoil";
...
export const thirdState = atom<string>({
key: "thirdState",
default: "",
});
...
thirdState
: 세 번째 질문에 대한 상태를 관리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%)`;
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의 스타일을 적용세 번째 취향설문을 사용자가 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
를 통해 isOpen
이 true
이고 mark
가 빈 문자열일 경우 mark
를 "1"로 설정thirdState
값을 "1"로 수정함const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setMark(event.target.value);
};
event.target.value
를 사용하여 mark
상태 업데이트const handleChange = (val: number) => {
setMark(val.toString());
};
mark
상태 업데이트const labels = Array.from({ length: 5 }, (_, i) => i + 1).map((val) => (
<style.Label key={val} onClick={() => handleChange(val)}>
{val}시간
</style.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;
range slider
를 직접 구현하면서 프로젝트 당시에는 구현하지 못했던 것을 지금은 구현할 수 있게 된 모습이 뿌듯하다. 원래는 모듈화를 하려고 했으나 한 번만 사용하는데 모듈화를 하는 것은 불필요하다고 판단했다. 프로젝트에서 컴포넌트의 쓰임을 보고 모듈화 여부를 결정하는 게 좋은 방향성이라는 것을 배웠다.