복잡한 유효성 검사

노성호·2021년 7월 10일
0

react

목록 보기
9/12
post-thumbnail

요구사항

  • 하위 항목이 여러개 있는 datetime range 유효성 검사를 해야한다.
  • 활성화된 항목만 유효성 검사(뒤의 시간이 앞의 시간보다 늦어야 함. 유효성 검사에 실패하면 에러 메시지를 띄운다(submit 불가능))를 한다.
  • main range의 라디오버튼의 all을 선택하면 하위항목 모두 같은 datetime range를 갖는다.
  • main range의 라디오버튼을 each range를 선택하면, main range의 datetime range가 disabled되고 하위항목의 체크박스가 활성화 된다.
  • 각 하위 항목을 체크하면, 해당 항목의 datetime range가 활성화 된다.
  • 전체범위의 유효성 검사는 무시된다.
  • 다시 전체 main range를 all으로 설정하면 하위 항목의 모든 유효성 검사는 초기화된다.

구현 방향

처음에는 main container의 상태로 관리가 안될것 같아서 redux로 상태관리를 하려고 했다. 그래도 이런 휘발성 데이터를 redux로 관리하는 것은 아닌것 같아 state를 좀 나누는 방향으로 진행했다.

구현부

파일구조

  • CreateProject.tsx
    • ProjectPage.tsx
      • SubjectRangeComponent.tsx

CreateProject.tsx

const [projectState, setProjectState] = useState({
  range: { startDate: new Date(), dueDate: new Date(),
  checkRanges: [
    { title: string,
      checked: false,
      range: { startDate: new Date(), dueDate: new Date(),
    }, ...
  ]
 });
 
 return (
   <>
     <Route path='/project' render={() => <ProjectPage { ...projectState } />} />
   </>
 )
  • 메인 컨테이너. 최상위 컨테이너로 모든 상태를 관리한다.
  • checkRanges가 하위항목의 체크박스 / datetime picker

ProjectPage.tsx

interface DateRange {
  startDate: Date;
  dueDate: Date;
}

interface ProjectProps {
  title: string;
  range: DateRange;
  checkRanges: {
    index?: number;
    title: string;
    checked: boolean;
    date: DateRange;
  }
  propsHandler: (form: ProjectProps) => void;
}

// ProjectPage Component
const [checkDates, setCheckDates] = useState(new Array(props.checkRanges.length).fill(false)); // 하위항목들의 체크여부
const [checkRangeEnabled, setCheckRangeEnabled] = useState(false); // main range의 체크 여부
const [submitProject, setSubmitProject] = useState(false); // form submit
// 전체 form 유효성 검사
const [formValidator, setFormValidator] = useState({
  title: true,
  range: false,
  isCheckAll: true,
  checkRanges: new Array(props.checkRanges.length).fill({ checked: false, range: false }),
});

function titleHandler(newTitle: string) {
  props.propsHandler({ ...props, title: newTitle });
  setFromHandler({ ...formValidator, title: newTitle.trim() ? true : false });
}

// 하나의 datepicker만 검사할 경우 아래와 같음.
function dateHandler(range: DateRange, index: number) {
  if (index < 0) {
    props.propsHandler({ ...props, range: range });
  } else {
    const newCheckedRanges = { ...props, checkRanges };
    newCheckedRanges[index].date = date;
    props.propsHandler({ ...props, checkRanges: newCheckedRanges });
  }
}

function radioHandler(isAll: boolean) {
  props.propsHandler({ ..props, isCheckAll: isAll });
}

function checkRangeChangeHandler(check: boolean, index: number) {
  const newCheckRanges = [ ...checkDates ];
  newCheckRanges[index] = check;
  setCheckDates(newCheckRanges);
}

// 여러개의 checkbox가 있고, 각각의 checkbox를 체크하면 datepicker가 활성화. 그 이후 유효성 검사를 하는 케이스.
// 코드가 너무 더러움. 리팩토링이 가능할지 확인을 해봐야됨.
function checkerDateValidateHandler(
  check: boolean,
  index: number,
  range: DateRange,
  dateType: string,
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
  if (!check) return;
  const dateRange = { ...range, [dateType]: new Date(event.target.value) };
  const checkedDates = [ ...formValidator.checkRanges ];
  if (dateType === 'startDate') {
    checkedDates[index] = { checked: check, dates: moment(event.target.value).isBefore(dateRange.dueDate) };
  } else {
    checkedDates[index] = { checked: check, dates: moment(event.target.value).isAfter(dateRange.startDate) };
  }
  const checkDateValidArray = checkedDates.map((item) => {
    const isCheckDateValid = item.checked && item.dates ? true : false;
    const itemValid = item.checked && isCheckDateValid ? true : false;
    let dateValid = -1;
    if (!item.checked) {
      dateValid = -1;
    } else if (itemValid) {
      dateValid = 1;
    } else if (!itemValid) {
      dateValid = 0;
    }
    return dateValid;
  });
  setFormValidator({ ...formValidator, checkDates: checkedDates, dates: !checkDateValidArray.includes(0) });
}

function emptyDateValidation(
  range: DateRange,
  dateType: string,
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
  () => null;
}

function dateValidation(
  range: DateRange,
  dateType: string,
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
  const dates = { ...range, [dateType]: new Date(event.target.value) };
  dateType === 'startDate'
    ? setFormValidator({ ...formValidator, dates: moment(event.target.value).isBefore(dates.dueDate) })
    : setFormValidator({ ...formValidator, dates: moment(event.target.value).isAfter(dates.startDate) });
}

useEffect(() => {
  setSubmitProject(formValidator.title && formValidator.range);
}, [formValidator]);

// check ranges
const checkRangeComponents = props.checkRanges.map((item, idx = -1) => {
  return (
    <div key={idx}>
      <CheckerSubject 
        enabled={!checkRangeEnabled}
        title={item.title}
        checked={formValidator.checkRanges[idx].checked ? checkDates : true}
        index={idx}
        checkHandler={checkRangeChangeHandler}
      />
      <SubjectRangeComponent
        range={
          formValidator.checkRanges[idx].checked
          ? props.checkRanges[idx].date
          : { startRange: new Date(), dueDate: new Date() }
        }
        dateChange={dateHandler}
        enable={!enable ? !dateChecked[idx] : true}
        index={idx}
        dateValidation={emptyDateValidation} // 빈 핸들러
        check={checkDates[idx]
      />
    </div>
  )
}

// contaner
return (
  {/* 타이틀 */}
  <Box mb={2} mt={2}>
    <Grid container alignItems="center">
      <Box mr={2}>
        <Grid item>
          <SubtitleComponent title={'과제명'} />
        </Grid>
      </Box>
      <HiddenBlock item ishidden={formValidator.title}>
        <Typography variant="subtitle2" color="error">
          과제명은 필수 입력 사항입니다.
        </Typography>
      </HiddenBlock>
    </Grid>
  </Box>
  <TitleComponent title={props.title} onChange={titleChangeHandler} />

  {/* 과제범위 */}
  <SubjectRangeComponent
    left={'전체'}
    right={'범위지정'}
    radioHandler={subjectRangeRadioHandle}
    subjectRange={props.range}
    dateHandler={dateHandler}
    enable={checkRangeEnabled}
    index={-1}
    dateValidation={dateValidation}
    errorVisible={formValidator.dates}
  />
  // check ranges
  <Box mt={1}>{checkers}</Box>
)

SubjectRangeProject.tsx

 onChange={(e) => {
   if (!props.checkersDateValidator) {
     const startDate = new Date(e.target.value);
     const dueDate = dates.dueDate;
     props.dateChange({ startDate: startDate, dueDate: dueDate }, props.index);
     setDates({ startDate: startDate, dueDate: dueDate });
     props.dateValidation(dates, 'startDate', e);
   } else {
     const startDate = new Date(e.target.value);
     const dueDate = dates.dueDate;
     props.dateChange({ startDate: startDate, dueDate: dueDate }, props.index);
     setDates({ startDate: startDate, dueDate: dueDate });
     const check = props.check ? true : false;
     props.checkersDateValidator(check, props.index, dates, 'startDate', e);
   }
 }}
  • 이벤트 핸들러를 두 개 넣고 조건문으로 이벤트 핸들링을 함.
  • 인라인으로 구현한건 함수로 뺀다고 해도 두개의 핸들러로 유효성 검사를 하는게 너무 거슬림.

리팩토링?

일단은 돌아가는 코드를 짰다. 하지만 불필요한 빈 핸들러도 있고, 이벤트 처리를 인라인 조건문으로 처리하는 것을 도저히 참을 수 없다.

  • 일단 인라인 조건문 핸들링은 함수로 빼내고, ProjectPage.tsx의 핸들러 구현부에 조건문 처리를 하는 방향으로 해본다.
  • 어?

??????????????????????????????????????????

function checkerDateValidateHandler(
  check: boolean,
  index: number,
  range: DateRange,
  dateType: string,
  event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
  if (!check) return;  <= what's this???????
  const dateRange = { ...range, [dateType]: new Date(event.target.value) };
  const checkedDates = [ ...formValidator.checkRanges ];
  ...
}

solution

아래 코드를

if (!check) return;

아래 코드처럼

if (index < 0) { ... }

조건문으로 나눠서 구현 쌉가능...

SubjectRangeProject의 index 프로퍼티를 -1 또는 배열 인덱스로 쓰고 있었음. 메인 range로 쓸 때는 -1을 넣어주고, 리스트 컴포넌트로 사용할땐 배열 인덱스로 쓰고 있으니 index가 0보다 적을때 / 0 이상 일때로 조건을 나눠 이벤트 처리를 하면 좀 더 간결하게 이벤트 핸들링이 가능할것 같다. 아니 가능하다.

블로그 포스팅하면서 솔루션 찾음...
리팩토링 방향을 찾은건 다행이긴 함...

0개의 댓글