티스토리 뷰

넘블챌린지의 두번째 미션이 끝나가고 있다.

이번주에 회사에서 급하게 프로젝트를 진행하느라 참여를 못하다가 10일정도지난 월요일에 챌린지를 시작할 수 있었다. 4일이라는 짧은 기간동안만 참여하여 시간이 엄청 부족했지만 나름대로 구현은 할수있어서 다행이었다.

💻 과제

이번과제는 아래 컴포넌트들을 만드는 미션이었다.

  1. Button Component
    • 일반적인 button element
    • form의 submit
    • a tag를 이용한 페이지 라우팅 (next/link 사용)
  2. Input Component
    • react-hook-form의 register를 사용할 수 있어야 한다.
    • focus시 border-bottom이 파란색으로, error시 빨간색으로 바뀐다.
    • 좌측에 icon이 표시된다.
    • invalid한 값 입력 시 적절한 에러 메세지를 보여준다.
  3. CheckBox Component & CheckBox Group
    • react-hook-form의 register를 사용할 수 있어야 한다.
    • font bold 처리
    • description 유무
    • 포함관계

🤔 아쉬웠던 점

  1. 짧은 기간동안만 참여하게 되면서 깊에 고민해보지 못하고 구현만 생각하여 가이드에 적힌 단단하게 설계된 컴포넌트 구현은 실패한 것 같다.
  2. 이번 미션에서 Compound Component 패턴을 적용해보려 했으나 결국 시간이 부족해 제대로 적용하지 못했다.
  3. 구현 가이드를 이해하는데 시간이 생각보다 많이 들었던거같다.

💡 해결방법

Button Component

  • 버튼 컴포넌트는 굉장히 심플하게 구현하였다. 따로 크기나 디자인커스텀을 막고 정해진 두가지 디자인의 버튼을 theme Props에 따라 바뀌도록 하였고 to 라는 optional prop 을 받아 to 가 있을경우 Link 태그를 사용하도록 구현하였다.
interface IProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  to?: string;
  theme?: "default" | "reverse";
}

const Button = ({ children, type, to, theme = "default" }: IProps) => {
  const Btn = (
    <StyledBtn theme={theme} type={type}>
      {children}
    </StyledBtn>
  );

  return to ? <Link href={to}>{Btn}</Link> : Btn;
};

export default Button;

Input Component

  • react-hook-form 자체는 사용한 경험이 있어 어렵지 않게 구현할 수 있었다. placeholder와 icon(Element를 외부에서 주입), register를 받아 Input component를 구성하고 react-hook-form에서 나온 errorMessage를 받아 errorMessage를 표시한다.
interface IProps extends InputHTMLAttributes<HTMLInputElement> {
  register: UseFormRegisterReturn;
  icon?: React.ReactNode;
  errMessage?: string | null;
}

const Input = ({ register, icon, type, errMessage, placeholder }: IProps) => {
  return (
    <div>
      <InputWrapper isValid={!!errMessage}>
        {icon && <IconWrapper>{icon}</IconWrapper>}
        <CustomInput placeholder={placeholder} type={type} {...register} />
      </InputWrapper>
      {errMessage && <ErrorMsg>{errMessage}</ErrorMsg>}
    </div>
  );
};

export default Input;

CheckBox Component & CheckBox Group

  • 개인적으로 굉장히 아쉬움이 남는 컴포넌트였다. Compound Component pattern을 적용하여 Group Component안에서 내부적으로 로직을 처리하려 했으나 시간안에 구현이 힘들거같아 과감히 지우고 컴포넌트 내부의 값이 변할때 컴포넌트 외부에서 체크된 아이템들을 계산하여 setValue로 처리하였다
// CheckBox Component
interface ICheckBoxProps {
  register?: UseFormRegisterReturn;
  errMessage?: string | null;
  value: string;
  label: string;
  description?: string;
  bold?: boolean;
  onChange?: (isChecked: boolean, id: string) => void;
}

const CheckBox = ({
  register,
  errMessage,
  value,
  label,
  description,
  bold,
  onChange,
}: ICheckBoxProps) => {
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { checked } = e.target;
    if (onChange) {
      onChange(checked, value);
    }
  };

  return (
    <>
      <CheckBoxComponent>
        <CheckBoxWrapper>
          <input
            id={value}
            value={value}
            type="checkbox"
            {...register}
            onChange={(e) => {
              register?.onChange(e);
              handleChange(e);
            }}
          />
          <CustomCheckBox htmlFor={value}>
            <FontAwesomeIcon icon={faCheck} />
          </CustomCheckBox>
          <CheckBoxContent>
            <CheckBoxText bold={bold}>{label}</CheckBoxText>
            {description && (
              <CheckBoxDescription>{description}</CheckBoxDescription>
            )}
          </CheckBoxContent>
        </CheckBoxWrapper>
      </CheckBoxComponent>
      {errMessage && <ErrorMsg>{errMessage}</ErrorMsg>}
    </>
  );
};

export default CheckBox;
// CheckBoxGroup
export type CheckboxValueType = string | number | boolean;

interface IGroupProps {
  children: React.ReactNode;
  border?: boolean;
}

interface ICheckboxGroupContext {
  registerValue: (value: string) => void;
}

export const GroupContext =
  React.createContext<ICheckboxGroupContext | null>(null);

const CheckBoxGroup = ({ children, border }: IGroupProps) => {
	// 구현중 시간이 부족하여 포기하고 남은 잔해들..
  const [registeredValues, setRegisteredValues] = React.useState<
    CheckboxValueType[]
  >([]);

  const registerValue = (value: string) => {
    setRegisteredValues((prev) => [...prev, value]);
  };

  return (
    <GroupContext.Provider value={{ registerValue }}>
      <CheckBoxGroupWrapper border={border}>{children}</CheckBoxGroupWrapper>
    </GroupContext.Provider>
  );
};

export default CheckBoxGroup;

🙋 마치며

회사일이 바빠 일주일이 넘도록 챌린지를 손도 안댄내가 원망스러울 정도로 4일동안 정말 힘들게 달렸던거같다.. 다음 주차부터는 아무리 바빠도 조금씩이라도 해두어야할거같다..

 

 

+추가

갑자기 넘블 기간이 토요일 자정까지로 연장되어 아쉬웠던 부분을 개선할수 있는 시간이 생겼다 연장된 이틀동안 컴포넌트의 비즈니스 로직을 최대한 분리하는 작업을 진행하였고, 분리한 로직을 useCheckBoxItem 이라는 훅으로 만들어 사용하도록 수정하였다.

const useCheckBoxItems = () => {
  const [list, setList] = useState<TCheckBoxItem[]>([]);
  const [allChecked, setAllChecked] = useState(false);

  useEffect(() => {
    if (list.length) {
      setAllChecked(list.every((item) => item.checked));
    }
  }, [list]);

  // 아이디로 해당 체크박스의 상태를 반환하는 함수
  const getItemState = useCallback(
    (id: string) => {
      return list.find((item) => item.id === id)?.checked;
    },
    [list]
  );

  // 부모체크박스의 상태를 변경
  const updateParent = useCallback(
    (id: string) => {
      const item = list.find((v) => v.id === id);
      const parent = list.find((v) => v.id === item?.parentId);
      if (!parent) return;
      const children = list.filter((v) => v.parentId === parent.id);
      if (
        children.length === children.filter((child) => child.checked).length
      ) {
        setList((prev) => {
          return prev.map((item) => {
            if (item.id === parent.id) {
              return { ...item, checked: true };
            }
            return item;
          });
        });
      } else if (
        children.length === children.filter((child) => !child).length
      ) {
        setList((prev) => {
          return prev.map((item) => {
            if (item.id === parent.id) {
              return { ...item, checked: false };
            }
            return item;
          });
        });
      } else {
        setList((prev) => {
          return prev.map((item) => {
            if (item.id === parent.id) {
              return { ...item, checked: true };
            }
            return item;
          });
        });
      }

      updateParent(parent.id);
    },
    [list]
  );

  const setUnchecked = useCallback(
    (id: string) => {
      setList((prev) => {
        return prev.map((item) => {
          if (item.id === id) {
            return { ...item, checked: false };
          }
          return item;
        });
      });
      // 자식이 있으면 자식도 체크해제
      list
        .filter((item) => item.parentId === id)
        .forEach((item) => {
          setUnchecked(item.id);
        });
    },
    [list]
  );

  const setChecked = useCallback(
    (id: string) => {
      setList((prev) => {
        return prev.map((item) => {
          if (item.id === id) {
            return { ...item, checked: true };
          }
          return item;
        });
      });
      // 자식이 있으면 자식도 체크
      list
        .filter((item) => item.parentId === id)
        .forEach((item) => {
          setChecked(item.id);
        });
    },
    [list]
  );

  // 체크박스 상태 변경
  const updateItemState = useCallback(
    (clickedId: string) => {
      const itemState = getItemState(clickedId);
      if (itemState === undefined) return;
      itemState ? setUnchecked(clickedId) : setChecked(clickedId);
      updateParent(clickedId);
    },
    [getItemState, setUnchecked, setChecked, updateParent]
  );

  // 초기 체크박스 리스트 초기화
  const registerValue = (state: TCheckBoxItem) => {
    setList((prev) => {
      if (prev.some((item) => item.id === state.id)) {
        return prev;
      }
      return [...prev, state];
    });
  };

  const setAllItemState = useCallback(() => {
    setList((prev) => {
      return prev.map((item) => {
        return { ...item, checked: !allChecked };
      });
    });
    setAllChecked((prev) => !prev);
  }, [setList, allChecked]);

  return { list, registerValue, updateItemState, allChecked, setAllItemState };
};

export default useCheckBoxItems;

기간이 늘어나 조금더 코드를 개선해 볼수 있었지만 여전히 문제가 많이 남았다 전체클릭과같은 행동에서 리액트훅이 체크박스들을 트래킹 하지 못해 값을 못가져오는 등등 많은 오류가 생겨 깔끔하게 컴포넌트로 분리하지 못했다.

그래서 외부페이지에서 계산하는 로직이 지저분하게 들어가게되었다.. 이부분은 추후 개선이 필요할거같다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday