
프로젝트 요구 상 Datepicker와 Timepicker를 써야했는데 구상한 디자인에 맞춘 기능을 만들어내기 위해
라이브러리를 찾다보니 MUI를 택하게 되었다
https://mui.com/x/react-date-pickers/getting-started/
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/x-date-pickers
npm install dayjs
Date Library는 dayjs를 선택
https://mui.com/x/react-date-pickers/date-picker/
DatePicker종류는 3가지가 있는데 모바일 화면 위주의 프로젝트였기 때문에 MobileDatePicker로 구현했다.

보다시피 디자인도 깔끔하고 기능상으로 변경해야할 점은 없어서 바로 커스텀으로 바로 들어갈 수 있었다.
하지만.. DatePicker를 커스텀화하는 방식이 정말 무궁무진하다보니 하나하나 컴포넌트를 찾아서 SlotProps를 통해 커스텀하다보면 코드가 정말 끝도 없이 길어지는걸 느낄 수 있었다.
도저히...이건 아닌 거 같아서 찾아보니
https://mui.com/x/react-date-pickers/custom-components/
<DatePicker
{...otherProps}
slots={{
// Override default <ActionBar /> with a custom one
actionBar: CustomActionBar,
}}
slotProps={{
// pass props `actions={['clear']}` to the actionBar slot
actionBar: { actions: ['clear'] },
}}
/>
이와 같이 Picker의 컴포넌트 하나하나를 분리해서 커스텀화하여 넘겨줄 수 있었다.
원래는 slotprops로 무한정 늘어나는 sx(css)코드로 이루어져 있었지만

다음과 같이 커스텀화된 textField, toolbar, actionBar를 따로 넘겨줌으로서 정돈된 코드의 형태를 만들 수 있었다.
단, textField로 내려주는 일부 prop을 slotProps에서 작성하는 것이 공식문서에 따르면 정석인 것 같은데 내가 원하는 방식으로 작동을 하지 않아서 저렇게 prop을 내려주고 있다.
day와 같은 경우 slot으로 넘길 customDPDay를 만들수도 있었지만 건드리는 순간 날짜들이 다 흩어져서 그걸 다시 sx(css)로 잡아주는 것보다는 선택된 날짜의 컬러만 바꿔주는게 가장 편리해서 따로 구분하지 않았다.

이때, DatePicker의 textField, toolbar, actionBar를 간단하게 설명하면

Datepicker를 클릭하여 불러내는 부분이고 MUI에서 input의 역할을 한다.
export function CustomDPTextField(
props: TextFieldProps & { showStartAdornment?: boolean } & {
errorType?: string;
}
) {
const { showStartAdornment, errorType, ...otherProps } = props;
return (
<TextField
{...otherProps}
InputProps={{
readOnly: true,
startAdornment: showStartAdornment ? (
<InputAdornment position="start" sx={{ position: "relative" }}>
<Typography
sx={[
labelCSS,
{
color: errorType
? "var(--default-red-color)"
: "var(--gray-color-4)",
},
]}
>
생일
</Typography>
</InputAdornment>
) : null,
endAdornment: (
<InputAdornment position="end" sx={{ position: "relative" }}>
<CalendarIcon
sx={[
iconCSS,
{
color: errorType
? "var(--default-red-color)"
: "var(--gray-color-4)",
},
]}
/>
</InputAdornment>
),
}}
/>
);
}

DatePicker를 켰을 때, 선택된 날짜가 나타나는 상단부분이다
export default function CustomDPToolbar(
props: DatePickerToolbarProps<Dayjs>
) {
return (
<Box className={props.className}>
<DatePickerToolbar
toolbarFormat="YYYY년 MM월 DD일"
toolbarPlaceholder="값이 없습니다"
sx={{
backgroundColor: "var(--default-back-color)",
"& .MuiTypography-overline": {
color: "var(--default-caffein-color)",
fontSize: "var(--font-size-h6)",
},
"& .MuiDatePickerToolbar-title": {
color: "var( --gray-color-1)",
fontSize: "var(--font-size-h2)",
},
}}
{...props}
/>
</Box>
);
}

DatePicker 하단의 버튼부분이다.
이건 크게 바꾼 것도 없는데 왜? 따로 작성을 했을까 싶을 수 있는데... 기본 버튼이 영어라 한글(취소/확인)로 바꿔줘야했다.
export default function CustomDPActionbar(
props: PickersActionBarProps
) {
const { onAccept, onCancel } = props;
return (
<DialogActions className={props.className}>
<Button onClick={onCancel} sx={datePickerButtonStyle}>
취소
</Button>
<Button onClick={onAccept} sx={datePickerButtonStyle}>
확인
</Button>
</DialogActions>
);
}
이런 과정을 거치지 않았다면, 위에 적힌 모든 스타일을 비롯한 코드가 하나의 CustomDatePicker 컴포넌트 안에서 작성되어져야 했을 것이다.
하지만 이렇게 slot을 활용하여 따로 컴포넌트를 분리함으로서 관리가 훨씬 용이해졌다.
DatePicker를 커스텀 구현하는데는 색깔이나 포멧의 변화가 대부분이라 큰 문제가 없었지만...
https://mui.com/x/react-date-pickers/time-picker/

MobileTimePicker의 디자인이...어떻게 고쳐봐도 너무 아니였다

내가 원하는건 이런 형탠데..!
(사실 Desktop Varient를 사용하면 위와 같은 형태가 나타나긴 했지만 오른쪽 Clock버튼을 눌러야 열려서 모바일 웹에는 적합하지 않았다)
결국, 고심 끝에 자체 드롭다운 안에 MultiSectionDigitalClock을 담아서 사용했다

먼저 DigitalClock을 담을 dropdown을 만들었다.
컴포넌트 외부를 클릭했을때 드롭다운이 닫히도록 useState와 useRef를 활용했다.
클릭될때마다 콜백함수를 호출하여 클릭된 DOM이 ref에 저장된 DOM이 존재함과 동시에, 이 ref DOM 안에 포함되지 않는다면 state를 false로 바꾸어 닫히는 형식으로 구현했다.
이에 관해서는 많은 블로그자료가 있으니 쉽게 구현이 가능했다.
+)작성하진 못했지만 customHook으로 작성됐을 때 유용하게 쓰일 수 있다.
export default function CustomTimePicker({
value,
handleTimeChange,
}: CustomTimePickerProps) {
...
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const toggleDropdown = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
}
if (isOpen) {
// 렌더링 후 이벤트 리스너 등록
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
// 컴포넌트 언마운트 시 이벤트 리스너 제거 : Clean-up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
return (
<ThemeProvider theme={theme}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ko">
<div css={dropDownWrapperCSS} ref={wrapperRef}>
<div css={selectInputWrapperCSS} onClick={toggleDropdown}>
{formattedTime ? formattedTime : "설정된 시간이 없습니다"}
</div>
{isOpen ? (
<ArrowDropDownIcon
css={[inputIconCSS, { transform: "rotate(180deg)" }]}
/>
) : (
<ArrowDropDownIcon css={inputIconCSS} />
)}
{isOpen && (
<div css={optionWrapperCSS}>
<CustomDigitalClock
selectedTime={value}
onChange={handleTimeChange}
/>
</div>
)}
</div>
</LocalizationProvider>
</ThemeProvider>
);
}
https://mui.com/x/react-date-pickers/digital-clock/
export default function CustomDigitalClock({
selectedTime,
onChange,
}: DigitalClockProps) {
return (
<MultiSectionDigitalClock
value={selectedTime}
onChange={onChange}
// 10분 간격
timeSteps={{ minutes: 10 }}
// 선택할 수 있는 view들(meridiem은 오전/오후)
views={["meridiem", "hours", "minutes"]}
sx={digitalClockCSS}
/>
);
}
const digitalClockCSS = {
// 각 ul 태그 넓이
"& .MuiList-root": {
width: "33.3%",
padding: "0px",
// scrollbar
"&::-webkit-scrollbar": {
display: "none",
},
// selected Btn
"& .Mui-selected ": {
color: "black",
backgroundColor: "transparent",
borderBottom: "1px solid var(--gray-color-2)",
"&:hover": {
backgroundColor: "transparent",
},
},
},
"& .MuiButtonBase-root": {
width: "100%",
margin: "0px",
},
};
컴포넌트 클래스 네임 찾는게 좀 고역이었지만(무작정 개발자도구에서 찍어서 확인하기 말고도 방법이 있었을까?) 어쨌든 원하는 형태로 구현해냈다.