Input 자동 X 아이콘과 초기화: type="search"로 간편하게 관리하기

최소희·2025년 1월 5일
0

프론트엔드 학습

목록 보기
22/23
post-thumbnail

사내에서 디자인 시스템 중에는 TextInput 컴포넌트에 “사용자가 텍스트를 입력하면 X 아이콘이 자동으로 보이고, 이 아이콘을 클릭하면 인풋이 비워지는 기능”이 있다.

처음에는 별도의 자바스크립트 로직으로 직접 아이콘을 표시하고, 클릭 이벤트를 연결해 value를 초기화하는 방안을 떠올렸다.

그러나 제어(Controlled)와 비제어(Uncontrolled) 상황을 모두 고려하다 보니, 분기 처리가 꽤나 복잡해졌다.

예를 들어, 해당 컴포넌트에 내려주는 값을 비제어로 내려주는지, 제어로 내려주는지에 따른 모든 케이스들을 다 고려하여 value는 빈 값으로 변경해줘야하기 때문이다.

그런데 알고 보니 type="search"라는 HTML 속성만 써도 이 모든 일을 브라우저가 알아서 처리해준다는 사실을 발견했다.

추가 로직 없이도 X 아이콘이 생기고, 클릭하면 인풋 값이 자동으로 비워지니 한결 마음이 편해졌다.

이 글에서는 “기존 JS 로직 버전”과 “type="search" 버전”을 차례로 살펴보면서,
왜 최종적으로 type="search"가 제어/비제어 인풋 모두에서 더 단순하게 동작하는지 알아본다.

비포(Before) 코드: 자바스크립트로 직접 X 아이콘 제어

제어 컴포넌트와 비제어 컴포넌트 개념이 익숙하지 않다면, 아래 글을 살펴본 후 이어서 읽기를 권장드린다.

제어 컴포넌트와 비제어 컴포넌트 제대로 이해하기 (feat. React Hook Form)

1. 제어 vs. 비제어 분기 처리 예시

아래 코드는 “제어와 비제어 상황을 모두 대응해야 한다”는 가정하에 작성된 간단한 예시다.

현실적인 코드에서는 state와 props가 더 다양하고, 조건부 스타일이나 이벤트 핸들러도 늘어나 복잡도가 더 높아질 수 있다.

(이 코드는 완전히 구현된 코드가 아니다. 모든 분기를 처리하면서 커지는 코드의 복잡성을 이해하는 용도로만 참고하길 바란다.)

import React, { useRef, useState, forwardRef, Ref } from 'react';

interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

interface MyTextInputProps extends TextInputProps {
  controlled?: boolean;
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
}

const MyTextInput = forwardRef<HTMLInputElement, MyTextInputProps>(({
  controlled = false,
  value = '',
  defaultValue = '',
  onChange,
  ...props
}, ref: Ref<HTMLInputElement>) => {
  // 비제어일 경우 내부적으로 관리할 값
  const [internalValue, setInternalValue] = useState(defaultValue);

  // DOM 접근을 위한 ref
  const inputRef = useRef<HTMLInputElement>(null);

  const currentValue = controlled ? value : internalValue;

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (controlled) {
      // 상위에서 제어하는 경우, 상위 콜백 호출
      onChange?.(e.target.value);
    } else {
      // 비제어인 경우 내부 state 갱신
      setInternalValue(e.target.value);
    }
  };

  const handleClearClick = () => {
    if (controlled) {
      onChange?.('');  // 제어 상태에서는 상위로 빈 문자열 전달
    } else {
      setInternalValue('');  // 비제어 상태에서는 내부 state 초기화
      // 추가로 DOM의 value도 초기화할 수 있음
      if (inputRef.current) {
        inputRef.current.value = '';
      }
    }
  };

  return (
    <div style={{ position: 'relative' }}>
      <input
        ref={inputRef}
      	value={currentValue}
        onChange={handleChange}
        {...props}
      />
      {currentValue && (
        <span
          onClick={handleClearClick}
          style={{
            position: 'absolute',
            right: 0,
            top: 0,
          }}
        >
          X
        </span>
      )}
    </div>
  );
}

MyTextInput.displayName = 'MyTextInput';                                                                                                                    
export default MyTextInput;

이 코드의 문제점

  • 조건 분기: 제어인지 비제어인지 판별 후 상, 하위 로직이 달라져야 한다.
  • 이벤트 처리: onChange와 X 아이콘 클릭, DOM 값 초기화 등이 나뉘어 있어 가독성이 떨어진다.
  • 추가 기능: 디자인 변경, 아이콘 이미지 교체 등 UX 요구가 생길 때마다 로직이 추가된다.

개발자가 스스로 X 아이콘과 이벤트를 모두 관리해야 하므로 “버그가 생길 여지”가 많고 “코드가 장황해지는” 경향이 있다.

type="search" 버전: 브라우저에서 자동 처리

위와 같은 고민을 한 번에 해결해 주는 방법이 바로 type="search"다.

HTML5 표준에 따르면, input[type="search"]는 검색어 입력 필드로 인식되어, 입력값이 있을 때 자동으로 X 아이콘을 띄우고, 클릭하면 인풋을 빈 문자열로 초기화한다.

이때도 onChange 이벤트가 트리거되므로, React 제어/비제어 인풋 모두 자연스럽게 동기화된다.

import React, {forwardRef, Ref } from 'react';

interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {}


const MyTextInput = forwardRef<HTMLInputElement, TextInputProps>(({
  ...props
}, ref: Ref<HTMLInputElement>) => {
 
  return (
      <input
        type='search'
		ref={ref}
        {...props}
      />
  );
});

MyTextInput.displayName = 'MyTextInput';

export default MyTextInput;

위처럼 작성하면, 입력값이 있을 때 브라우저가 알아서 X 아이콘을 띄우고, 클릭 시 value=""로 지워준다.

별도의 JS 이벤트나 DOM 조작 코드를 짤 필요가 없다.

input type example

검색어 입력과 삭제가 어떻게 이뤄지는지는 위 이미지로 확인할 수 있다.

type="search"는 내부적으로 어떻게 동작할까?

1. 기본 브라우저 스타일

HTML5 스펙에 따르면, input[type="search"]는 "검색 필드"라는 의미를 갖는다. 이를 인지한 브라우저들은 사용자 경험을 돕기 위해 다음과 같은 요소를 기본 제공한다.

  • 입력값이 존재할 때만 X 아이콘이 나타남
  • X 아이콘 클릭 시 value를 비운 뒤, onChange 이벤트를 발생시킴

브라우저가 자체적으로 이 아이콘들을 렌더링하며, ::-webkit-search-cancel-button 등 CSS pseudo-element(의사 요소) 를 통해 어느 정도 스타일링할 수 있다.


┌─────────────────────────────────┐
│  [검색어 입력]         ( X )   │
└─────────────────────────────────┘

위 그림은 대략적인 구조를 나타낸 것으로, 오른쪽에 X 아이콘이 표시되어 입력값이 있을 때 클릭하면 내용을 지운다.

2. 값 변경 및 이벤트 트리거

사용자가 X 아이콘을 클릭하면, 브라우저가 내부적으로 해당 input의 value를 빈 문자열("")로 바꾼다.

이때 onChange 이벤트도 자연스럽게 트리거되어, 제어(Controlled) 상태라면 setState를 통해 빈 문자열이 반영되고, 비제어(Uncontrolled) 상태라면 DOM 자체의 value가 비워진다.

왜 충돌이 없을까?

  • 제어 컴포넌트에서는 브라우저가 value""로 변경 →입력 필드의 값이 변경되어, input 이벤트가 트리거됨 → React의 onChange 이벤트 감지 → 상태값이 ""로 업데이트 → 다시 컴포넌트에 value가 주입되어 리렌더링.
  • 비제어 컴포넌트에서는 브라우저가 value""로 변경 → DOM이 바로 반영 → 내부 참조(ref 등)를 통해도 항상 최신 값을 가져올 수 있음.

즉, 브라우저가 값을 지우고 → React가 이벤트로 값 변경 사실을 인식하는 흐름이므로, 충돌 없이 제어/비제어 인풋에서 모두 자연스럽게 동작한다.

3. 기본 제공 기능이기에 자바스크립트가 불필요

X 아이콘이 표시되는 타이밍(입력값이 존재할 때)과 아이콘 클릭 시 동작(값 비우기)이 모두 HTML 표준과 브라우저의 기본 로직으로 처리된다.

이것이 바로 별도의 자바스크립트 로직 없이도, 제어/비제어 인풋 어디서든 무리 없이 구동되는 핵심 이유다.

브라우저별 처리 방식

type="search"는 사실 브라우저별로 구현이 조금씩 다르다.

예를 들어 WebKit 기반(Chrome, Safari 등)에서는 ::-webkit-search-cancel-button이라는 CSS 의사 요소가 등장한다.

Firefox의 경우는 ::-moz-search-cancel-button과 같은 방식이 있을 수도 있고, 어떤 브라우저는 검색 아이콘만 지원하거나 X 아이콘을 표시하지 않을 수도 있다.

다만 대다수 최신 브라우저에서 type="search" 시 X 아이콘을 표시하고, 클릭 시 value를 비워주는 경험은 거의 비슷하게 작동한다.

X 아이콘을 내 맘대로 꾸미고 싶다면?

브라우저마다 기본 X 아이콘 모양이 달라 통일된 디자인을 원할 때는, 아래처럼 CSS를 활용할 수 있다.

WebKit 기반(Chrome, Safari 등)에서는 ::-webkit-search-cancel-button으로 X 아이콘을 직접 정의할 수 있다.

input[type="search"] {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 28px;
}

input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  /* 여기서 기본 아이콘 제거 *
  background: url('/path/to/close-icon.svg') no-repeat center center;
  background-size: 16px 16px;
  cursor: pointer;
}

마무리

자바스크립트로 직접 아이콘을 표시하고, 클릭 이벤트를 달아 값을 비우는 방법도 나쁘진 않다.

하지만 여러 가지 예외 상황(제어 vs. 비제어 등)까지 고려하다 보면 코드가 금방 복잡해진다.

반면, type="search"는 HTML 표준에 의해 브라우저가 알아서 제공해주는 기능이므로, 추가 로직 없이도 안정적으로 동작한다.

특히 “X 클릭 시 인풋 초기화”라는 UX가 필요하고, 복잡한 디자인 요구사항이 없는 경우라면 type="search"가 가장 깔끔한 해법이 될 것이다.

참고 자료 & 이미지

profile
프론트엔드 개발자 👩🏻‍💻

0개의 댓글