새해 시작과 동시에 원티드 프리온보딩 프론트엔드 챌린지를 지원했다.
사전과제는 회원가입 & 로그인과 Todo List 구현하는 것이였다.
지금까지 공부했던 내용들을 복습하며 만들어 보았다.
@
, .
포함Todo List API를 호출하여 Todo List CRUD 기능을 구현해주세요
한 화면 내에서 Todo List와 개별 Todo의 상세를 확인할 수 있도록 해주세요.
한 페이지 내에서 새로고침 없이 데이터가 정합성을 갖추도록 구현해주세요
로그인 경로와 회원가입 경로를 따로 만들어서 구성했다.
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "",
element: <Home />,
},
{
path: "auth",
element: <Login />,
},
{
path: "sign",
element: <CreateAccount />,
},
],
errorElement: <NotFound />,
},
]);
로그인 Form과 회원가입 Form 모두 React-Hook-Form을 이용하여 이메일과 비밀번호의 유효성 검사를 진행하였다.
이 부분을 처음에는 useForm()에서 formState.errors값의 유무로 버튼 활성화/비활성화 되도록 코드를 작성하였다.
<Button
value="로그인"
disabled={Object.keys(errors).length !== 0}
type="submit"
/>
이렇게 하고 나니 맨처음 화면에 진입하였을 때에는 errors에 아무것도 없기때문에 버튼이 활성화 되어있는 상태였고, Email이나 Password에 무언가를 입력해야 errors에 값이 들어왔었다.
처음부터 버튼이 비활성화되는 상태를 원했었기 때문에 어떻게 해야할 지 고민하다가, 공식문서에서 formState.isValid
를 찾게 되었고, 이를 이용하면 원했던 방식으로 구현이 됐었다.
<Button value="로그인" disabled={!isValid} type="submit" />
useForm()에서 formState : { errors , isValid }
를 활용하여, 유저에게 error message표시 및 버튼 활성화여부를 설정할 수 있도록 하였다.
// 🟡 Login.tsx
function Login() {
const {
register,
handleSubmit,
formState: { errors, isValid },
getValues,
setValue,
} = useForm({
mode: "onChange",
defaultValues: {
email: "",
password: "",
},
});
const navigate = useNavigate();
const [loginError, setLoginError] = useState(false);
const [logIn, setLogIn] = useRecoilState(isLoggedIn);
const onSubmitValid = async () => {
const userInfo = getValues();
const result = await login(userInfo);
const { details, token } = result;
if (details !== undefined) {
setLoginError(true);
setValue("email", "");
setValue("password", "");
return;
}
setLoginError(false);
localStorage.setItem("TOKEN", token);
localStorage.setItem("isLoggedIn", "true");
navigate("/todo");
setValue("email", "");
setValue("password", "");
};
useEffect(() => {
setLogIn(Boolean(localStorage.getItem("isLoggedIn")));
if (logIn) {
navigate("/todo");
}
}, [logIn, setLogIn, navigate]);
return (
<Layout>
<FormBox>
<Title>{loginError ? "로그인에 실패했습니다" : "로그인"}</Title>
<form onSubmit={handleSubmit(onSubmitValid)}>
<Input
{...register("email", {
required: "이메일을 입력해주세요.",
pattern: {
value: /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/,
message: "이메일의 형식이 맞지 않습니다.",
},
})}
placeholder="Email"
hasError={Boolean(errors?.email?.message)}
autoComplete="off"
/>
<Error>{errors?.email?.message}</Error>
<Input
{...register("password", {
required: "비밀번호를 입력해주세요.",
minLength: {
value: 8,
message: "최소 8자 이상의 비밀번호를 입력해주세요.",
},
})}
placeholder="Password"
type="password"
hasError={Boolean(errors?.password?.message)}
autoComplete="off"
/>
<Error>{errors?.password?.message}</Error>
<Button value="로그인" disabled={!isValid} type="submit" />
<Link to={"/sign"}>계정이 없습니까 ? ➡️</Link>
</form>
</FormBox>
</Layout>
);
}
export default Login;
TodoList의 목록과 상세내용을 동시에 보여줘야 해서 다음과 같이 router를 구성하였다.
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "",
element: <Home />,
},
{
path: "auth",
element: <Login />,
},
{
path: "sign",
element: <CreateAccount />,
},
{
path: "todo",
element: <Todo />,
children: [
{
path: ":todoId",
element: <TodoContent />,
},
],
},
],
errorElement: <NotFound />,
},
]);
Recoil을 이용하여 TodoList 저장 할 전역변수를 만들고, 로그인 후 /todo 진입 시 서버에 TodoList를 요청한다.
받은 결과값을 전역변수에 저장 한 뒤에 목록에 표시되도록 코드를 작성하였다.
// 🟡 Todo.tsx
function Todo() {
const [toDos, setToDos] = useRecoilState(toDosListAtom);
const getTodoList = async () => {
const todoList = await getTodos().then((res) => res);
setToDos(todoList.data);
};
useEffect(() => {
getTodoList();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Wrapper>
<Title>할 일 목록</Title>
<hr />
<CreateTodo />
<hr />
<TodoListContainer>
<TodoList>
{toDos.map((todo) => (
<CTodo key={todo.id} id={todo.id} title={todo.title} />
))}
</TodoList>
<Outlet /> // <TodoContent /> 를 불러옴
</TodoListContainer>
</Wrapper>
);
}
export default Todo;
할 일 생성하는 부분도 React-Hook-Form 을 사용하였다.
// 🟡 CreateTodo.tsx
function CreateTodo() {
const {
register,
handleSubmit,
formState: { errors },
getValues,
setValue,
} = useForm({
mode: "onChange",
defaultValues: {
todo: "",
content: "",
},
});
const navigate = useNavigate();
const setToDos = useSetRecoilState(toDosListAtom);
const handleValid = async () => {
const { todo, content } = getValues();
const data = {
title: todo,
content,
};
const result = await createTodo(data);
setToDos((oldToDos) => [...oldToDos, result.data]);
navigate(`/todo/${result.data.id}`);
setValue("todo", "");
setValue("content", "");
};
return (
<form onSubmit={handleSubmit(handleValid)}>
<Input
{...register("todo", {
required: "제목은 필수사항 입니다.",
})}
placeholder="제목을 적어주세요."
hasError={Boolean(errors?.todo?.message)}
autoComplete="off"
/>
<Error>{errors?.todo?.message}</Error>
<Input
{...register("content")}
placeholder="내용을 적어주세요."
hasError={Boolean(errors?.content?.message)}
autoComplete="off"
/>
<Button value="등록하기" type="submit" />
</form>
);
}
export default CreateTodo;
Todo의 상세내용 부분에서 수정버튼을 누르게 되면 수정모드가 활성화 되도록 하였다.
또한 수정 모드에서 저장버튼을 누르면 Update가 진행이 되고, 취소버튼을 누르면 변경사항이 반영되지 않도록 하였다.
// 🟡 TodoContent.tsx
function TodoContent() {
const { todoId } = useParams();
const [todo, setTodo] = useRecoilState(toDoAtom);
const setToDos = useSetRecoilState(toDosListAtom);
const [isChange, setIsChange] = useState(true); //수정모드감시
const navigate = useNavigate();
const { register, handleSubmit, setValue, getValues } = useForm();
const getContent = async () => {
const result = await getTodoById(todoId);
setTodo(result.data);
};
const handleChangeTitleInput = (event: any) => {
setTodo({ ...todo, title: event.target.value });
};
const handleChangeContentInput = (event: any) => {
setTodo({ ...todo, content: event.target.value });
};
const handleModify = () => {
setIsChange(!isChange);
};
const getTodoList = async () => {
const todoList = await getTodos().then((res) => res);
setToDos(todoList.data);
};
useEffect(() => {
getContent();
setValue("toDoTitle", todo?.title);
setValue("toDoContent", todo?.content);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [todoId, isChange]);
const onSubmitValid = async () => {
const { toDoTitle, toDoContent } = getValues();
setIsChange(!isChange);
const data = {
title: toDoTitle,
content: toDoContent,
};
await updateTodo(data, todoId);
getTodoList();
};
const handleDeleteTodo = async () => {
await deleteTodo(todoId);
setToDos((arr) => {
return arr.filter((data) => data.id !== todoId);
});
navigate(`/todo`);
};
return (
<TodoList>
<Wrapper>
<form onSubmit={handleSubmit(onSubmitValid)}>
<Icon>
{isChange ? (
<FontAwesomeIcon icon={faPen} onClick={handleModify} />
) : (
<FontAwesomeIcon icon={faXmark} onClick={handleModify} />
)}
{isChange ? null : (
<Button type="submit">
<FontAwesomeIcon icon={faSave} />
</Button>
)}
<FontAwesomeIcon icon={faTrash} onClick={handleDeleteTodo} />
</Icon>
<hr />
<InputData
{...register("toDoTitle")}
value={todo?.title || ""}
onChange={handleChangeTitleInput}
disabled={isChange}
autoComplete="off"
/>
<hr />
<InputDataText
{...register("toDoContent")}
autoComplete="off"
value={todo?.content || ""}
onChange={handleChangeContentInput}
disabled={isChange}
/>
</form>
</Wrapper>
</TodoList>
);
}
export default TodoContent;
사실 작년에도 몇번 프리온보딩 코스를 신청할까말까 고민했었는데 일이 바쁘다는 핑계로 계속 미뤄왔었다. 새해 목표에 자기개발 및 이직이기 때문에, 첫 단추로 원티드 프리온보딩 프론트엔드 챌린지에 신청을 하게 되었다.
공부했던 내용들을 잘 정리하고, 반복을 통해 까먹지 않도록 해야겠다.🔥