
: 지난번 포스팅에 이어서 작업을 해보았다. 꽤나 많은 부분이 변화했는데, 크게 변화시킨 부분은
함수 조합기와 curry, pipe 등 원래 쓰던 함수형 프로그래밍 패턴을 더해서 합성하는 식으로 변경했다.export const META_DATA = {
  email: {
    required: true,
    validators: [checkValidEmail],
  },
  nickname: {
    validators: [
      checkNicknameLength,
      checkNicknameCharacter,
      checkNicknameWhiteSpace,
    ],
    required: true,
  },
  password: {
    required: true,
    validators: [
      checkPasswordLength,
      checkPasswordHasUpperCase,
      checkPasswordHasLowerCase,
      checkPasswordHasSpecificCharacters,
    ],
  },
  introduction: {
    validators: [checkIntroductionLength],
    required: false,
  },
};
이런식으로 hook을 가져다 쓰는 쪽에서 MetaData를 props로 내려주도록 했다. 이 때, 일단 이 훅은 내가 쓸 것이기 때문에 validators에 들어가는 모든 함수는 Either 모나드를 사용하여 Right, Left를 리턴하도록 했다.
import { useCallback, useEffect, useRef, useState } from "react";
import { createFieldValidator } from "../utils/validator";
import {
  ErrorMessage,
  FormData,
  FormErrors,
  MetaData,
  ValidatorRef,
} from "../types/validator";
import { useSkipFirstRender } from "./useSkipFirstRender";
import { alt, identity, isValid, sequence } from "@myorg/utils";
import { curry } from "lodash";
interface UseValidationProps {
  formData: FormData;
  metadata?: MetaData;
}
/* 
** Document ** 
: useValidation은 form 데이터와 metadata를 받아서 form 데이터의 유효성을 검사하는 커스텀 훅입니다.
- metadata는 form 데이터의 필드에 대한 유효성 검사 규칙을 정의합니다.
ex) 
const META_DATA = {
  email: {
    required: true,
    validators: [checkValidEmail],
  },
  nickname: {
    validators: [
      checkNicknameLength,
      checkNicknameCharacter,
      checkNicknameWhiteSpace,
    ],
    required: true,
  },
  password: {
    required: true,
    validators: [
      checkPasswordLength,
      checkPasswordHasUpperCase,
      checkPasswordHasLowerCase,
      checkPasswordHasSpecificCharacters,
    ],
  },
  introduction: {
    validators: [checkIntroductionLength],
    required: false,
  },
};
이 때, 모든 유효성 검사 함수는 Either.right or Either.left를 반환해야 합니다.
- formData는 실제로 이 커스텀 훅을 가져다 쓰는 컴포넌트(예를 들어, Signup or Form 등)에서 관리하는 form 데이터입니다.
ex) 
  const [formData, setFormData] = useState<FormData>({
    email: "",
    nickname: "",
    password: "",
    introduction: "",
  });
이 때, formData의 타입은 packages/ui/src/types/validator.ts에 정의된 FormData 타입에 맞춰서 써야합니다. 
input 태그를 쓰는 value에 한하여 유효성 검사 로직을 만든 것이기 때문에 모든 value는 string이라고 가정합니다.
- 실제 가져다 쓸 때는 
const { errors, isFormValid } = useValidation({
  formData,
  metadata: META_DATA,
});
이런식을로 사용하고, errors에는 formData 각 필드에 대한 유효성 검사 결과가 담겨있고, isFormValid는 전체 form 데이터가 유효한지를 나타냅니다.
따라서 컴포넌트(Input 태그등)에서 사용할 때 이런식으로 활용합니다. 
<FormGroup>
  <Label>{META_DATA.password.required && <span>*</span>}비밀번호</Label>
  <Input
    type="password"
    name="password"
    data-testid="password-input"
    hasError={isValid(errors.password)}
    value={formData.password}
    onChange={handleChange}
  />
  {errors.password && (
    <ErrorMessage data-testid="password-error">
      {errors.password}
    </ErrorMessage>
  )}
</FormGroup>
포인트는 errors.특정_필드_이름에 에러가 있으면 에러 메시지가 할당되고, 문제가 없으면 "" 이 할당된다는 것 입니다. 또한, 초기 아무 값도 입력하지 않았을 때는 
에러가 없는 상태지만, 유효성 검사를 통과한 상태는 아니기 때문에 undefined로 처리 됩니다.
- 마지막으로 submit 버튼을 유효성 검사 clear 여부에 따라 활성, 비활성 처리를 하고자 할 때는 이런식으로 가져다 쓰면 됩니다.
<SubmitButton
  type="submit"
  data-testid="submit-button"
  disabled={!isFormValid}
>
  가입하기
</SubmitButton>
** PS
<Label>{META_DATA.password.required && <span>*</span>}비밀번호</Label>
required 여부를 UI로 표시하고자 할 때는 따로 훅에서 정보를 제공하진 않으며, META_DATA(hook에 파라미터로 제공하는)에서
가져다가 위와 같이 써주시면 됩니다.
*/
export const useValidation = ({ formData, metadata }: UseValidationProps) => {
  const [errors, setErrors] = useState<FormErrors>({});
  const prevFormData = useRef(formData);
  const validatorRef = useRef<ValidatorRef>({});
  const filterActualChangedFields = useCallback(
    (field: string) => formData[field] !== prevFormData.current[field],
    [formData]
  );
  const checkValueIsReset = useCallback(
    (field: string) => !isValid(formData[field]),
    [formData]
  );
  const activateValidator = useCallback(
    (field: string) => {
      validatorRef.current[field](formData[field]);
    },
    [formData]
  );
  const checkValidatorSaved = useCallback(
    (field: string) => validatorRef?.current && validatorRef.current[field],
    []
  );
  const handleSetErrors = useCallback(
    curry((value: ErrorMessage, field: string) => {
      setErrors((prev) => ({
        ...prev,
        [field]: value,
      }));
    }),
    []
  );
  const handleNotRequiredField = handleSetErrors("");
  const handleFirstRenderingField = handleSetErrors(undefined);
  const handleValidate = (formData: FormData) => {
    Object.keys(formData)
      .filter(filterActualChangedFields)
      .forEach(
        alt(
          checkValidatorSaved,
          alt(
            checkValueIsReset,
            alt(isRequired, handleFirstRenderingField, handleNotRequiredField),
            activateValidator
          ),
          identity
        )
      );
  };
  const resetPrevFormData = (formData: FormData) => {
    prevFormData.current = formData;
  };
  useSkipFirstRender(() => {
    sequence(handleValidate, resetPrevFormData)(formData);
  }, [formData, metadata]);
  const hasMetaData = useCallback(
    (metadata: MetaData | undefined) => metadata !== undefined,
    [metadata]
  );
  const hasValidator = useCallback(
    (field: string) =>
      hasMetaData(metadata) && metadata[field] !== undefined ? field : false,
    [metadata]
  );
  const isRequired = useCallback(
    (field: string) =>
      hasMetaData(metadata) && metadata[field].required ? true : false,
    [metadata]
  );
  const initializeErrors = useCallback((formData: FormErrors) => {
    Object.keys(formData).forEach(handleFirstRenderingField);
  }, []);
  const initializeValidator = useCallback(
    (field: string) => {
      validatorRef.current[field] = createFieldValidator(
        field,
        setErrors
      ).createCheckField((metadata as MetaData)[field].validators);
    },
    [metadata]
  );
  const initializeMetadata = useCallback(() => {
    Object.keys(formData).forEach(
      alt(
        hasValidator,
        sequence(
          initializeValidator,
          alt(isRequired, identity, handleNotRequiredField)
        ),
        identity
      )
    );
  }, [formData, initializeValidator, isRequired, handleSetErrors]);
  useEffect(() => {
    initializeErrors(formData);
    initializeMetadata();
  }, []);
  const isValidData = (message: ErrorMessage) => message === "";
  const isFormValid = Object.values(errors).every(isValidData);
  return {
    errors,
    isFormValid,
  };
};
주석으로 대부분의 설명을 끝내놓았다.
tags: string[] 이런 타입의 데이터는 formData로 넣을 수 없다.export interface FormData {
  [key: string]: string;
}
하지만, input 태그의 value만을 커버하기 위해 설계한거긴 해서 예외 상황은 아니다. tags와 같이 배열 등의 데이터를 유효성 검사하려면 따로 로직을 분리해서 작성하던지(이게 좀 불편할 것 같다. formData 말고 다른 state를 따로 만들어서 해야하기 때문이다) 하면 되기 때문이다.