[원티드 프리온보딩] 프론트엔드 챌린지 사전 과제 - 개선 사항 2-1

바질·2023년 1월 24일
0

원티드 프리온보딩 프론트엔드 챌린지 사전 과제 개선
2023.1.9 ~ 2023.1.20
[코드 리팩토링 2차]

to do list 메인 홈

소스 폴더 구조

📦src
 ┣ 📂api
 ┣ 📂components
 ┃ ┣ 📂auth
 ┃ ┃ ┣ 📂views
 ┃ ┣ 📂common
 ┃ ┃ ┣ 📂view
 ┃ ┗ 📂toDo
 ┃ ┃ ┣ 📂views
 ┣ 📂hook
 ┃ ┣ 📂auth
 ┃ ┣ 📂common
 ┃ ┣ 📂mutation
 ┃ ┗ 📂query
 ┣ 📂screen
 ┣ 📂types

개선할 점
개선 사항 1차에서 고치고 싶은 점을 고쳤다.

1. 사진에서 볼 수 있듯이 to do list를 사용하다가 로그아웃을 하면 상세내용 창이 닫혀야 하는데 닫히지 않는다. ✅
2. to do를 클릭했을 때 빈 상세화면만 나오는 것이다. ✅
3. to do를 삭제했을 때 역시, 닫히지 않고 빈 상세화면이 나온다. ✅
4. 불필요한 코드를 삭제하고 컴포넌트 분리 등의 리팩토링 ✅

그러나 아직, 더 개선해야 할 점이 보여서 계속 진행해보겠다.

개선 사항

  1. 컴포넌트 분리 및 리팩토링 후 모달창 로직 수정 ✅
  2. 커스텀 훅을 최대한 이용하여 코드 리팩토링 ✅
  3. 변경된 값이 없을 시 안내 메세지 띄우기 ✅
  4. 폴더 구조 변경 ✅

code

1. 컴포넌트 분리 및 리팩토링 후 모달창 로직 수정

모달창을 리팩토링 하면서 HTML 구조와 style을 담고있는 ModalView.tsx과 핵심 기능을 담당하는 Modal.tsx 파일, 커스텀 훅인 useModal.tsx파일 3가지로 나누었다.

useModal.tsx

export default function useModal() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const modal = {
    remove(toDoTitle: string) {
      setTitle(`${toDoTitle}을(를) 삭제할까요?`);
      setContent(`이 동작은 복구할 수 없습니다. 할 일에서 영구히 삭제됩니다.`);
      return { title, content };
    },
    update(toDoTitle: string) {
      setTitle(`${toDoTitle}을(를) 변경할까요?`);
      setContent(`이 동작은 복구할 수 없습니다. 할 일이 변경됩니다.`);
      return { title, content };
    },
  };

  return modal;
}

ModalView.tsx

 <Wrapper>
      <div>
        <Title>{toDo.title}</Title>
        <p>{toDo.content}</p> 
      </div>
      <div>
        <button
          onClick={() => {
            setIsSelete(true);
            setIsModalOpen(false);
          }}
        ></button>
        <button
          onClick={() => {
            setIsSelete(false);
            setIsModalOpen(false);
          }}
        >
          아니오
        </button>
      </div>
    </Wrapper>

Modal.tsx

export function Modal({ title: toDoTitle, actionType }: IModalType) {
  const [toDo, setToDo] = useState({
    title: toDoTitle,
    content: "",
  });
  const setIsSelete = useSetRecoilState(isChoosen);
  const setIsModalOpen = useSetRecoilState(isModalOpenAtom);
  const modal = useModal();
  const params = useParams();
	
  useEffect(()=>{
  	setToDo(actionType === "delete" ? modal.remove() : modal.update())
  },[actionType,params.id]);
  
  const modalProps:IModal = {
    setIsSelete,
    setIsModalOpen,
    toDo
  };

  return <ModalView {...modalProps} />;
}

함수는 props로 전달하고 delete, update의 액션을 구분하여 텍스트를 넣어주는 커스텀 훅을 사용했다. 전역 상태 관리를 사용해서 모달 창이 열리는 것과 닫히는 것은 저번 1차에서 설명했으니 넘어가겠다.

문제점 발생
이렇게 하니 작동은 되지만 todo의 title과 actionType에 따른 텍스트가 나오지 않았다. console.log로 확인하니 한박자 느리게 실행되고 있었다.

뭐가 문제일지 생각해봤다. 일단, 컴포넌트를 분리하기 전까지는 정상적으로 작동하는 것을 확인했었다. 분리 후에 일어난 오류였고, 생각해보니 useEffect가 원인일 거라 생각했다.

useEffect를 사용하여 location의 id가 달라지거나 actionType이 달라질 때, state를 변경한다. 그렇다면 컴포넌트가 최초 실행됐을 때는 어떨까? 이 역시도 state는 변경된다. id와 actionType이 변경되었기 때문이다. 이론으로 생각했을 때는 맞는 로직인데 실행하면 오류가 생기니 다른 관점에서 생각했다.

커스텀 훅까지 title이 넘어오는 것을 확인했고 커스텀 훅도 문제가 없었다. 그렇다면, toDo state에 title과 content 값을 담기 전에 props로 전달된 건 아닐까?

즉, useEffect를 사용하여 state값을 변경하고 있지만, 컴포넌트를 실행하면서 state의 값이 변경되는 것을 기다리지 않고, 아무 값이 없는 state(초기값)을 props로 전달하고 렌더링이 끝난 것이라고 결론을 냈다.

그래서 이걸 어떻게 해야될지 고민해봤다. effect를 사용하는 것까진 좋으나 외부로 값을 가져가기 위해서 state를 사용하는 것은 필수적이다. 그렇다면 effect를 사용하지 않는 방법은 뭐가 있을까?

ModalView.tsx

<Wrapper>
      <div>
        {actionType === "delete" ? (
          <>
            <Title>{toDoTitle}() 삭제할까요?</Title>
            <p>이 동작은 복구할 수 없습니다. 할 일에서 영구히 삭제됩니다.</p>
          </>
        ) : (
          <>
            <Title>{toDoTitle}() 변경할까요?</Title>
            <p>이 동작은 복구할 수 없습니다. 할 일이 변경됩니다.</p>
          </>
        )}
      </div>
      <div>
        <button
          onClick={() => {
            setIsSelete(true);
            setIsModalOpen(false);
          }}
        ></button>
        <button
          onClick={() => {
            setIsSelete(false);
            setIsModalOpen(false);
          }}
        >
          아니오
        </button>
      </div>
    </Wrapper>

Modal.tsx

export function Modal({ title: toDoTitle, actionType }: IModalType) {
  const setIsSelete = useSetRecoilState(isChoosen);
  const setIsModalOpen = useSetRecoilState(isModalOpenAtom);

  const modalProps: IModal = {
    setIsSelete,
    setIsModalOpen,
    toDoTitle,
    actionType,
  };

  return <ModalView {...modalProps} />;
}

해결
훅을 사용하지 않고 처음부터 HTML 구조에 텍스트를 담았다. 그리고 actionType을 props로 가져와 조건 렌더링을 시킨다. 이러면 effect를 사용하지 않고도 가능했다. 그리고 id가 바뀌면 상세내용 창이 닫히도록 설정했다.

2. 커스텀 훅을 최대한 이용하여 코드 리팩토링

📦hook
 ┣ 📂auth
 ┃ ┗ 📜useAuth.tsx
 ┣ 📂common
 ┃ ┗ 📜useModal.tsx
 ┣ 📂mutation
 ┃ ┣ 📜useCreate.tsx
 ┃ ┣ 📜useDelete.tsx
 ┃ ┗ 📜useUpdate.tsx
 ┗ 📂query
 ┃ ┗ 📜useGetTodoById.tsx

hook 폴더의 구조이다. 로그인 혹은 회원가입을 할 때, api를 이용한다. 그리고 token이 돌아오면 token을 저장하고 메인 홈으로 리다이렉트 시켜준다. 이걸 login 파일에 두기엔 가독성이 떨어져서 분리했다.

export default function useAuth() {
  const setNoticeMsg = useSetRecoilState(noticeMsgAtom);
  const navigator = useNavigate();
  const seIstLogged = useSetRecoilState(isLogged);

  const handleToken = (token?: string, message?: string, details?: string) => {
    if (!token) {
      return setNoticeMsg(details || null);
    }
    seIstLogged(true);
    setNoticeMsg(message || null);
    localStorage.setItem("token", token);
    navigator("/");
  };

  async function signIn(user: IUser) {
    const { token, message, details } = await userAPi(user, "login");
    return handleToken(token, message, details);
  }
  async function signUp(user: IUser) {
    const { token, message, details } = await userAPi(user, "create");
    return handleToken(token, message, details);
  }
  const auth = {
    signIn,
    signUp,
  };

  return auth;
}

auth.signIn(user)라면 로그인 api를, auth.signUp(user)라면 회원가입 api를 호출한다. 응답이 성공하면 token과 message를 전달해주고, 응답이 실패하면 details를 전달해준다.

응답이 성공했든 실패했든, handleToken를 호출하고 함수는 종료된다. 그렇다면 handleToken을 보자.

  const handleToken = (token?: string, message?: string, details?: string) => {
    if (!token) {
      return setNoticeMsg(details || null);
    }
    seIstLogged(true);
    setNoticeMsg(message || null);
    localStorage.setItem("token", token);
    navigator("/");
  };

3가지의 매개변수를 받는다. 토큰이 없다면, 응답이 실패했다고 생각하고 해당 에러 메세지를 recoil을 통해 사용자에게 확인시켜준다. 그게 아니라면 응답이 성공했으니 로그인을 시켜주고 로그인이 되었다고 사용자에게 알려준 뒤 토큰을 로컬에 저장하고 홈으로 보내준다.

여기까지가 커스텀 훅을 이용해 리팩토링한 내용이다.

3. 변경된 값이 없을 시 안내 메세지 띄우기

 const ChangeToDoProps: IChangeTodoProps = {
    onSubmit(e) {
      e.preventDefault();
      if (!isDisabled) return;
      if (newToDo.title === "" && newToDo.content === "")
        return setNoticeMsg("값이 변경되지 않았습니다.");
      if (newToDo.title === "")
        setNewToDo({ ...newToDo, title: toDo?.title || "" });
      if (newToDo.content === "")
        setNewToDo({ ...newToDo, content: toDo?.content || "" });
      setNewToDo({ title: "", content: "" });
      setActionType("update");
      setIsOpen(true);
    },

조건문을 여러가지로 작성했다. isDisabled은 버튼의 비활성화, 활성화를 담당한다. 버튼이 비활성화되었을 때, 폼을 제출했다면 return 시킨다.

newToDoChangeToDoinput의 값이 들어온다. 따라서 해당 값이 ""라면 아무것도 입력하지 않은 상태로 폼을 제출했다는 것이 되므로 값이 변경되지 않았다고 사용자에게 알려준 뒤 함수를 종료한다.

또 다른 조건으로는 title만 변경되었을 때, content만 변경되었을 때이다. 이 경우에는 변경되지 않은 항목에 기존 데이터를 넣어주어야 하기 때문에 setNewToDo를 사용하여 데이터를 넣어준다.

그리고 가장 어이없는 실수가 발생했다. 이후 폼을 제출하면 값이 나오지 않는 이상한 에러가 있었는데, 바로 밑에 있는 setNewToDo({ title: "", content: "" }); 코드 때문이었다. 나는 이걸 발견하지 못했고, 또 다른 방법을 사용해 해결했다.
내가 왜그랬지? 왜 발견을 못했을까

newToDo에 남아있는 값을 초기화해주려고 한 모양이다. 폼을 제출하고나서 다시 제출하게 되면, 중복 제출로 인정이 되기 때문에 그 후 값을 초기화해서 변경되지 않았다면 사용자에게 알려주고자 했다.

 const ChangeToDoProps: IChangeTodoProps = {
    onSubmit(e) {
      e.preventDefault();
      if (!isDisabled) return;
      if (newToDo.title === "" && newToDo.content === "")
        return setNoticeMsg("값이 변경되지 않았습니다.");
      if (newToDo.title === "")
        setNewToDo({ ...newToDo, title: toDo?.title || "" });
      if (newToDo.content === "")
        setNewToDo({ ...newToDo, content: toDo?.content || "" });
      setActionType("update");
      setIsOpen(true);
      setNewToDo({ title: "", content: "" });
    },

그러려면, 코드의 순서를 제일 마지막으로 보내야 하지 않을까 싶다. 폼 제출은 actionType에 의해 관리된다. 즉, actionType이 트리거이다. 그리고 모달 창이 열리면 그때 state를 초기화 해주는 순서가 맞다.

4. 폴더 구조 변경

과제를 제출하면서 다른 분들의 과제 또한 경험할 수 있는 기회가 있었다. 살펴보니 파일 구조를 잡는 게 나와는 전혀 달랐고, 실제 오픈 소스에서 사용하는 듯한 구조였다. 저런 게 리팩토링이구나 싶었다. 예전의 나는 파일 구조는 생각하지 못하고 코드만 깔끔하게 줄이면 끝이라고 생각했었다. 그래서 다른 분의 구조를 보며 최대한 참고하여 폴더를 분리했다.


이상으로 개선 사항 2-1을 마치겠다. 아직, 개선할 부분이 남아있어 2-2가 마지막이 될 것 같다.

0개의 댓글