[CryptoMeter] react-datepicker 라이브러리 사용기

1

CryptoMeter

목록 보기
5/6
post-thumbnail

스타일 시스템 구축과 인터페이스 설계, 공통 컴포넌트제작을 마치고 다음 마일스톤인 컴포넌트 조립 및 화면 구성을 진행했다. 나의 역할은 InputBoard 컴포넌트를 만드는 것이었다. 이 컴포넌트에는 다음과 같이 여러 종류의 input이 들어가게 되는데, 이번 포스팅에서는 날짜를 선택하는 input을 react-datepicker 라이브러리를 사용하여 만든 기록을 소개해보고자 한다.

react-datepicker

사용법

기본적인 사용법

  • 설치
    npm install react-datepicker --save
  • 사용
    • 아래와 같이 DatePicker의 selectedDate가 input에 display될 날짜이다. 또한 onChange props로 함수를 넘겨주어 선택한 날짜를 display할 날짜로 세팅해줄 수 있다. 희한한 점은 css파일도 함께 import해줘야 한다는 점이다. 필자는 처음에 이걸 몰라서 달력 모양이 아무 스타일 없이 세로로 길게 뻗어있는 것을 보고 충격에 휩싸이기도 했다.
      ...
      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}
    }
  }
`;

DatePicker 동작 커스텀하기

외부 div클릭 시 focus주기

디자인은 완성되었는데, 이 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>
...

step 1. 가장 바깥 div에 onClick 핸들러 등록

S.InputWrapper(div요소)를 클릭하면 다음과 같은 동작이 필요하다.

  • 드롭다운 핸들이 회전한다(input이 열려있음을 사용자에게 알려주는 용도)
  • 달력이 pop된다 (DatePicker가 focus됨)
  • 준비물
    • isOpen 상태
    • DatePicker를 참조하는 ref변수
    • S.InputWrapper에 등록할 handleClickInputWrapper 핸들러 함수

다음과 같이 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();
  }
};
...

step 2. 달력 클릭 시 선택적으로 isOpen 토글시키기

이 부분이 어려웠는데, 문제는 달력의 내용을 클릭할 때도 이벤트 버블링으로 인해 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자체도 시간이 된다면 커스텀해볼 에정이다.

1개의 댓글

comment-user-thumbnail
2023년 5월 21일

쩌네욤.

답글 달기