editRefect

해당 컴포넌트는 유저의 자소서 리스트를 관리하기 위한 목적으로 작성되었습니다.

  • EDIT라는 명칭은 의미를 파악하기 힘들었습니다.

  • 이전 문제와 동일하게 컴포넌트의 관심사 분리 & 매직넘버 문제가 있었습니다.

  • Props드릴링 문제도 발생했습니다.

상세 문제점 파악

  • 표현이 애매한 도메인명

    • EDIT : 편집 무엇을 편집?

  • 복잡한 페이지 라우팅 구조 & 명칭

    • myEssay : 자소서 확인 페이지

    • edit : 자소서 작성 페이지 라우팅

    • edit essay=2 : 자소서 수정 페이지

  • idx로 이루어진 map 키값

EDIT 해결 방안

  • 대표 도메인명 수정

    • coverLetter: 자기소개서

  • 복잡한 라우팅 & 명칭 개선

    • coverLetter : 자소서 확인 페이지

    • coverLetter/new : 자소서 작성 페이지

    • coverLetter/[id]/edit: 자소서 편집 페이지

  • 컴포넌트 관심사 분류

  • UUID4()를 키값으로 사용하기

  • 트리가 깊은 경우 리덕스 활용

자기소개서 리팩토링 적용전

유저 자소서 리스트 관리

  • 유저가 선택한 자소서 넘버 상태를 관리 하기 위한 로직이 Props드릴링 구조의 주요 원인이라 판단되었습니다.

유저 자소서 생성 & 편집

생성과 편집의 스타일이나 로직이 비슷하다는 이유로 하나의 컴포넌트에서 이를 처리하려 했던 안일한 접근이 문제였습니다.

  • isEdit 상태에 대한 의존도가 높아져 가독성이 떨어지고 유지보수 측면에서 비효율적이었습니다.

  • 삼항 연산자를 이용하여 기능을 분류하는 방식은 절차적인 성향의 코드에 가까워 컴포넌트 간의 의존도를 높이고 유지보수 측면에서 매우 비효율적이며 가독성을 떨어트렸습니다.


[ 해결방안]

  • 컴포넌트 분리: 생성 기능과 편집 기능을 별도의 컴포넌트로 분리하였습니다. 커스텀 훅 활용: 비슷하고 겹치는 비즈니스 로직은 커스텀 훅으로 분리하여 상태 관리를 효율적으로 하였습니다.

  • 명칭 변경: handle ~~ 형태의 불명확한 명칭을 최대한 직관적으로 변경하였습니다.

  • 고유 키 값 사용: 서버에서 id 값을 지정해주기 때문에 key 값에 크게 신경 쓰지 않았으나, CRUD 관련 컴포넌트의 맵핑에서는 key 값을 유니크한 값으로 지정하는 것이 옳다고 판단하여 uuid4()를 채택하였습니다.

EDIT 리팩토링 적용전 코드

리팩토링전 코드
type QnAEditorProps = {
  isEdit: boolean;
};

const QnAEditor = ({ isEdit }: QnAEditorProps) => {
  {
    /**리덕스에 대한 선언 */
  }
  const router = useRouter();
  const dispatch = useDispatch();
  /**나의 자소서를 호출중 판단여부 */
  const readEssayLoading = useSelector(
    (state: RootReducerType) => state.essay.loading,
  );
  /**나의 자소서 리스트 */
  const readEssayList = useSelector(
    (state: RootReducerType) => state.essay.readEssayList,
  );
  /**관리자의 샘플 리스트 */
  const templateReducer: InitiaState = useSelector(
    (state: RootReducerType) => state.template,
  );

  const [qnaLists, setQnALists] = useState<QnAList[]>([]);
  const [templateTitle, setTemplateTitle] = useState<string>("커스텀자소서");
  const [mainTitle, setMainTitle] = useState<string>("");
  const [qnaContent, setQnAContent] = useState<qnaList>([]);
  const [isOpenModal, setIsOpenModal] = useState<boolean>(false);
  const [modalMsg, setModalMsg] = useState<string>("");
  const essayId = isEdit
    ? (JSON.parse(router.query.essayId as string) as number)
    : 0;

  /** ESSAY의 로딩이 완료가되면 useState에 반영한다. */
  useEffect(() => {
    if (!readEssayLoading) {
      if (readEssayList[0].title === "") {
        return;
      }
      setQnALists(readEssayList);
      setTemplateTitle(readEssayList[0]?.title);
    }
  }, [readEssayLoading]);

  /** 템플릿의 로딩이 완료가되면 useState에 반영한다. */
  useEffect(() => {
    if (!templateReducer.loading) {
      const customTemplate = {
        templateId: 10000,
        title: "커스텀자소서",
        qnaList: [],
      };
      setQnALists(cur => [customTemplate, ...templateReducer.templateList]);
    }
  }, [templateReducer.templateList]);
  /** 수정모드에서는 기존의 리스트를 반환, 작성모드에서는 선택된 템플릿 리스트를 반환 */
  const getQuestionItem = useCallback(() => {
    if (isEdit) {
      return qnaLists;
    }
    const filteItem = qnaLists.filter(list => list.title === templateTitle);
    return filteItem;
  }, [templateTitle, qnaLists]);

  const handleChangeMainTitle = (title: string) => {
    setMainTitle(title);
  };

  /**질문FORM 추가 함수 */
  const handleAddQnA = () => {
    if (templateTitle === "") {
      return;
    }
    setQnALists(prevLists => {
      const newLists = [...prevLists];
      const filterIndex = newLists.findIndex(a => a.title === templateTitle);
      const newContent = [...newLists[filterIndex].qnaList, " "];
      newLists[filterIndex].qnaList = newContent;
      return newLists;
    });
  };

  /**현재 질문리스트 수정 */
  const handleChangeQnA = (index: number, question: string, answer: string) => {
    const newList = [...qnaContent];
    newList[index].question = question;
    newList[index].answer = answer;
    setQnAContent(newList);
  };

  /**현재 질문리스트의 정보를 전달 */
  const handleQnASelect = (index: number, question: string, answer: string) => {
    const filterList = qnaContent[index];
    if (filterList) {
      return;
    } else {
      setQnAContent(curList => [
        ...curList,
        { qnaId: index, question, answer: answer },
      ]);
    }
  };

  /**필터링후 자소서 작성 반영 */
  const handleSubmit = () => {
    const filterJudge = qnaContent.filter(
      a => a.answer === "" || a.question === "",
    ).length;
    if (!isEdit && mainTitle === "") {
      setIsOpenModal(true);
      setModalMsg("메인 제목을 입력해주세요");
      return;
    }
    if (qnaContent.length < 1) {
      setIsOpenModal(true);
      setModalMsg("질문과 답변은 1개이상 작성해주세요.");
      return;
    }
    if (filterJudge > 0) {
      setIsOpenModal(true);
      setModalMsg("질문 답변의 내용을 채워주세요");
      return;
    }
    const data = {
      title: isEdit
        ? mainTitle === ""
          ? templateTitle
          : mainTitle
        : mainTitle,
      qnaList: qnaContent,
    };
    if (isEdit) {
      dispatch(updateEssay(data, essayId));
      router.replace("/myEssay");
      return;
    }
    dispatch(addEssay(data, Number(Cookies.get("useId"))));
    router.replace("/myEssay");
  };


  /**모드1은 액션이 없는 모달, 모드2 삭제액션이 담긴 모달 */
  const handleModal = (mode: number) => {
    if (mode === 1) {
      setIsOpenModal(false);
    }
    if (mode === 2) {
      setModalMsg(`${essayId}번 자소서를 정말 삭제하시겠습니까?`);
      setIsOpenModal(true);
    }
  };

  /**자소서 삭제*/
  const handleDeleteEssay = () => {
    dispatch(deleteEssayList(essayId));
    router.push("/myEssay");
  };

  return (
    <QnAEditorStyle>
      <h2>{isEdit ? "자소서 수정하기" : "자소서 작성하기"}</h2>

      <TopContainer>
        <QnAListTitle onChange={handleChangeMainTitle} />
        <ControlMenu
          Size={{ width: `${v.lgItemWidth}`, height: "40px" }}
          value={templateTitle}
          optionList={qnaLists}
          onChange={setTemplateTitle}
        />
        {qnaLists &&
          getQuestionItem().map(list => (
            <QuestionItmesContainer
              key={isEdit ? list.essayId : list.templateId}
            >
              {list.qnaList &&
                list.qnaList.map((qna, idx) => (
                  <QuestionItem
                    key={idx}
                    content={qna}
                    onChange={handleChangeQnA}
                    curInfo={handleQnASelect}
                    index={idx}
                  ></QuestionItem>
                ))}
            </QuestionItmesContainer>
          ))}
      </TopContainer>

      <BottomContainer>
        <BiPlus onClick={handleAddQnA}></BiPlus>
        <ControlButtonContainer>
          <div>
            <CustomButton
              Size={{ width: "150px", font: "20px" }}
              onClick={() => router.push("/myEssay")}
              text="뒤로가기"
            />
          </div>
          <div>
            {isEdit ? (
              <CustomButton
                Size={{ width: "150px", font: "20px" }}
                onClick={() => handleModal(2)}
                text="삭제하기"
              />
            ) : (
              <></>
            )}
            <CustomButton
              Size={{ width: "150px", font: "20px" }}
              onClick={handleSubmit}
              text={isEdit ? "수정완료" : "작성완료"}
            />
          </div>
        </ControlButtonContainer>
      </BottomContainer>

      {isOpenModal && (
        <Modal
          isOpen={isOpenModal}
          onClose={() => handleModal(1)}
          onAction={isEdit ? handleDeleteEssay : () => handleModal(1)}
          contents={{
            title: "경고",
            content: modalMsg,
          }}
        ></Modal>
      )}
    </QnAEditorStyle>
  );
};

export default QnAEditor;
  • 주석이 없으면 이해할수 없는 코드와 구조

리팩토링 적용후(코드 일부)

const CoverLetterEdit = () => {
  const [coverLetterTitle, setCoverLetterTitle] = useState < string > "";
  const router = useRouter();
  const moveCoverLetterMainPage = "/coverLetter";
  const { id } = router.query;

  const { qnaList, setQnAList, deleteQnAList, changeQnAList, addQnAList } =
    useCoverLetterCreatorLogic();

  const {
    loading,
    targetQnAData,
    getDetailEssayList,
    coverLetterMainTitle,
    changeCoverLetter,
    deleteCoverLetter,
  } = useCoverLetterManager();

  useEffect(() => {
    getDetailEssayList(Number(id));
  }, []);

  useEffect(() => {
    if (!loading) {
      setQnAList(targetQnAData);
      setCoverLetterTitle(coverLetterMainTitle);
    }
  }, [targetQnAData]);

  if (loading) return <p>로딩중...</p>;

  return (
    <CoverLetterCreatorContainer>
      <MainTitle>자소서 수정하기</MainTitle>
      <CoverLetterTitle
        value={coverLetterTitle}
        onChange={(e) => setCoverLetterTitle(e.target.value)}
        placeholder="자소서 제목"
      ></CoverLetterTitle>
      {qnaList.map((qna, i) => (
        <CoverLetterQuestionItems
          key={qna.qnaId}
          item={qna}
          onDelete={deleteQnAList}
          onUpdate={changeQnAList}
        ></CoverLetterQuestionItems>
      ))}
      <BiPlusStyled onClick={addQnAList}></BiPlusStyled>
      <ControllerBtns>
        <CustomButton
          Size={{ width: "150px", font: "20px" }}
          onClick={() => router.push(moveCoverLetterMainPage)}
          text={"뒤로가기"}
        />
        <CustomButton
          Size={{ width: "150px", font: "20px" }}
          onClick={() => deleteCoverLetter(Number(id))}
          text={"삭제하기"}
        />
        <CustomButton
          Size={{ width: "150px", font: "20px" }}
          onClick={() =>
            changeCoverLetter(Number(id), coverLetterTitle, qnaList)
          }
          text={"수정완료"}
        />
      </ControllerBtns>
    </CoverLetterCreatorContainer>
  );
};

export default CoverLetterEdit;
  • 비즈니스 로직을 외부로 분리하여 가독성과 관리가 용이 해졌습니다.

  • 비즈니스 로직을 커스텀훅으로 분리하여 재 사용할 수 있는 가능성이 생겼습니다.

  • 유지보수 측면에서 효율적으로 변했습니다.

  • 컴포넌트 자체는 UI렌더링과 간단한 상태관리에 집중되어 단일 책임 원칙을 지켰습니다.

리팩토링 후 구조

컴포넌트

📦CoverLetter
┣ 📂edit
┃ ┗ 📜CoverLetterEdit.tsx
┣ 📂hooks
┃ ┣ 📜useControlTemplate.ts
┃ ┣ 📜useCoverLetterCreatorLogic.ts
┃ ┗ 📜useCoverLetterManager.ts
┣ 📂new
┃ ┣ 📜CoverLetterCreator.tsx
┃ ┗ 📜CoverLetterQuestionItems.tsx
┣ 📜CoverLetter.tsx
┣ 📜CoverLetterItems.tsx
┣ 📜CoverLetterList.tsx
┗ 📜CoverLetterPreView.tsx

라우팅

📦coverLetter
 ┣ 📂new
 ┃ ┗ 📜index.tsx
 ┣ 📂[id]
 ┃ ┗ 📂edit
 ┃ ┃ ┗ 📜index.tsx
 ┗ 📜index.tsx

리덕스

📦Essay
 ┣ 📂server
 ┃ ┣ 📜actions.tsx
 ┃ ┣ 📜reducer.tsx
 ┃ ┗ 📜types.tsx
 ┗ 📂user
 ┃ ┣ 📜actions.ts
 ┃ ┣ 📜reducer.ts
 ┃ ┗ 📜types.ts

Last updated