본문 바로가기
코드잇 스프린트 4기/후기

코드잇 스프린트 14주 차 후기

by devwqc 2024. 3. 31.

코드잇 스프린트 4기

 

기간

2024-03-26 ~ 2024-03-30

 

과제

팀 데일리 미션: 매일 팀마다 1명이 1개의 질문을 하고 나머지 팀원들이 답변

위클리 미션: 주마다 배운 내용을 바탕으로 스프린트 과정 동안 만들어가는 개인 프로젝트

위클리 페이퍼: 주마다 정해진 2개의 주제에 대해 조사

 

학습

토픽 26. Next.js로 웹사이트 만들기

토픽 27. Next.js API 만들기

 

스터디

codingTest: 매 주 월, 목 프로그래머스 Lv. 0 모든 문제 풀기

으쌰으쌰: 매일 모던 자바스크립트 Deep Dive 책 정해진 진도 공부 후 정리

 

후기

14주 차 위클리 미션으로 페어 프로그래밍으로 진행했다. 3명이 한 팀을 이루어서 Input 컴포넌트를 만들었다.

다들 생각이 정리되지 않은 상태여서 너무 내 생각으로 만든 느낌이 강해서 미안한 느낌이 들었고 서로 생각을 공유해서 해보길 원했기 때문에 아쉬움도 남았다.

다들 일정이 있기도 했고 작업을 꽤 진행했어서 이후 작업은 개인적으로 완성하고 서로 공유해 보기로 했다.

 

> components/Input.tsx

export const VALIDATE_TYPE = {
  email: 'email',
  password: 'password',
};

type InputProps = {
  className?: string;
  validateType?: keyof typeof VALIDATE_TYPE;
  isVisibleToggle?: boolean;
  isValidationCheck?: boolean;
  [key: string]: any;
};

function Input({
  className,
  validateType,
  isVisibleToggle,
  isValidationCheck,
  ...rest
}: InputProps) {
  const [isVisible, setIsVisible] = useState(false);
  const [isError, setIsError] = useState(false);
  const [message, setMessage] = useState('');

  const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
    if (!isValidationCheck) return;
    const value = e.target.value;

    switch (validateType) {
      case VALIDATE_TYPE.email:
        setIsError(!validateEmail(value));
        break;
      case VALIDATE_TYPE.password:
        setIsError(!validatePassword(value));
        break;
      default:
        return;
    }
  };

  const handleVisibleToggle = () => {
    setIsVisible((preIsVisible) => !preIsVisible);
  };

  const classNames = `${styles.input} ${className}`;
  return (
    <div>
      <div>
        <input className={classNames} {...rest} onBlur={handleBlur} />
        {isVisibleToggle && (
          <button type="button" onClick={handleVisibleToggle}>
            <Image src={isVisible ? eyeOnImg : eyeOffImg} alt="눈 감음" />
          </button>
        )}
      </div>
      {isError && <p>{message}</p>}
    </div>
  );
}

export default Input;

 

페어 프로그래밍으로 작업한 Input 컴포넌트이다.

 

유효성 검사 로직을 Input 안에서 가지고 있기 때문에 커스텀이 용이하지 않았다.

 

여기서 문제는 blur 뿐만 아니라 submit 했을 때도 재검사해야 하는데 그걸 대응하지 못하고 Input 컴포넌트가 수행하는 동작을 내부에 가지고 있어서 커스텀도 어렵다. 즉, 공통 컴포넌트로서의 역할을 제대로 하지 못하고 있다.

 

페어 프로그래밍 이후로 이러한 문제점을 해결하기 위해서 개인적으로 완성한 Input 컴포넌트는 다음과 같다.

 

> components/Input.tsx

type InputProps = {
  type?: string;
  className?: string;
  hasVisibleToggler?: boolean;
  initialIsVisible?: boolean;
  message?: string;
  isError?: boolean;
} & InputHTMLAttributes<HTMLInputElement>;

function Input({
  type: initialType = 'text',
  className = '',
  hasVisibleToggler = false,
  initialIsVisible = false,
  message = '',
  isError = false,
  ...rest
}: InputProps) {
  const [type, setType] = useState(
    hasInitialInputType(hasVisibleToggler, initialIsVisible)
      ? initialType === 'password'
        ? 'text'
        : initialType
      : 'password'
  );
  const [isVisible, setIsVisible] = useState(initialIsVisible);

  const handleVisibleToggle = () => {
    const nextType = isVisible
      ? 'password'
      : initialType === 'password'
      ? 'text'
      : initialType;
    setType(nextType);
    setIsVisible((preIsVisible) => !preIsVisible);
  };

  const classNames = `${styles.input} ${
    styles[isError ? 'error' : '']
  } ${className}`;
  return (
    <div>
      <div>
        <input type={type} className={classNames} {...rest} />
        {hasVisibleToggler && (
          <button type="button" onClick={handleVisibleToggle}>
            <Image
              src={isVisible ? eyeOnImg : eyeOffImg}
              alt={isVisible ? '눈 뜸' : '눈 감음'}
            />
          </button>
        )}
      </div>
      <p>{message}</p>
    </div>
  );
}

export default Input;

 

크게 달라진 건 유효성 검사 로직이 Input 컴포넌트 내부에 있지 않고 isError, message 등 외부에서 넘겨주는 방식으로 바꿨다.

 

그리고 Input을 제어 컴포넌트로 변경하였고 Input에 대한 제어는 useInput 커스텀 훅을 통해서 구현했다.

 

> pages/index.tsx (Input을 사용하는 곳)

export default function Home() {
  const email = useInput({
    validator: getValidateEmailCode,
  });

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const form = e.currentTarget;

    const _email = form['email']?.value;

    const validateInputEmailCode = getValidateEmailCode(_email);
    
    let isValid = true;
    if (validateInputEmailCode !== VALIDATION_CODE.PASS) {
      email.setInputErrorCode(validateInputEmailCode);
      isValid = false;
    }

    if (!isValid) return;

    console.log('fetch');
  };

  return (
    <form onSubmit={handleSubmit}>
      <Input
        type="email"
        name="email"
        value={email.value}
        message={email.message}
        isError={email.isError}
        onBlur={email.handleInputChange}
        onChange={email.handleInputChange}
      />
      <button>submit</button>
    </form>
  );
}

 

 

useInput 컴포넌트에서는 validator 함수를 받아서 유효성 검사를 통해 isError, message를 처리했다.

return으로 handleInputChange를 내보내서 제어 컴포넌트로 사용할 수 있도록 했고 onSubmit 등 추가적인 에러 처리를 할 수 있게 setInputErrorCode도 내보냈다.

 

> hooks/useInput.ts

type useInputTypes = {
  initialValue?: string;
  validator?: (value: string, valueConfirm: string) => ValidationCode;
};

function useInput({ initialValue = '', validator }: useInputTypes = {}) {
  const [value, setValue] = useState(initialValue);
  const [isError, setIsError] = useState(false);
  const [message, setMessage] = useState('');

  const handleInputChange = (
    e: ChangeEvent<HTMLInputElement>,
    valueConfirm = ''
  ) => {
    const _value = e.target.value;

    setValue(_value);

    if (!validator || !isFunction(validator)) return;

    const validationResultCode = validator(_value, valueConfirm);
    setIsError(validationResultCode === VALIDATION_CODE.PASS ? false : true);
    setMessage(VALIDATION_MESSAGE[validationResultCode]);
  };

  const setInputErrorCode = (code: ValidationCode) => {
    setIsError(code === VALIDATION_CODE.PASS ? false : true);
    setMessage(VALIDATION_MESSAGE[code]);
  };

  const resetInput = () => {
    setValue(initialValue);
    setMessage('');
    setIsError(false);
  };

  return {
    value,
    message,
    handleInputChange,
    isError,
    setIsError,
    setInputErrorCode,
    resetInput,
  };
}

export default useInput;

 

> constants/validation.ts

export const VALIDATION_CODE = {
  PASS: 'PASS',
  EMPTY_EMAIL: 'EMPTY_EMAIL',
  EMPTY_PASSWORD: 'EMPTY_PASSWORD',
  EMPTY_PASSWORD_CONFIRM: 'EMPTY_PASSWORD_CONFIRM',
  PATTERN_ERROR_EMAIL: 'PATTERN_ERROR_EMAIL',
  PATTERN_ERROR_PASSWORD: 'PATTERN_ERROR_PASSWORD',
  PATTERN_ERROR_PASSWORD_CONFIRM: 'PATTERN_ERROR_PASSWORD_CONFIRM',
};

export const VALIDATION_MESSAGE = {
  [VALIDATION_CODE.PASS]: '',
  [VALIDATION_CODE.EMPTY_EMAIL]: '이메일을 입력해 주세요.',
  [VALIDATION_CODE.EMPTY_PASSWORD]: '비밀번호를 입력해 주세요.',
  [VALIDATION_CODE.EMPTY_PASSWORD_CONFIRM]: '비밀번호를 입력해 주세요.',
  [VALIDATION_CODE.PATTERN_ERROR_EMAIL]: '올바른 이메일 주소가 아닙니다.',
  [VALIDATION_CODE.PATTERN_ERROR_PASSWORD]:
    '비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요.',
  [VALIDATION_CODE.PATTERN_ERROR_PASSWORD_CONFIRM]:
    '비밀번호가 일치하지 않아요.',
};

 

> utils/validation.ts

export function validateEmail(email: string) {
  const regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/i;
  return regex.test(email);
}

export function getValidateEmailCode(email: string): ValidationCode {
  if (!email) {
    return VALIDATION_CODE.EMPTY_EMAIL;
  }

  if (!validateEmail(email)) {
    return VALIDATION_CODE.PATTERN_ERROR_EMAIL;
  }

  return VALIDATION_CODE.PASS;
}

 

 

현재의 내가 할 수 있는 최선을 다했지만 작업을 진행하면서도 마음에 들지 않았다. 안티패턴으로 느껴졌고 사용성도 좋지 않다.

 

use-hook-form과 같이 useRef를 사용한 방식으로 개선해 봐도 좋을 거 같다.

 

 

감사합니다.

댓글