프로젝트 내부에서 Date Time Picker 적용하기

kkojae·2022년 10월 27일
1

줍줍 프로젝트는 슬랙 메시지를 아카이빙하는 서비스입니다.
프로젝트 github

DatePicker, TimePicker가 필요한 이유


저희 서비스는 슬랙메시지를 아카이빙 해주는 서비스입니다.

단순히 메세지를 아카이빙 해주는 것 보다는 사용자 입장에서 메세지를 Reminde 할 수 있는 기능이 추가되면 좋지 않을까?! 라는 생각에서 부터 시작할 수 있었습니다.

이전에 받았던 메시지 중 몇 일 후 혹은 몇 주에서 몇 달 후 다시 Reminde 할 수 있도록 설정하는 기능이 추가된다면 사용자 입장에서 잊어버리면 안되는 중요한 약속이나 정보를 다시 확인할 수 있을 것 같다는 생각을 하게 되었습니다.

따라서 사용자가 메시지 알람을 설정할 수 있도록 모달창이 필요해졌고, 여기에 DatePicker와 TimePicker를 만들어 날짜와 시간을 선택할 수 있도록 구현했습니다.

구현 내용


1. 먼저 가장 상위 컴포넌트인 ReminderModal 컴포넌트

AddReminder 안의 주요한 컴포넌트인 Date Time Picker를 우선 Date Picker, Time Picker로 분리해서 관리해주고 있습니다. DatePicker의 비즈니스 로직은 useDatePicker로 TimePicker의 비즈니스 로직은 useTimePicker로 컴포넌트의 복잡도를 낮추며 관심사의 분리를 해주게 되었습니다. 라이브러리 없이 구현하다보니 요소를 직접 컨트롤해야하는 부분들이 다수 존재해 return value도 많고, props로 전달되는 상태들도 많아지게 되었습니다.

DatePicker와 TimePicker의 내부 동작은 동일하기때문에 이번 글에서는 DatePicker에 대해서 설명하도록 하겠습니다.

function AddReminder({
	// some code...
}: Props) {
  const {
    yearRef,
    monthRef,
    dateRef,
    checkedYear,
    checkedMonth,
    checkedDate,
    handleChangeYear,
    handleChangeMonth,
    handleChangeDate,
    handleResetDatePickerPosition,
  } = useDatePicker({ remindDate });

  const {
		// some code...
  } = useTimePicker({ remindDate });

	// some code ...

  return (
    <Styled.Container>
      <Styled.Title>리마인더 생성</Styled.Title>

      <DatePicker
        yearRef={yearRef}
        monthRef={monthRef}
        dateRef={dateRef}
        checkedYear={checkedYear}
        checkedMonth={checkedMonth}
        checkedDate={checkedDate}
        handleChangeYear={handleChangeYear}
        handleChangeMonth={handleChangeMonth}
        handleChangeDate={handleChangeDate}
        handleResetDatePickerPosition={handleResetDatePickerPosition}
      />

      <TimePicker
				// some code...
      />
			
			{/** some code... */}

    </Styled.Container>
  );
}

export default ReminderModal;

useMutateReminder 커스텀 훅으로 분리한 로직은 Reminder를 생성, 수정, 삭제할 수 있는 부분을 분리하여 각 상황에 맞는 버튼에 handler를 붙여줘 각 버튼의 역할을 할 수 있도록 버튼을 만들어줬습니다.

function ReminderModal({
  messageId,
  remindDate,
  handleCloseReminderModal,
  refetchFeed,
}: Props) {
	// some code...

  const { handleCreateReminder, handleModifyReminder, handleRemoveReminder } =
    useMutateReminder({ handleCloseReminderModal, refetchFeed });

  return (
    <Styled.Container>
			{/** some code... */}

      <FlexRow justifyContent="flex-end" gap="8px" marginTop="18px">
        <Styled.Button
          text="취소"
          type="button"
          onClick={handleCloseReminderModal}
        >
          취소
        </Styled.Button>

        {!remindDate && (
          <Styled.Button
            text="생성"
            type="button"
            onClick={() =>
              handleCreateReminder({ /* some code... */})
            }
          >
            생성
          </Styled.Button>
        )}

        {remindDate && (
          <Styled.Button
            text="삭제"
            type="button"
            onClick={() => handleRemoveReminder(messageId)}
          >
            삭제
          </Styled.Button>
		)}

		{remindeDate && (
		  <Styled.Button
              text="수정"
              type="button"
              onClick={() =>
            	handleModifyReminder({/* some code... */})
              }
          	>
              수정
          </Styled.Button>
        )}
      </FlexRow>
    </Styled.Container>
  );
}

export default ReminderModal;

2. DatePicker custom hook 내부를 살펴 보자.

먼저 커스텀 훅에서 사용하는 상태는 아래 코드 조각과 같습니다.

getDateInformation으로 현재 년, 월, 일을 찾아주고, 년, 월, 일 엘리먼트를 각 각 yearRef, monthRef, dateRef로 관리를 해줬습니다.

그리고 마지막으로 dateDropdownFlag를 통해 dropdown 메뉴가 열리고 닫혀있는 상태를 관리할 수 있도록 설정 해줬습니다.

function useDatePicker({ remindDate }: Props) {
  const { year, month, date } = getDateInformation(new Date());
  const yearRef = useRef<HTMLDivElement>(null);
  const monthRef = useRef<HTMLDivElement>(null);
  const dateRef = useRef<HTMLDivElement>(null);

  const [dateDropdownFlag, setDateDropdownFlag] = useState(false);

  // some code ...
}

export default useDatePicker;

다음 상태 관리로는 useInput 커스텀 훅을 통해 년, 월, 일에 대한 input radio의 값을 상태로 관리할 수 있도록 설정해줬습니다.

function useDatePicker({ remindDate }: Props) {
	// some code ...
  const {
    value: checkedYear,
    handleChangeValue: handleChangeYear,
    changeValue: changeYear,
  } = useInput<number>({
    initialValue: year,
  });

  const {
    value: checkedMonth,
    handleChangeValue: handleChangeMonth,
    changeValue: changeMonth,
  } = useInput<number>({ initialValue: month });

  const {
    value: checkedDate,
    handleChangeValue: handleChangeDate,
    changeValue: changeDate,
  } = useInput<number>({
    initialValue: date,
  });

  const handleResetDatePickerPosition = () => {
    setDateDropdownFlag((prev) => !prev);
  };

  // some code...
}

export default useDatePicker;

마지막으로 useEffect 훅을 사용한 로직을 확인해보면,

첫 번재 useEffect는 해당하는 ref 요소가 클릭 될 때마다 정해진 높이를 계산해 스크롤의 위치를 변경함으로 써 클릭 된 요소를 중앙으로 배치 해주는 코드입니다.

설명과 코드만 보면 어떤 동작을 하는지 예측하기 어려운 부분이 있어 해당하는 부분은 GIF를 통해 확인해보시는게 좋을 것 같아 GIF를 먼저 보고 코드를 확인해보겠습니다.

GIF에서 확인할 수 있듯 07월을 클릭하게 되었을 때 해당하는 요소의 위치로 스크롤을 이동시키는 로직입니다.

function useDatePicker({ remindDate }: Props) {
	// some code...

  useEffect(() => {
    if (yearRef.current) {
      yearRef.current.scrollTo({
        top: (checkedYear - year) * PICKER_OPTION_SCROLL.HEIGHT,
        behavior: PICKER_OPTION_SCROLL.BEHAVIOR,
      });
    }

    if (monthRef.current) {
      monthRef.current.scrollTo({
        top: (checkedMonth - 1) * PICKER_OPTION_SCROLL.HEIGHT,
        behavior: PICKER_OPTION_SCROLL.BEHAVIOR,
      });
    }

    if (dateRef.current) {
      dateRef.current.scrollTo({
        top: (checkedDate - 1) * PICKER_OPTION_SCROLL.HEIGHT,
        behavior: PICKER_OPTION_SCROLL.BEHAVIOR,
      });
    }
  }, [checkedYear, checkedMonth, checkedDate, dateDropdownFlag]);
	
	// some code...
}

export default useDatePicker;

두번째 useEffect는 remindDate가 변경되었을 경우 해당하는 date 값으로 input 값을 변경해주는 코드이다.

function useDatePicker({ remindDate }: Props) {
	// some code...

  useEffect(() => {
    if (remindDate) {
      const { year, month, date } = parseDate(remindDate);

      changeYear(year);
      changeMonth(month);
      changeDate(date);
    }
  }, [remindDate]);

	// some code...
}

export default useDatePicker;

3. 마지막으로 DatePicker 컴포넌트 내부를 확인해보자.

DatePicker에서는 DateTimePickerToggle, DateTimePickerOptions 컴포넌트로 구성되어 있습니다.

DateTimePickerToggle에는 선택된 Remind Date가 보여지고, DateTimePickerOptions 컴포넌트에는 Date의 경우 년, 월, 일을 선택할 수 있는 radio 버튼들이 존재합니다.

function DatePicker({
	// some code ...
}: Props) {
  const theme = useTheme();

  return (
    <Dropdown toggleHandler={handleResetDatePickerPosition}>
      {({ innerRef, isDropdownOpened, handleToggleDropdown }) => {
        const { years, months, dates } = getFutureDateOption();

        return (
          <FlexColumn marginBottom="10px" ref={innerRef}>
            <Styled.Subtitle>언제</Styled.Subtitle>

            <DateTimePickerToggle
              text={`
                ${parsePickerOptionText({
                  optionText: checkedYear,
                  unit: TIME_UNIT.YEAR,
                })} 
                ${parsePickerOptionText({
                  optionText: checkedMonth,
                  unit: TIME_UNIT.MONTH,
                })}
                ${parsePickerOptionText({
                  optionText: checkedDate,
                  unit: TIME_UNIT.DATE,
                })}
              `}
              handleToggleDropdown={handleToggleDropdown}
            >
              <CalendarIcon
                width="16px"
                height="16px"
                fill={theme.COLOR.SECONDARY.DEFAULT}
              />
            </DateTimePickerToggle>

            {isDropdownOpened && (
              <Styled.TextOptionContainer>
                <Styled.TextOptionsWrapper ref={yearRef}>
                  <DateTimePickerOptions
                    optionTexts={years}
                    unit={TIME_UNIT.YEAR}
                    checkedText={checkedYear}
                    handleChangeText={handleChangeYear}
                  />
                </Styled.TextOptionsWrapper>

                <Styled.TextOptionsWrapper ref={monthRef}>
                  <DateTimePickerOptions
                    optionTexts={months}
                    unit={TIME_UNIT.MONTH}
                    checkedText={checkedMonth}
                    handleChangeText={handleChangeMonth}
                  />
                </Styled.TextOptionsWrapper>

                <Styled.TextOptionsWrapper ref={dateRef}>
                  <DateTimePickerOptions
                    optionTexts={dates}
                    unit={TIME_UNIT.DATE}
                    checkedText={checkedDate}
                    handleChangeText={handleChangeDate}
                  />
                </Styled.TextOptionsWrapper>
              </Styled.TextOptionContainer>
            )}
          </FlexColumn>
        );
      }}
    </Dropdown>
  );
}

export default DatePicker;

위와 같은 방식으로 TimePicker도 구현해줘 최종적으로 AddReminder 컴포넌트가 완성되게 되었습니다.

최종적으로 구현된 AddReminder 컴포넌트 동작을 확인하며 이 글을 마무리하려 합니다.

0개의 댓글