스타일 시스템 구축과 인터페이스 설계, 공통 컴포넌트제작을 마치고 다음 마일스톤인 컴포넌트 조립 및 화면 구성을 진행했다. 나의 역할은 InputBoard 컴포넌트를 만드는 것이었다. 이 컴포넌트에는 다음과 같이 여러 종류의 input이 들어가게 되는데, 이번 포스팅에서는 날짜를 선택하는 input을 react-datepicker 라이브러리를 사용하여 만든 기록을 소개해보고자 한다.
npm install react-datepicker --save
...
import "react-datepicker/dist/react-datepicker.css"; // 중요!!!
() => {
const [startDate, setStartDate] = useState(new Date());
return (
<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} />
);
};
공식 문서를 참고하면 더욱 다양한 상황에서의 사용법을 배울 수 있다.
InputBoard의 DateInput 컴포넌트의 경우 아래와 같이 디자인을 먼저 완성해 놓았다. 하지만 아래의 디자인은 input 태그로 구현한 것이 아니라 그냥 div로 된 아무 기능없는 껍데기일 뿐이었다.
이제 datepicker를 사용해서 커스텀을 진행해보자.
먼저, DatePicker 컴포넌트가 어디에 들어가야 할 지부터 고민을 했는데, 크게 감싸고 있는 div의 자식으로 들어가면 좋겠다고 생각했다. 참고로 위에 사진에 보이는 날짜는 span태그이다. 이 span태그 대신에 DatePicker가 들어가게 되고, 폰트나 색상은 전부 기존 span태그와 똑같이 만들어주면 된다.
그런데 DatePicker라는 컴포넌트에 디자인을 어떻게 입힐 지 몰라 요소 검사를 하다가 input태그가 다음과 같이 내부에 들어가 있다는 것을 알았다.
이 클래스명을 가지고 디자인을 입히는 것은 조금 아닌것 같아 그냥 styled component로 DatePicker를 감싸고 다음과 같이 스타일을 입혀주었다.
// DateInput.jsx
<S.DatePickerWrapper>
<DatePicker
ref={datePickerRef}
selected={displayDate}
dateFormat="yyyy년 MM월 dd일"
onChange={handleChangeDate}
/>
</S.DatePickerWrapper>
...
S.DatePickerWrapper = styled(SDiv)`
input {
${b1} // 미리 정의해둔 css문이다. font-size: xxpx;과 같다고 보면 됨.
${white}
${disableSelect}
background: transparent;
border: none;
@media only screen and (max-width: 768px) {
${g9}
}
}
`;
디자인은 완성되었는데, 이 input을 감싸고 있는 조상 div를 클릭했을 때 달력이 나와야 한다. DatePicker컴포넌트는 다음과 같이 외부 div들에게 감싸져 있다.
// DateInput.jsx
...
<S.InputWrapper onClick={handleClickInputWrapper}>
<SDiv row sb>
<S.DatePickerWrapper>
<DatePicker
ref={datePickerRef}
selected={displayDate}
dateFormat="yyyy년 MM월 dd일"
onChange={handleChangeDate}
/>
</S.DatePickerWrapper>
<SDiv ct>
{isTablet ? (
<DropdownHandleIcon
w={12}
h={12}
fill={colors.black}
rotate={isOpen.toString()}
/>
) : (
<DropdownHandleIcon rotate={isOpen.toString()} />
)}
</SDiv>
</SDiv>
</S.InputWrapper>
...
S.InputWrapper(div요소)를 클릭하면 다음과 같은 동작이 필요하다.
다음과 같이 InputWrapper를 클릭했을 때, open, close상태가 토글되게 한다.
const [isOpen, setIsOpen] = useState(false);
const datePickerRef = useRef(null);
...
const handleClickInputWrapper = (e) => {
// isOpen 상태를 토글하고, isOpen에 따라 datepicker에 focus, blur를 부여
setIsOpen((prev) => !prev);
if (datePickerRef.current) {
if (isOpen) {
datePickerRef.current.setBlur();
return;
}
datePickerRef.current.setFocus();
}
};
...
이 부분이 어려웠는데, 문제는 달력의 내용을 클릭할 때도 이벤트 버블링으로 인해 handleClickInputWrapper가 동작한다는 것이었다. 일반적으로 날짜를 선택하면 잘 동작한다. 날짜를 클릭함과 동시에 input이 blur되면서 입력창이 닫히고 핸들리 다시 180도 회전하는 것이 자연스럽니다. 하지만 달력의 내용중에 다음 월, 이전 월로 가는 버튼을 클릭할 때 blur는 되지 않지만 내부적으로 isOpen이 바뀌면서 핸들이 돌아가는 현상이 발생한다.
가장 먼저 생각한 것은 stopPropagation함수였다. 하지만 버블링을 모두 막아버리면 날짜를 선택했을 때 handleClickInputWrapper가 아예 작동하지 않는 문제가 있다.
결국 클릭 이벤트가 발생한 타겟을 검사하여 특정 타겟에서 발생한 클릭에는 핸들러가 동작하지 않게 조건을 걸어주는 방식으로 해결하였다.
/**
* 자식과 부모 노드를 넘겨서 넘겨준 부모 노드의 자식이 맞는지 리턴하는 함수입니다.
* @param {string} parentClassName 부모 노드의 클래스 명
* @param {DOMObject} child 자식노드
* @returns {boolean} 자식노드가 부모 노드의 자존이 맞는지 리턴
*/
const isDescendant = (parentClassName, child) => {
const parent = document.querySelector(`.${parentClassName}`);
let node = child.parentNode;
while (node != null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
};
...
...
const handleClickInputWrapper = (e) => {
// datepicker UI 내에서 header와 navigation을 클릭했을 때는 isOpen상태를 바꾸면 안됨
if (
isDescendant("react-datepicker__header", e.target) ||
e.target.classList.contains("react-datepicker__navigation-icon") ||
e.target.classList.contains("react-datepicker__navigation")
) {
return;
}
// isOpen 상태를 토글하고, isOpen에 따라 datepicker에 focus, blur를 부여
setIsOpen((prev) => !prev);
if (datePickerRef.current) {
if (isOpen) {
datePickerRef.current.setBlur();
return;
}
datePickerRef.current.setFocus();
}
};
...
위와 같이 handleInputWrapper함수에 조건을 걸어 event가 발생한 타겟이 특정 영역이라면 return하게 했다.
아래와 같이 원하는 대로 잘 동작한다!
다음에는 달력 UI자체도 시간이 된다면 커스텀해볼 에정이다.
쩌네욤.