완벽한 input[type="password"]를 만드는 방법

우빈·2024년 11월 24일
3
post-thumbnail

브라우저에서 비밀번호 타입의 input을 렌더링할 때, PC와 모바일의 차이점이 있다는 사실 알고 계셨나요?

모바일PC

모바일에서는 type이 password인데도 방금 입력한 글자를 몇 초간 보여주는 기능이 있습니다.
그런데 어플리케이션의 요구 사항에 따라서 PC나 모바일을 불문하고
아예 글자를 보여주지 않게 해야 하는 경우가 생기면, 어떻게 대처해야 할까요?

이 글에서는...

  • React를 기반으로 구성된 어플리케이션을 중점으로 설명합니다.
  • state를 활용해 패스워드 타입의 input의 value를 완벽히 가리는 방법을 설명합니다.

단방향 바인딩을 이용하기

다른 라이브러리나 프레임워크와는 다르게 React에서는 state와 view가 단방향으로 바인딩이 되어 있습니다.

그렇기에 state를 view에 연결하려면 onChange와 value에 state를 모두 연결해 주어야
입력값을 완벽히 받아 state에 저장할 수 있습니다.

const [content, setContent] = useState("");

<input 
  onChange={(e) => setContent(e.target.value)}
  value={content}
/>

그렇다면 onChange와 value가 각각 다른 state를 바라보고 있다면,
입력받는 값을 따로 저장해 두고, 사용자에게 보여주는 값은 원하는대로 보여줄 수 있지 않을까요?

위 사진처럼 onChange가 트리거될 때의 입력값을 기반으로 content와 render content를 관리하고,
input의 value에는 render content를 렌더링하는 식으로 관리해보겠습니다.

하나의 input을 두 개의 state로 관리하기

먼저 저장할 값과 보여줄 값, 총 두 state를 정의해주겠습니다.

const [content, setContent] = useState("");
const [renderContent, setRenderContent] = useState("");

이번 글에서의 목적은 값 대신 password에서 사용되는 점을 보여주어야 하기 때문에,
renderContent는 점으로 관리해 보겠습니다.

const handleContentChange = (e: ChangeEvent<HTMLInputElement>) => {
    const typedValue = e.target.value;
    setRenderContent("•".repeat(typedValue.length));
  
    if ((e.nativeEvent as { data?: string }).data === null) {
      setContent((text) => `${text.slice(0, text.length - 1)}`);
    } else {
      setContent((text) => `${text}${typedValue[typedValue.length - 1]}`);
    }
};

동작 흐름을 설명 드리겠습니다.

  1. 입력값을 'typedValue'로 정의합니다.
  2. 렌더링할 content를 입력값의 length만큼 패스워드 점으로 채워줍니다.
  3. 사용자가 BackSpace를 입력할 경우 nativeEvent의 data는 null입니다.
    BackSpace인 경우 입력값 끝의 한 글자를 지웁니다.
    BackSpace가 아닌 값일 경우 마지막으로 입력한 값을 추가합니다.

이런 식으로 state를 구성한 후 렌더링 단에서 renderContent를 보여주면 문제는 해결됩니다.

<input
  onChange={handleContentChange}
  value={renderContent}
  type="input"
  maxLength={7}
  pattern="[0-9]*"
/>
viewcontent

이제 브라우저의 환경과 관계없이 똑같은 UI를 노출시킬 수 있습니다.

#Edge Case : selection을 이동한다면요?

현재 로직에는 무조건 기존 입력값의 뒷부분을 기준으로 content 삭제나 추가를 제공합니다.

input 값에서 selection을 이동할 경우 값이 원하는 바와 다르게 저장되는 이슈가 발생합니다.
그렇기에 input에서 사용자가 selection하는 것을 막아, 끝에서부터 입력할 수 있게 함으로
발생하는 Edge Case를 처리할 수 있습니다.

<input
  onKeyDown={(e) => {
	(e.target as HTMLInputElement).selectionStart = content.length;
	(e.target as HTMLInputElement).selectionEnd = content.length;
  }}
/>

selection 자체를 막지는 않고, 키보드를 눌렀을 때 바라보는 selection이 무조건 content의 끝이도록 세팅해 주었습니다.

마무리

HTML에서 input 태그는 특히나 개발자가 자유롭게 사용하기에 어느 정도 제약이 있는 element입니다.
해당 이슈를 겪었던 이유는 input이 제공하는 <input type="password" /> 태그의 패스워드 점이 폰트에 비해 너무 작았기 때문입니다.

이 때문에 해당 패스워드 점을 키워주는 pass라는 폰트를 사용하려 했는데,
pass는 기본 시스템의 font를 override하고 있어 점은 커지더라도
다른 숫자와 폰트가 동일하지 않는다는 이슈가 있어 매우 난감했습니다.

그렇기에 해당 이슈를 기술적인 측면에서 state로 처리하여, 모바일에서도
똑같이 최근 입력한 숫자를 보여주지 않게 바꾸었습니다.

비슷한 요구 사항을 처리해야 하는 분들께 도움이 되고자 글을 남깁니다.

profile
프론트엔드 공부중

1개의 댓글

comment-user-thumbnail
약 1시간 전

e.target.value에 입력 하거나 지울 때 둘 다 현재의 입력되어있는 값이 그대로 찍힐거 같은데, nativeEvent로 구분해서 setContent를 하는 이유가 있을까용? 그냥 바로 e.target.value를 setContent하면 안 되는건지 궁금합니다~

답글 달기