🚀 프로젝트/따꼼

따꼼 | 날짜선택 Form을 Shadcn의 Date Picker로 교체 구현 및 관련 오류 해결

seheej 2024. 11. 26. 19:59

 

기존 코드

프로필 수정 폼에서 생년월일 입력 필드는 단순히 날짜를 텍스트로 입력받는 방식이었습니다. 이는 사용자 경험이 다소 불편할 수 있어, 달력 컴포넌트(Calendar)를 사용하여 날짜를 선택하는 방식으로 개선했습니다.

{/* 생년월일 입력 필드 */}
<FormField
  control={form.control}
  name="birth"
  render={({ field }) => (
    <FormItem>
      <FormLabel className="text-gray-800">생년월일</FormLabel>
      <FormControl className="text-gary-700 px-6 py-4 rounded-xl">
        <Input className="h-full text-text-xl" type="date" {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

 

수정된 코드

날짜 입력 필드를 개선하기 위해 PopoverCalendar 컴포넌트를 사용했습니다. 달력에서 날짜를 선택하면 입력값이 자동으로 업데이트되며, 포맷팅된 날짜가 표시됩니다.

<FormField
  control={form.control}
  name="birth"
  render={({ field }) => (
    <FormItem>
      <FormLabel className="text-gray-800">생년월일</FormLabel>
      <Popover>
        <PopoverTrigger asChild>
          <FormControl>
            <Button
              variant={"outline"}
              className={cn(
                "w-full h-full text-text-xl text-left text-gray-800",
                !field.value && "text-muted-foreground"
              )}
            >
              {field.value ? format(field.value, "PPP") : <span>날짜 선택</span>}
              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
            </Button>
          </FormControl>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0" align="start">
          <Calendar
            mode="single"
            selected={field.value}
            onSelect={field.onChange}
            disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
            initialFocus
          />
        </PopoverContent>
      </Popover>
      <FormMessage />
    </FormItem>
  )}
/>

 

주요 변경 사항

  1. 사용자 경험 개선:
    • Calendar와 Popover를 활용하여 날짜를 선택할 수 있는 UI를 구현했습니다.
    • 선택한 날짜는 date-fns의 format 함수로 포맷팅되어 "PPP" 형식(예: "March 10th, 2024")으로 표시됩니다.
  2. 코드 간결화:
    • 날짜 선택을 위한 버튼과 달력을 구성해 불필요한 텍스트 입력 대신 직관적인 인터페이스 제공합니다.
  3. 유효성 검증:
    • 달력에서 오늘 이후의 날짜와 1900년 이전의 날짜는 비활성화하여 유효하지 않은 입력을 방지합니다.

 


오류 발생 1

1. 문제 설명: selected 속성 타입 불일치

  • Calendar 컴포넌트의 selected 속성에 string 타입 값이 전달되어 다음과 같은 타입 오류가 발생했습니다.
Type 'string' is not assignable to type 'Matcher | Matcher[] | undefined'.ts(2322)
The selected day.

 

2. 원인 분석

 

  • Calendar 컴포넌트의 selected 속성은 반드시 Date 객체를 받아야 합니다.
  • 하지만 field.value는 string 값으로 설정되어 있어 타입 불일치 문제가 발생했습니다.

3. 해결 방법: Date 객체로 변환

  • field.value를 Date 객체로 변환하여 selected 속성에 전달합니다.
  • 선택한 날짜를 onSelect 핸들러를 통해 Date 타입으로 처리합니다.

오류 발생 2

1. 문제 설명: 선택한 날짜가 하루 전날로 표시되는 문제

  • 캘린더에서 날짜를 선택했을 때, 표시되는 값이 하루 전날로 나타나는 문제가 발생했습니다. 이는 toISOString과 관련된 시간대(Timezone) 설정 문제로, 선택한 날짜가 UTC로 변환되면서 발생합니다.

2. 원인 분석

 

  • toISOString 메서드는 UTC 기준 시간을 반환합니다.
  • 예를 들어, 한국 시간(UTC+9)에서 2024-11-25를 선택하면 내부적으로 2024-11-25T00:00:00.000+09:00으로 저장됩니다. 이를 toISOString으로 변환하면 2024-11-24T15:00:00.000Z로 변환되어 날짜가 하루 전으로 보이는 문제가 생깁니다.

3. 해결 방법: 로컬 시간대 반영

  • toISOString 대신 date-fns 라이브러리의 format 메서드를 사용해 YYYY-MM-DD 형식으로 날짜를 처리합니다.
  • 시간대 문제를 해결하며, 사용자에게 로컬 시간대 기준 날짜를 정확히 표시할 수 있습니다.

수정된 코드

<FormField
  control={form.control}
  name="birth"
  render={({ field }) => (
    <FormItem>
      <FormLabel className="text-gray-800">생년월일(필수)</FormLabel>
      <Popover>
        <PopoverTrigger asChild>
          <FormControl className="text-gary-700 px-6 py-4 rounded-xl">
            <Button
              variant={"outline"}
              className={cn(
                "w-full h-full text-text-xl text-left text-gray-800",
                !field.value && "text-muted-foreground"
              )}
            >
              {field.value ? (
                format(new Date(field.value), "yyyy-MM-dd") // 선택된 날짜 포맷
              ) : (
                <span className="text-gray-800">생년월일을 선택해주세요.</span>
              )}
              <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
            </Button>
          </FormControl>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0" align="start">
          <Calendar
            mode="single"
            selected={field.value ? new Date(field.value) : undefined} // `Date` 타입 변환
            onSelect={(date) =>
              field.onChange(date ? format(date, "yyyy-MM-dd") : undefined) // 로컬 시간대로 처리
            }
            disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
            initialFocus
          />
        </PopoverContent>
      </Popover>
      <FormMessage />
    </FormItem>
  )}
/>

 

 

 

 

주요 변경 사항

  1. Date 객체로 변환:
    • field.value ? new Date(field.value) : undefined를 통해 string 값을 Date 객체로 변환.
  2. format 사용:
    • 날짜 선택 시 format(date, "yyyy-MM-dd")을 사용해 사용자에게 로컬 시간대 기준 날짜를 표시.
  3. onSelect 핸들러:
    • 선택된 날짜를 처리할 때 Date 객체로 저장하여 타입과 시간대 문제를 동시에 해결.

 

오류 해결 과정에서 배운 점

타입스크립트의 타입 엄격성 이해

  • TypeScript는 컴포넌트의 속성 타입을 엄격히 검사하여 개발 중에 발생할 수 있는 런타임 에러를 사전에 방지합니다. 이번 오류를 통해 타입 선언이 잘못되었을 때 TypeScript가 얼마나 유용하게 문제를 알려주는지 다시금 깨달았습니다.
  • 특히, 외부 라이브러리(shadcn)의 컴포넌트를 사용할 때, 공식 문서를 꼼꼼히 읽고 올바른 타입을 전달해야 한다는 점을 배웠습니다.

시간대(Timezone) 관련 문제 이해

  • 브라우저가 기본적으로 로컬 시간대를 기반으로 하지만, 일부 메서드(toISOString)는 UTC를 기준으로 동작한다는 점을 알게 되었습니다.
  • 이로 인해 날짜가 변경되는 문제가 발생할 수 있음을 경험하며, 날짜/시간 데이터를 다룰 때 시간대가 중요한 요소임을 깨달았습니다.

협업 도구와 문서화의 중요성

  • 이번 오류 해결 과정은 단순히 문제를 해결하는 것뿐만 아니라, 해결 과정을 정리하고 문서화하는 것이 나중에 팀원들과 지식을 공유하거나 유사한 문제를 해결할 때 큰 도움이 될 수 있음을 느꼈습니다.