원티드 프리온보딩 프론트엔드 챌린지 사전 과제 개선
2023.1.9 ~ 2023.1.20
[코드 리팩토링 1차]
공부하면서 진행하는 챌린지이기 때문에 css에 시간을 많이 들이지는 않았다. 적당히 볼 수 있을 정도로 스타일링 했고, 로그인을 해야만 to do list를 확인할 수 있다.
작동은 위의 영상과 같이 돌아간다. 로그인에 실패하면 실패하였다 안내해주고, 로그인을 한 뒤에 todo list를 작성할 수 있다.
삭제와 수정이 가능하고 실시간으로 반영된다. 또한 todo list는 로컬에 저장되어 새로고침을 해도 유지된다.
(저장은 서버에서 진행되고, 서버는 사전 과제로 깃헙에서 받았다.)
개선할 점
영상에서도 보이듯 사용하면서 버그가 보인다. 하나씩 고쳐나가면서 추가하고 싶은 기능도 추가해보겠다.
- 사진에서 볼 수 있듯이 to do list를 사용하다가 로그아웃을 하면 상세내용 창이 닫혀야 하는데 닫히지 않는다.
- to do를 클릭했을 때 빈 상세화면만 나오는 것이다.
- to do를 삭제했을 때 역시, 닫히지 않고 빈 상세화면이 나온다.
- 불필요한 코드를 삭제하고 컴포넌트 분리 등의 리팩토링
- 모달 및 안내 메세지 추가
(로그인, 계정 생성, 할 일 생성, 삭제, 수정 시)
Modal.tsx
export function Modal({ actionType, title: todoTitle }: IProps) {
const [title, setTitle] = useState("");
const [text, setText] = useState("");
const setIsChoosen = useSetRecoilState(isChoosen);
const setIsModalOpen = useSetRecoilState(isModalOpen);
useEffect(() => {
if (actionType === "delete") {
setTitle(`${todoTitle}을(를) 삭제할까요?`);
setText(`이 동작은 복구할 수 없습니다. 할 일에서 영구히 삭제됩니다.`);
} else {
setTitle(`${todoTitle}을(를) 변경할까요?`);
setText(`이 동작은 복구할 수 없습니다. 할 일이 변경됩니다.`);
}
}, []);
...
모달의 경우에는 actionType
과 title
이라는 2개의 props을 받는다.
actionType
은 string으로 "delete"
, "update"
두 개가 들어오는 데, 삭제와 변경 시에 다른 문구로 state를 변경해줘야 하기 때문에 모달 컴포넌트를 사용 시 props로 전달해줘야 한다.
title
은 to do의 제목이다.
setIsChoosen
,setIsModalOpen
이라는 전역 변수(상태 관리)를 만들었다.
setIsChoosen
은 모달을 띄웠을 때, 예, 아니오로 버튼을 클릭한다. 예를 누르면 to do를 삭제 혹은 수정해줘야 하고 아니오를 누르면 어떠한 상태 변경 없이 모달창을 닫아야한다.
setIsModalOpen
은 아니오를 눌렀을 때, 혹은 삭제, 수정 버튼을 눌렀을 때 모달 창을 열거나 닫아야 한다.
Notice.tsx
export default function Notice() {
const [noticeMsg, setNoticeMsg] = useRecoilState(noticeMsgAtom);
useEffect(() => {
const timer = setTimeout(() => {
setNoticeMsg(null);
}, 2000);
return () => clearTimeout(timer);
}, [noticeMsg]);
...
notice는 간단한 안내 메세지를 띄운다. 따라서 다른 컴포넌트의 동작에 따라 안내 메세지를 받아야 한다. 안내 메세지는 전역 상태 관리에 의해 atom으로부터 받는다.
안내 메세지가 뜨면, 2초 후에 사라지게끔 setTimeout
을 걸었다. 그리고 notice를 null
로 만들어준다.
저번 게시글인 to do의 삭제/수정을 구현했던 코드를 분리해서 개선시켜보았다. 한 곳에 삭제와 수정을 진행해서 코드가 길고 가독성이 떨어졌다.
우선, 크게 삭제 함수와 수정 함수를 분리했다.
Edit.tsx
export function UpdateTodo() {
const queryClient = useQueryClient();
const setChangeTodo = useSetRecoilState(isChange);
const setNoticeMsg = useSetRecoilState(noticeMsgAtom);
const id = useRecoilValue(todoId);
return useMutation((newTodo: INewTodo) => updateTodoApi(newTodo), {
onSuccess: () => {
setNoticeMsg("할 일을 변경하였습니다.");
queryClient.invalidateQueries(["todo", id]);
setChangeTodo((prev) => !prev);
},
onError: () => {
setNoticeMsg("할 일을 변경하는 데 실패했습니다. 당신 잘못이 아니에요.");
},
});
}
export function DeleteTodo() {
const queryClient = useQueryClient();
const setChangeTodo = useSetRecoilState(isChange);
const setNoticeMsg = useSetRecoilState(noticeMsgAtom);
const id = useRecoilValue(todoId);
return useMutation((config: IDelete) => deleteTodoApi(config), {
onSuccess: () => {
setNoticeMsg("할 일을 삭제했습니다.");
queryClient.invalidateQueries(["todo", id]);
setChangeTodo((prev) => !prev);
},
onError: () => {
setNoticeMsg("할 일을 삭제하는 데 실패했습니다. 당신 잘못이 아니에요.");
},
});
}
Edit.tsx에 삭제와 수정 함수를 넣고 export했다. 처음에는 개별로 파일을 생성할까 생각했는데, 코드의 양이 적고, 하는 일이 수정과 삭제밖에 없어서 하나의 파일에 넣었다.
지금 다시 생각해보면, 프로젝트가 이보다 더 커졌을 때는 관리하기 힘들 것 같다. 그래서 다시 개선할 때는 별도의 파일로 만들어볼 생각이다.
ChangeTodos.tsx
...
useEffect(() => {
const config = {
...todo,
id: id || "",
token: token || "",
};
if (isModalChoosen && modalType.actionType === "delete") {
const deleteTodoFn = DeleteTodo();
deleteTodoFn.mutate(config);
} else if (isModalChoosen && modalType.actionType === "update") {
const updateTodoFn = UpdateTodo();
updateTodoFn.mutate(config);
}
setIsModalChoosen(false);
}, [isModalChoosen]);
...
우선, 앞서 설명한듯이 버튼을 눌렀을 때, 모달창의 상태가 false
에서 true
로 변경된다. 그렇다면 todo를 변경하겠다는 것이므로, 조건문을 사용해주었다.
또한 모달 타입이 delete
일 때는 삭제 함수를 사용하고, update
일 때는 수정 함수를 사용했다.
코드가 불필요하게 복잡한 것 같아 코드의 개선이 더 필요하다고 느낀다. state를 줄일 수 있는 방법이 있지 않을까 고민해야겠다.
const handleToken = (token?: string, message?: string, details?: string) => {
if (!token) {
console.clear();
return setNoticeMSg(details || null);
}
seIstLogged(true);
setNoticeMSg(message || null);
localStorage.setItem("token", token);
navigator("/");
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(user, signUpMatch);
const { password, confirmPassword } = user;
if (password !== confirmPassword)
return setNoticeMSg("비밀번호가 같지 않습니다.");
if (signUpMatch) {
const { token, message, details } = await userAPi(user, "create");
return handleToken(token, message, details);
}
// login api
const { token, message, details } = await userAPi(user, "login");
handleToken(token, message, details);
};
form을 제출하면 user에 정보를 담고, 현재 endpoint
가 /auth/sign-up
이라면 if
문 안의 내용이 실행된 뒤 종료하도록 했다.
token
을 저장하고 안내 메세지를 띄우는 등의 공통적 액션은 함수로 빼주어 재사용을 높였다.
여기서 나는 로그인과 회원가입의 컴포넌트를 나누었다. 정확히는 로그인 컴포넌트를 확장해서 회원가입 컴포넌트를 만들었고, 여전히 이어져있다. 따라서 로그인 컴포넌트에 과한 로직이 들어있다고 느낀다.
처음 리팩토링 할 때는 단순히, 로그인 후에 회원이 없으면 회원가입으로 이동하는 모습이 꽤 신선해서 따라하고 싶어 최대한 건드리지 않았다.
그러나 지금은 컴포넌트도 분리되었고, url도 따로 빼서 자식 컴포넌트로 회원가입을 만들었기에 큰 의미는 없는 것 같다.
추가적인 고민이 필요할 것 같다. 아예 페이지를 분리시켜 하나의 일만 하도록 만들든지, 혹은 조금 더 리팩토링하여 내가 원하던 모양으로 구현해볼 수도 있을 것 같다. 우선, 후자를 고민해보고 안되면 전자로 가야겠다.
여기까지 진행한 리팩토링을 정리했다. 코드를 리팩토링하다보니 어제의 코드보다 오늘의 코드가 더 개선됐다고 느낀다. 평소의 나는 리팩토링을 하지 않았다. 나중에 하면 되지
라고 생각했는데, 지금에 와서 다시 생각해보면 바로 해야한다
라고 느낀다. 코드를 만드는 것보다 기능을 유지하며 코드를 줄여나가는 것이 더 어렵고 고민이 많이 된다. 이렇게 하면서 배우는 것도 있는 것 같다.
프리온보딩 신청하길 잘했다...^^!
아직도 코드가 복잡해보이는 것 같다. 1차 개선을 하면서 느낀 것을 토대로 다시 한 번 코드의 리팩토링을 시도해봐야겠다. 앞서 설명한 고치고 싶은 점을 만져보고, 오늘 하는 강의를 듣고 다시 수정해야겠다.
또한 최대한 재사용을 염두에 두고 썼지만 오히려 그렇게 하다보니 복잡하게 코드가 얽혀있고 나중에 관리하고 보수하기에 까다로워진 느낌이다. 이걸 풀고 최대한 간결하게 하여 보수하기 용이하도록 만들어야 겠다.
그리고 자료 구조를 손보고 싶다. 누가 알려주는 것이 아니어서 강의를 보며 막연하게 파일과 폴더를 쌓았는데, 이제는 제대로 알고 사용하고 싶다...