HTML-CSS

input 태그로 Date Picker 구현하기

GoJay 2024. 12. 4. 18:20

UI 컴포넌트 중 Date Picker를 구현하면서 삽질한 이런저런 내용을 기록해 둔다.

구현 요구사항

구현하고 싶었던 Date Picker는 이런 식이었다.

처음엔 input[type='date']로 설정하면 쉽게 구현할 수 있을 거라고 생각했다. 그런데, 막상 구현하다 보니 구현에 있어 몇 가지 허들이 있었다.

  • input[type='date']로 설정하면 input 텍스트 영역(날짜를 직접 입력하는 영역)에 '연도. 월. 일'이 기본 텍스트로 들어가 있었다. 그래서, placeholder로 원하는 텍스트를 넣어주는 게 어려웠다.
  • 브라우저에서 기본으로 제공하는 Date Picker Indicator는 원하는 모양으로 커스텀이 어려웠다.
  • 그리고, 아래로 떨어지는 Date Picker는 Date Picker Indicator 이미지를 눌러야만 확인이 됐는데, 사용자 경험 상 전체 input 태그 중 어디를 눌러도 Date Picker가 나오도록 하는 게 맞다고 생각했다.

결국, 브라우저에서 기본 제공되는 UI 기본값을 어떻게 제어할지에 대한 문제로 귀결된다고 생각했고, input[type='date']에 커스텀한 디자인을 입히기 위한 이런저런 공부와 시도들을 해봤다.

구현해 보기

브라우저에서 기본 제공되는 UI 속성들을 제거하고(연도, 월, 일 텍스트와 Date Picker Indicator), 그다음 원하는 스타일과 기능들을 입히는 식으로 할 계획을 가졌다.

Picker Indicator 속성 제어하기

먼저, 브라우저의 기본 속성을 관리해 주는 appearance를 사용해 봤다. MDN 문서에 따르면 '운영 체제의 테마에 따라 플랫폼 특유의 스타일로 UI 요소를 표시하는 데 사용된다'라고 안내돼 있다. 해당 속성을 사용하면 브라우저에서 기본으로 입혀주는 속성과 스타일들도 끌 수 있지 않을까 생각했다.

input[type='date'] {
  appearance: none;
}

하지만, 원하는 대로 Date Picker Input 요소가 제어되지 않았다. MDN 문서를 잘 읽어보면 appearance는 크로스 브라우징(Cross Brwosing) 이슈를 가진 속성이고, 구형 브라우저(CSS 표준이 적용되기 이전에 벤더사들만의 고유한 Prefix 속성을 사용하던 브라우저)에선 -moz-appearance-webkit-appearance 비표준 속성을 사용하라고 권장되어 있다.

관련해서 브라우저의 역사를 간단하게 찾아봤다. 과거에 여러 브라우저들은 통일된 HTML-CSS 표준을 적용하는 것이 아니라, 벤더사마다 각자의 커스텀한 속성을 브라우저에 적용했다. 이후 HTML-CSS의 표준이 정해진 이후에 나온 브라우저들은 통일된 규격을 따라 속성을 사용하게 권장되었지만, 과거 벤더사 고유의 커스텀한 속성을 사용하던 곳에선 아직도 일부 별도 접두어(Prefix)를 붙인 속성 사용이 필요한 상황이다.

대표적으로 Firefox 브라우저는 -moz라는 접두어를 사용한 별도 속성들이 정의되었고, Chrome, Edge, Safari 등은 -webkit 접두어를 사용했다. CSS에서 -webkit이라는 접두어를 쳐보면 굉장히 많은 속성들이 존재하는 게 확인된다(-moz도 마찬가지다). 해당 사실에 근거해서, appearance가 별도 접두어를 요구하는 일부 브라우저에서 동작하지 않기 때문에 발생하는 문제이지 않을까 싶어서, 아래와 같이 모든 브라우저에 대응할 수 있게 값을 변경해 봤다.

/* ... 다른 CSS 정의들 생략 */

input[type='date'] {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
}

역시 원하는 대로 동작하지 않았다.

좀 더 확인해 보니, appearance는 요소의 외형 스타일에 영향을 미치지만, 내부 가상 요소를 직접 제어하진 못한다가 한다. 여기서 말하는 '내부 가상 요소'란 UI 구성 요소에 포함된 '추가적인' 인터페이스 요소로, HTML 마크업으로 명시적으로 작성하지 않더라도 브라우저가 해동 요소를 렌더링 할 때 기본적으로 생성하여 제공하는 부분을 의미한다.

이러한 '내부 가상 요소'를 제어하기 위해선 '가상 요소 선택자'를 사용해야 하며, 특별히 Chrome-Edge(MS)-Safari에서 제공하는 -webkit에선 -webkit-calendar-picker-indicator라는 가상 선택자로 input[type='date'] 태그의 Picker Indicator를 제어할 수 있도록 제공해주고 있다.

input[type='date']::-webkit-calendar-picker-indicator {
  display: none;
}

Picker Indicator 아이콘이 잘 제거되었다.

Date Picker 메뉴 작동시키기

그런데, Picker Indicator를 없애고 나니, 아래와 같이 Date Picker 메뉴가 펼쳐지게 할 수 없다는 문제가 생겼다. input[type='date']에서 Picker Indicator 아이콘은 Date Picker 창을 열어주는 역할을 해주는데, 해당 아이콘을 없애니 트리거가 사라진 셈이다.

그래서, Indicator를 완전히 없애지는 말고, 단순히 '안 보이게' 처리해서 사용자에겐 존재하지 않는 것처럼 느껴지게 아래와 같이 처리해 봤다.

input[type='date']::-webkit-calendar-picker-indicator {
  opacity: 0;
}

눈에 보이는 Indicator 없이 Date Picker 메뉴를 나타낼 수 있었다. 물론, 이렇게만 하면 Indicator가 있는 보이지 않는 영역을 눌러야만 Date Picker가 열리기 때문에 굉장히 불편하다. 눈에 보이는 input 창 어디를 눌러도 Date Picker가 등장해야 사용자 입장해선 쉽게 날짜 선택을 할 수 있을 것이다.

해당 처리는 JavaScript를 통해 처리해야 한다. 아래는 Date Picker를 만들고 있는 컴포넌트에서 input 창 어디를 누르든 Date Picker가 보이게 처리해 준 코드이다. input 창에 onClick 이벤트 핸들러로 전달된 콜백 함수를 잘 확인하길 바란다.

export const DateInput = ({ inputTitle }: InputProps) => {
  return (
    <div className={styles.dateInputContainer}>
      <p className={styles.inputTitle}>{inputTitle}</p>
      <div>
        <input
          type='date'
          onClick={(e) => {
            e.currentTarget.showPicker();
          }}
        />
      </div>
    </div>
  );
};

input 창의 프로토타입인 HTMLInputElementshowPicker라는 메서드를 제공한다. 해당 메서드를 사용하면 굳이 Picker Indicator의 작은 영역을 누르지 않더라도, input창 어디를 눌러도 Date Picker를 보여준다.

이제 input 창 어디를 눌러도 Date Picker가 잘 확인된다.

커스텀 이미지 추가하기

브라우저에서 기본 제공하는 Picker Indicator가 아니라, 별도 디자인된 아이콘을 input창에 추가해 주기 위해 img 태그와 input 태그를 하나의 태그로 함께 감싸준다. 이때, div가 아니라 label 태그로 묶어주면 이미지 태그를 클릭해도 input 창을 클릭한 것과 마찬가지로 Date Picker 메뉴가 자동으로 보이게 된다.

<label>
  <img className={styles.calendarIcon} src={calendar} alt='calendar-image' />
  <input
    type='date'
    onClick={(e) => {
      e.currentTarget.showPicker();
    }}
  />
</label>

이때, img로 추가한 아이콘은 input 창 밖에 위치하게 된다. 만들려는 Date Picker는 아이콘이 input창 안에 위치해야 하기 때문에, label태그에 position: relative, img 태그에 position: absolute를 작성해 줬다. 그러면, 이렇게 label 영역의 input 안에 img 아이콘이 들어와 있는 게 확인된다.

label {
  display: relative;
}

label > img {
  position: absolute;
  width: 15px;
  height: 15px
}

이제 수동으로 값을 조절하여 아이콘과 input 태그 내부 요소의 위치를 조정해서 필요한 UI의 형태를 잡아준다.

label > input[type='date'] {
  /* ...다른 CSS 요소들 생략 */
  padding: 12px 16px 12px 40px;
}

label > img {
  position: absolute;
  width: 15px;
  height: 15px;
  margin-top: 12px;
  margin-left: 16px;
}

이제 제법 기대했던 Date Picker의 형태가 잡혔다.

placeholder 설정하기

만들면서 가장 까다로웠던 부분이 placeholder였다. 일단 가상 선택자 ::-webkit-datetime-edit을 사용하면 연도. 월. 일 부분의 텍스트 스타일을 조정하는 게 가능하단 건 확인했다.

/* 연도. 월. 일 텍스트 표시가 사라짐 */
input[type='date']::-webkit-datetime-edit {
  display: none
}

이를 사용하면 초기에 브라우저에서 기본 제공하는 텍스트 스타일로 제어할 수 있다. 하지만, 이러한 방식엔 몇 가지 문제가 있다. 먼저, -webkit-datetime-edit은 초기값만 적용되는 게 아니라, Date Picker로 날짜를 설정한 이후의 값도 같이 제어되기 때문에, display: none으로 하면 설정된 날짜가 보이지 않게 된다. 이를 제어하려면 자바스크립트의 이벤트 핸들러로 onFocus, onClick, onChange 등의 이벤트를 제어해 input 태그의 상태에 따라 표시를 다르게 해줘야 한다는 제약이 있었다.

두 번째로, input[type='date']input 태그에선 placeholder를 설정하는 별도의 속성이 없다. 따라서, 날짜를 표시하는 텍스트를 가리더라도 placeholder를 만들려면 자바스크립트로 span 태그를 만들어서 텍스트를 입력해 별도 스타일을 먹이고 position: absolute로 해서 수동으로 위치를 조절해야 한다. 그리고, input에 이벤트가 발생했을 때 placeholder로 스타일해둔 span태그를 없애고, onBlur로 포커스가 아웃되면 다시 화면에 보여주는 식으로 처리해야 한다.

텍스트로 설명해서 이해가 잘 안 될 수 있는데, 쉽게 얘기해서 굉장히 번거롭다는 뜻이다. 이런 식으로 구현하는 건 너무 소모적이라는 생각을 했고, 대신 input 창의 처음 typetext로 해서 placeholder를 보여주다가 onFocus 됐을 때 inputtypedate로 바꿔주는 정도로 처리해 봤다.

export const DateInput = ({ inputTitle, placeholder }: InputProps) => {
  // input의 type을 다루는 상태 정의
  const [type, setType] = useState('text');

  // label 태그로 focus 되면 input 태그의 타입 date로 변경
  const handleFocus = () => {
    setType('date');
  };

  // label 태그가 focus out 되면 input 태그의 타입 text로 변경
  const handleBlur = () => {
    setType('text');
  };

  return (
    <div className={styles.dateInputContainer}>
      <p className={styles.inputTitle}>{inputTitle}</p>
      <label onFocus={handleFocus} onBlur={handleBlur}>
        <img className={styles.calendarIcon} src={calendar} alt='calendar-image' />
        <input
          type={type}
          onClick={(e) => {
            e.currentTarget.showPicker();
          }}
          placeholder={placeholder}
        />
      </label>
    </div>
  );
};

힘들었지만, (거의) 비슷하게 Date Picker를 구현할 수 있었다.

결론

많은 걸 찾아가며 어렵게 만들어보긴 했지만, 결과론적으론 사용할 수 있는 Date Picker는 아닌 것 같다. 결정적으로 input의 타입을 상태로 정의하고, Focus 여부로 상태를 변경해서 사용하다 보니, 처음 클릭에선 type='text'로 타입을 변경시키고, 그다음 클릭으로 Date Picker 메뉴를 활성화시키는 절차가 필요했다. 즉, 날짜 선택을 하려면 input 창을 두 번 클릭해야 하는 문제가 있었다.

이 문제는 번거롭긴 하지만, 그래도 자바스크립트로 잘 제어해서(예를 들어 useEffecttype 변경 시 동작 제어) type 변경 시 Date Picker가 나오도록 하는 건 가능할 것 같다. 하지만, 더 큰 문제는 위에서 사용한 -webkit이 들어간 가상 선택자가 모든 브라우저에서 동일하게 호환되기 어렵다는 것이다. 사용자의 브라우저가 어떤 환경일지 모르는 상황에서, 이렇게 특정 브라우저에 취약한 방식으로 구현된 코드는 분명 문제가 될 수 있다.

그렇기 때문에, 결국 결론은 Date Picker를 손쉽게 만들 수 있는 라이브러리를 가져와 사용하자는 것이다. 찾아보니 다양한 라이브러리에서 Date Picker UI 기능을 지원하고 있으며, 해당 라이브러리 중 하나를 선택해서 쓰는 게 좀 더 안전한 방식이다. 이에 대해선 포스트가 길어져서, 별도 포스팅에서 다뤄볼 예쩡이다.

결론이 좀 허무하지만, 아무튼 CSS에 대해 많은 걸 공부할 수 있어서 뜻깊었다. 끝.