[리액트] 입력값 검증하기

비얌·2022년 11월 28일
9
post-thumbnail

개요

지금까지는 바닐라 자바스크립트를 이용하여 문제를 해결하는 과제를 받았었는데, 이번에는 리액트로 과제를 받았다. (두근)

input 태그에 올바른 값을 입력했는지 검증하는 과제이며 완성된 결과는 아래와 같다.

입력된 이메일과 전화번호가 올바르지 않으면 에러 상황에 따른 에러 메시지를 출력한다. 그리고, 올바른 값을 입력하면 에러 메시지는 지우고 가장 아래에 완성된 이메일, 전화번호를 출력하면 된다.

전체 코드
'입력값 검증하기' 전체 코드가 있는 깃허브 주소



문제 설명

ValidatedInput이라는 컴포넌트를 구현해야 하는데, props로 전달된 검증 규칙들을 만족하는 경우에만 아래에 있는 이메일:, 전화번호:의 값을 업데이트해야 하며 검증 규칙들을 만족하지 않는 경우에는 에러에 따른 에러 메시지를 표시해야 한다.

✅ 정답 확인 모드를 체크했을 때 정답 화면을 볼 수 있으며 그것을 참고하여 체크하지 않은 상태에서 체크했을 때와 같은 화면을 만드는 것이 과제이다.



코드 파악하기

문제를 풀기 전, 일단 주어진 코드를 파악해보기로 했다.

1. main.jsx

일단 main.jsx를 보면, App 컴포넌트를 호출하고 있다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import "@unocss/reset/tailwind.css"
import "uno.css"

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

2. App.jsx

main.jsx에서 호출하는 App이다. 위에서부터 천천히 분석해보자!

임포트

useState를 임포트하고 있고, validatedInput 파일과 ValidatedInput.answer 파일에서 각각 ValidatedInputPractice, ValidatedInputAnswer 컴포넌트를 가져오고 있다.

import { useState } from 'react';
import ValidatedInputPractice from './ValidatedInput';
import ValidatedInputAnswer from './ValidatedInput.answer';

이메일, 전화번호 검증 함수

isEmail은 정규표현식으로 입력값이 올바른 입력값인지 검사하는 함수이다.
isNumeric은 정규표현식으로 입력값이 올바른 전화번호인지(숫자인지) 검사하는 함수이다.

/// input이 올바른 이메일인지 검사하는 함수이며, 정규 표현식을 사용해서 형식 검사
const isEmail = (input) =>
  /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
    input
  );

/// input이 숫자로만 이루어져 있는지 검사하는 함수이며, 정규 표현식 사용해서 모든 문자가 0-9 내에 들어가는지 검사
const isNumeric = (input) => /^[0-9]+$/.test(input);

state 선언

state를 세 개 선언하고 있다. 일단 isAnswerMode는 이름이 is로 시작하고 초깃값이 true인 것으로 보아 리턴값으로 true/false 중 하나를 가지는 것으로 보인다.

email은 빈 값으로 초기화되어있으며, phoneNumber도 마찬가지이다. input으로 받는 이메일과 전화번호 값을 각각 email, phoneNumber라고 한 것으로 보인다.

그리고 ValidatedInput은 ✅ 정답 확인 모드가 체크되면 정답 확인 모드에 들어가고, 아니면 연습 모드로 들어가게 하는 값으로 보인다.

function App() {
  const [isAnswerMode, setIsAnswerMode] = useState(true);
  const [email, setEmail] = useState('');
  const [phoneNumber, setPhoneNumber] = useState('');

  // 정답 확인 모드인지에 따라 실제로 사용되는 컴포넌트를 변경하는 코드, 무시해도 됨
  const ValidatedInput = isAnswerMode
    ? ValidatedInputAnswer
    : ValidatedInputPractice;

✅ 정답 확인 모드

실행 화면의 맨 위에는 체크박스가 포함된 ✅ 정답 확인 모드가 있다.

체크버튼이 눌리면(onChange) setIsAnserMode로 isAnswerMode의 상태가 바뀐다.

그리고 setEmail과 setPhoneNumber로 인해 화면에 보이는 input 창 안의 내용인 email과 phoneNumber가 초기화된다.

return (
  <div className="flex flex-col my-4 gap-2 items-center">
    <label className="flex gap-2">
      <input
        type="checkbox"
        checked={isAnswerMode}
        onChange={(e) => {
          setIsAnswerMode(e.target.checked);
          setEmail('');
          setPhoneNumber('');
        }}
        />
      정답 확인 모드
    </label>

email을 검증하는 부분

ValidatedInput 컴포넌트에 value, onInput, validators를 props로 주고 있다.

그런데 validators는 뭔지 잘 모르겠다. 👉 두개의 객체를 가진 배열이라고 생각하면 될 것 같다. validators는 fn이라는 함수와 message로 이루어져 있다.

<ValidatedInput
  value={email}
  onInput={setEmail}
  validators={[
    {
      fn: (input) => input.length > 0,
      message: '내용을 입력해주세요.',
    },
    {
      fn: isEmail,
      message: '올바른 형식의 이메일을 입력해주세요.',
    },
  ]}
  />

phoneNumber를 검증하는 부분

phoneNumber를 검증하는 이 부분 역시ValidatedInput 컴포넌트에 value, onInput, validators를 props로 주고 있다. validators는 fn이라는 함수와 message로 이루어진 배열이다.

<ValidatedInput
  value={phoneNumber}
  onInput={setPhoneNumber}
  validators={[
    {
      fn: (input) => input.length > 0,
      message: '내용을 입력해주세요.',
    },
    {
      fn: isNumeric,
      message: '숫자만 입력해주세요.',
    },
    {
      fn: (input) => input.length >= 10,
      message: '10자 이상 입력해주세요.',
    },
  ]}
  />

검증된 이메일, 전화번호를 표시하는 부분

email이라는 변수에 검증에 통과한 이메일을 담고 있다.
마찬가지로 phoneNumber이라는 변수에 검증에 통과한 전화번호를 담고 있다.

<p>이메일: {email}</p>
<p>전화번호: {phoneNumber}</p>

3. ValidatedInput.jsx

내가 변경해야 할 부분은 바로 ValidatedInput.jsx 파일이다. (정확히 말하면 onChange 함수 부분) 여기에는 아래와 같은 코드가 담겨있다.

props에는 value, onInput, validators를 받아왔으므로 이들이 모두 들어있다.

아래의 코드에서 useState 안에 들어가 있는 ??nullish 병합 연산자이다.

nullish 병합 연산자
a ?? b의 평가 결과: a가 null도 아니고 undefined도 아니면 a, 그 외의 경우는 b

import { useState } from 'react';

const ValidatedInput = (props) => {
  const [value, setValue] = useState(props.value ?? '');
  const [error, setError] = useState();

  const onChange = (e) => {};

  return (
    <div className="inline-block">
      <div className="flex flex-col items-center">
        <input
          type="text"
          value={value}
          onChange={onChange}
          className="border border-gray-300 rounded p-2"
        />
        <span className="text-red text-sm font-medium">{error}</span>
      </div>
    </div>
  );
};

export default ValidatedInput;


코드 짜기

1. input 창에 입력하면 화면에 반영되게 하기

일단 이메일을 입력했을 때부터 시작하여 코드를 하나하나 따라가보자.

이런식으로 주석을 달면서 따라가봤다.

// App.jsx

{/* 이메일 확인.. */}
<ValidatedInput
// ValidatedInput 컴포넌트에 props로 .. 3개를 넘겨주는?
// email은 위에서 state로 선언했음
value={email}
// onInput은 선언한 email의 최신값?
onInput={setEmail}
// validators는 객체임. 두 개의 원소가 있음.
// 근데 fn이 뭔지 모르겠음. 위에는 함수고 아래는..아래도 함수네
validators={[
            {
              fn: (input) => input.length > 0,
              message: '내용을 입력해주세요.',
            },
            {
              fn: isEmail,
              message: '올바른 형식의 이메일을 입력해주세요.',
            },
            ]}
/>
  {/* 여기까지 보고 ValidatedInput.jsx로 가보자 */}
// ValidatedInput.jsx

// 여기 props에 props.value, props.onInput, props.validators가 들어감
const ValidatedInput = (props) => {
  // props.value가 null도 아니고 undefined도 아니면 props.value 외는 ''
  const [value, setValue] = useState(props.value ?? '');
  const [error, setError] = useState();

  const onChange = (e) => {
	// 여기를...만들면...되는 것 같음....
  };

일단 onChange 함수 안에 아무것도 적지 않고 input 창에 입력을 하면 화면에 아무것도 반영되지 않는다.

왜냐하면, 입력됨에 따라 변화하는 input 태그 안의 값을 setState로 갱신해주지 않았기 때문에, 초깃값인 아무것도 없는 ''인 상태로 화면에 반영되는 것이다.

따라서 setState로 입력되는 값을 갱신해주려고 한다.

앞서 입력값인 value의 setState를 setValue라고 선언한 적이 있다. 따라서 setValue로 입력값을 갱신해주자.

const [value, setValue] = useState(props.value ?? '');

이렇게 하면, 입력하는대로 화면에 잘 반영된다!!!

const onChange = (e) => {
    setValue(e.target.value);
};


💥 삽질1 - value를 분리하지 않아도 된다

그런데 이렇게 하고 보니, 같은 컴포넌트에서 같은 이름의 props인 value로 email과 phoneNumber를 받고 있다는 것을 알게되었다. 그래서 이 둘을 분리해야겠다고 생각했다. 왜냐하면 같은 ValidatedInput라는 컴포넌트에 같은 이름인 value로 들어오니까 이 둘을 각각 다른 이름(email과 phoneNumber)으로 분리해서 각각 따로 검증을 해야한다고 생각했다.

그래서 아래와 같이 value를 분리하려고 했었다.

const [value, setValue] = useState({
    email: '',
    phoneNumber: ''
  });

const { email, phoneNumber } = inputs;

하지만!! 이 생각은 잘못됐다.

왜냐하면, 그렇게 분리할 필요가 없다.

처음에는 email과 phoneNumber는 다른 입력값이므로 같은 이름인 value가 아니라 둘을 email/phoneNumber와 같은 이름으로 분리해야 한다고 생각했는데, props를 자세히 보니 모두 fn이라는 함수로 입력값이 맞는지, 틀린지 검증하고 틀리면 message를 출력하게 하고 있었다. 그러니까 나는 onChange 함수에서 각 객체의 fn에 입력값을 넣고, 틀리면 그 객체의 message를 출력하는 공통의 함수만 만들면 되는 것이었다. 👉 컴포넌트를 쓰는 이유


💥 삽질2 - value 대신 e.target.value를 쓰자

이건 예전에도 겪었던 문제인데, input 태그 안에 들어오는 값을 value라고 정의했으므로 value을 fn의 인자로 쓰려고 했다. 그런데, 바로바로 반영이 안되는 오류가 발생했다.

이것은 그냥 value 대신 e.target.value를 쓰면 간단하게 해결된다.


2. 완성된 코드

완성된 코드는 아래와 같다.

const ValidatedInput = (props) => {
  // props.value가 null도 아니고 undefined도 아니면 props.value 외에는 ''
  const [value, setValue] = useState(props.value ?? '');

  const [error, setError] = useState();

  const onChange = (e) => {
    setValue(e.target.value);

    let hasError = false;

    props.validators.forEach((rule) => {
      if (!hasError) {
        if (!rule.fn(e.target.value)) {
          setError(rule.message);
          hasError = true;
        }
      }
    });

    if (!hasError) {
      setError('');
      props.onInput(e.target.value);
    }
  };
  
  return (
    ...
    )
}
  1. setValue(e.target.value)로 input 태그에 입력한 값이 화면에 반영되게 했다.

  2. props로 받아온 배열 validators를 forEach로 순회했다. validators의 각 fn에 e.target.value(현재 입력한 값)을 인자로 주어 false가 리턴되면 에러 메시지인 message를 출력하게 했다(setError를 이용하여 error를 갱신함)

  3. hasError라는 변수를 만들어 if 문의 조건으로 사용했다.


💥 여기서 hasError라는 변수를 만든 이유는?

hasError를 만든 이유는 한 번 에러를 탐지하면 그 후에는 검증하지 않기 위해서 만든 것이다.

email을 검증하는 절차에서 validators에는 두 번의 검증 절차가 있다. 따라서 에러메시지도 두 개가 있는데, 만약 hasError 없이 코드를 짠다면 아래와 같이 값을 입력했다가 모두 지워도 '내용을 입력해주세요.'가 뜨지 않고 계속 '올바른 형식의 이메일을 입력해주세요.'만 뜰 것이다.

// hasError를 사용하지 않았을 때

props.validators.forEach((rule) => {
  // if (!hasError) {
  if (!rule.fn(e.target.value)) {
    setError(rule.message);
    // hasError = true;
  } else {
    setError('');
    props.onInput(e.target.value);
  }
  // }
});

// if (!hasError) {
// setError('');
// props.onInput(e.target.value);
// }

왜 이러는 것일까?

왜냐하면, 위의 코드에서 두개의 fn이 모두 false일 때 첫 번째 오류 메시지인 '내용을 입력해주세요.'를 두 번째 오류 메시지인 '올바른 형식의 이메일을 입력해주세요.'가 덮어쓰기 때문이다.

따라서 의도한대로 오류메시지를 얻기 위해서는 첫번째에서 오류가 발생한다면(내용이 없다면) 이미 오류가 났기 때문에 다음 fn을 검사하지 않고 '내용을 입력해주세요.'를 출력한 뒤 멈춰야 한다. 이처럼 '이전에 오류를 탐지했는지'를 구별하기 위해 hasError라는 변수를 만든 것이다.

그리고 아무런 오류도 탐지하지 못하면 forEach문에서 hasError는 바뀌지 않을 것이고, 따라서 forEach문 밖에서 hasError는 초기값인 false가 될 것이다.

따라서 hasError가 true일 때만(올바른 값을 입력했을 때) setError('')로 에러 메시지에 아무것도 뜨지 않게 설정해줬다.



결과

오류를 잘 감지하고, 에러 메시지를 정상적으로 출력하는 것에 성공했다!!



정답과 비교

정답 코드는 아래와 같다.

const onChange = (e) => {
  const newValue = e.target.value;
  setValue(newValue);
  for (const rule of props.validators ?? []) {
    if (!rule.fn(newValue)) {
    setError(rule.message);
  return;
}
}
setError(null);
props.onInput?.(newValue);
};
  1. forEach 대신 for of를 썼다.
  2. hasError라는 변수를 만드는 대신 return을 했다.
    (forEach에서는 return을 쓸 수 없는데 for of에서는 쓸 수 있다고 한다. 처음 알았다.)
  3. 에러 메시지를 비울 때 '' 대신 null을 썼다.
  4. 옵셔널 체이닝를 써서 props로 onInput이 들어오지 않을 때를 대비했다.

<옵셔널 체이닝 ?.>
?.은 ?.'앞’의 평가 대상이 undefined나 null이면 평가를 멈추고 undefined를 반환한다.



🐹 회고

리액트는 정말 오랜만에 해보는데, (한달 쯤 전에 마지막으로 한 것 같음) 정말 재미있었다!!

특히 컴포넌트를 재사용한다는 건 바로 이런 거구나...! 하고 알 수 있었다. 컴포넌트를 재사용한 경험이 없을뿐더러 그동안 배운 재사용 가능한 컴포넌트는 디자인을 위한 컴포넌트 정도였기 때문이다.

이전에 디두님이 버튼 스타일 컴포넌트를 만들어서 모든 버튼에 적용되도록 재사용하자는 말씀을 하신 적이 있는데, 그때 관련 지식이 없어서 내가 할 수 있을지 망설였던 적이 있다. 이제는 컴포넌트를 재사용하는 경험을 해봤으니 다음에 코드를 짤 때 적용해보자😆

profile
🐹강화하고 싶은 기억을 기록하고 공유하자🐹

2개의 댓글

comment-user-thumbnail
2022년 12월 4일

글 잘보고 갑니다.
어디서 나온 과제인지 궁금한데 알려주실수 있을까요>?

1개의 답글