원티드 프리온보딩 프론트엔드 사전과제

Tinubee·2023년 1월 5일
0

원티드프리온보딩

목록 보기
1/1
post-thumbnail

1. 원티드 프리온보딩 사전과제

새해 시작과 동시에 원티드 프리온보딩 프론트엔드 챌린지를 지원했다.
사전과제는 회원가입 & 로그인과 Todo List 구현하는 것이였다.
지금까지 공부했던 내용들을 복습하며 만들어 보았다.

💡 완성본 Github 링크


2. 과제 요구사항

💡 Assignment 1 - Login / SignUp

  • /auth 경로에 로그인 / 회원가입 기능을 개발합니다
    • 로그인, 회원가입을 별도의 경로로 분리해도 무방합니다
    • 최소한 이메일, 비밀번호 input, 제출 button을 갖도록 구성해주세요
  • 이메일과 비밀번호의 유효성을 확인합니다
    • 이메일 조건 : 최소 @, . 포함
    • 비밀번호 조건 : 8자 이상 입력
    • 이메일과 비밀번호가 모두 입력되어 있고, 조건을 만족해야 제출 버튼이 활성화 되도록 해주세요
  • 로그인 API를 호출하고, 올바른 응답을 받았을 때 루트 경로로 이동시켜주세요
    • 응답으로 받은 토큰은 로컬 스토리지에 저장해주세요
    • 다음 번에 로그인 시 토큰이 존재한다면 루트 경로로 리다이렉트 시켜주세요
    • 어떤 경우든 토큰이 유효하지 않다면 사용자에게 알리고 로그인 페이지로 리다이렉트 시켜주세요

💡 Assignment 2 - Todo List

  • Todo List API를 호출하여 Todo List CRUD 기능을 구현해주세요

    • 목록 / 상세 영역으로 나누어 구현해주세요
    • Todo 목록을 볼 수 있습니다.
    • Todo 추가 버튼을 클릭하면 할 일이 추가 됩니다.
    • Todo 수정 버튼을 클릭하면 수정 모드를 활성화하고, 수정 내용을 제출하거나 취소할 수 있습니다.
    • Todo 삭제 버튼을 클릭하면 해당 Todo를 삭제할 수 있습니다.
  • 한 화면 내에서 Todo List와 개별 Todo의 상세를 확인할 수 있도록 해주세요.

    • 새로고침을 했을 때 현재 상태가 유지되어야 합니다.
    • 개별 Todo를 조회 순서에 따라 페이지 뒤로가기를 통하여 조회할 수 있도록 해주세요.
  • 한 페이지 내에서 새로고침 없이 데이터가 정합성을 갖추도록 구현해주세요

    • 수정되는 Todo의 내용이 목록에서도 실시간으로 반영되어야 합니다

3. 진행 과정

📷 로그인 / 회원가입 시연 영상

로그인 경로와 회원가입 경로를 따로 만들어서 구성했다.

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 시연 영상

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;

4. 마치며

사실 작년에도 몇번 프리온보딩 코스를 신청할까말까 고민했었는데 일이 바쁘다는 핑계로 계속 미뤄왔었다. 새해 목표에 자기개발 및 이직이기 때문에, 첫 단추로 원티드 프리온보딩 프론트엔드 챌린지에 신청을 하게 되었다.
공부했던 내용들을 잘 정리하고, 반복을 통해 까먹지 않도록 해야겠다.🔥

profile
✍️ 👨🏻‍💻🔥➜👍🤔 😱

0개의 댓글