Skip to content

πŸ“š μ›ν‹°λ“œ ν”„λ¦¬μ˜¨λ³΄λ”© ν”„λ‘ νŠΈμ—”λ“œ μ±Œλ¦°μ§€ 1μ›” μ‚¬μ „λ―Έμ…˜

Notifications You must be signed in to change notification settings

GitHubGW/wanted-pre-onboarding-challenge-fe-1

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ν”„λ¦¬μ˜¨λ³΄λ”© ν”„λ‘ νŠΈμ—”λ“œ μ±Œλ¦°μ§€ 1μ›”

λͺ©μ°¨

ν”„λ‘œμ νŠΈ μ‹€ν–‰

  1. git clone https://github.com/GitHubGW/wanted-pre-onboarding-challenge-fe-1λ₯Ό 톡해 리포지토리λ₯Ό ν΄λ‘ ν•©λ‹ˆλ‹€.
  2. npm install을 톡해 ν•„μš”ν•œ 라이브러리λ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€.
  3. npm startλ₯Ό 톡해 ν”„λ‘œμ νŠΈλ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
git clone https://github.com/GitHubGW/wanted-pre-onboarding-challenge-fe-1
npm install
npm start

μ‚¬μš©ν•œ 기술

  • React

    • ReactλŠ” 주둜 SPAλ₯Ό μœ„ν•œ μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ§Œλ“œλŠ” 데 μ‚¬μš©λ˜λŠ” Javascript λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.
    • 단방ν–₯ 데이터 흐름을 톡해 데이터 흐름 및 λ³€ν™”λ₯Ό μ˜ˆμΈ‘ν•˜κΈ° 쉽고, μ»΄ν¬λ„ŒνŠΈ 기반 ꡬ쑰λ₯Ό 톡해 μž¬μ‚¬μš©μ„±κ³Ό μ½”λ“œ 관리 및 μœ μ§€λ³΄μˆ˜λ₯Ό μœ„ν•΄ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • NextJS

    • Next.jsλŠ” μ„œλ²„μ‚¬μ΄λ“œ λ Œλ”λ§, 정적 νŽ˜μ΄μ§€ 생성 λ“± λ¦¬μ•‘νŠΈ 기반 μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κΈ°λŠ₯을 μœ„ν•œ μ›Ή 개발 ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€.
  • Typescript

    • TypescriptλŠ” Javascript μŠˆνΌμ…‹ μ–Έμ–΄μž…λ‹ˆλ‹€.
    • νƒ€μž…μ„ 미리 μ§€μ •ν•΄μ„œ λŸ°νƒ€μž„ 이전 λ‹¨κ³„μ—μ„œ 미리 μ—λŸ¬λ₯Ό 확인할 수 있고, μ½”λ“œ μžλ™ μ™„μ„± 및 디버깅을 μœ„ν•΄ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • React Query

    • React QueryλŠ” 데이터 Fetching, 캐싱, 동기화, μ„œλ²„ μͺ½ 데이터 μ—…λ°μ΄νŠΈ 등을 μœ„ν•œ λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.
    • 각 νŽ˜μ΄μ§€μ—μ„œ κ³΅ν†΅μœΌλ‘œ μ‚¬μš©ν•˜λŠ” μ „μ—­ μƒνƒœκ°€ 적고 μ„œλ²„ μͺ½ 데이터듀을 κ°„νŽΈν•˜κ³  효율적으둜 κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • React Hook Form

    • React Hook Form은 Reactμ—μ„œ Form을 μ œμ–΄ν•˜κ³  μœ νš¨μ„± 검사 및 μ—λŸ¬λ₯Ό μ²˜λ¦¬ν•  수 μžˆλŠ” λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.
    • 전체 μ½”λ“œ 양을 쀄이고, 일관적인 μ½”λ“œ μž‘μ„± 및 μœ μ§€λ³΄μˆ˜, λ Œλ”λ§ 이슈 등을 ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • Tailwind CSS

    • Tailwind CSSλŠ” μœ ν‹Έλ¦¬ν‹° 퍼슀트(Utility-first)λ₯Ό 지ν–₯ν•˜λŠ” μ˜€ν”ˆ μ†ŒμŠ€ CSS ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€.
    • μΌκ΄€λœ λ””μžμΈκ³Ό 자유둜운 μ»€μŠ€ν„°λ§ˆμ΄μ§• 및 생산성을 μœ„ν•΄ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

ν”„λ‘œμ νŠΈ ꡬ쑰

  • api : API 관리
  • components : μ»΄ν¬λ„ŒνŠΈ 관리
  • constants : μƒμˆ˜ 관리
  • pages : λΌμš°νŒ… 관리
  • queries : λ¦¬μ•‘νŠΈ 쿼리 관리
  • styles : μŠ€νƒ€μΌ 관리
  • types : 곡톡 νƒ€μž… 관리
  • utils : μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ 관리
 ┣ api
 ┃ ┣ auth.ts
 ┃ β”— todo.ts
 ┣ components
 ┃ ┣ layouts
 ┃ ┃ β”— MainLayout.tsx
 ┃ ┣ AuthForm.tsx
 ┃ ┣ FormError.tsx
 ┃ ┣ TodoDetail.tsx
 ┃ ┣ TodoDetailItem.tsx
 ┃ ┣ TodoForm.tsx
 ┃ ┣ TodoItem.tsx
 ┃ β”— TodoList.tsx
 ┣ constants
 ┃ ┣ auth.ts
 ┃ ┣ common.ts
 ┃ β”— queryKeys.ts
 ┣ pages
 ┃ ┣ _app.tsx
 ┃ ┣ _document.tsx
 ┃ ┣ auth.tsx
 ┃ β”— index.tsx
 ┣ queries
 ┃ ┣ useAuthMutate.ts
 ┃ ┣ useTodoMutate.ts
 ┃ β”— useTodoQuery.ts
 ┣ styles
 ┃ β”— globals.css
 ┣ types
 ┃ ┣ auth.ts
 ┃ β”— todo.ts
 ┣ utils
 ┃ β”— localStorage.ts

μ£Όμš” κΈ°λŠ₯

νšŒμ›κ°€μž…, 둜그인, λ‘œκ·Έμ•„μ›ƒ

  • /auth κ²½λ‘œμ— 둜그인 및 νšŒμ›κ°€μž… κΈ°λŠ₯을 κ΅¬ν˜„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • νšŒμ›κ°€μž… 및 둜그인 성곡 μ‹œ, 전달받은 토큰을 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯ν•˜κ³  루트 경둜둜 μ΄λ™μ‹œν‚΅λ‹ˆλ‹€.
  • λ‘œκ·Έμ•„μ›ƒ μ‹œ, 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯된 토큰을 μ‚­μ œν•˜κ³  /auth 경둜둜 μ΄λ™μ‹œν‚΅λ‹ˆλ‹€.
// components/AuthForm.tsx
const { useSignUpMutation, useLoginMutation } = useAuthMutation();
const { mutateAsync: signUpMutateAsync, data: signUpData } = useSignUpMutation();
const { mutateAsync: loginMutateAsync, data: loginData } = useLoginMutation();

const onValid = useCallback(
  async (formData: FormData) => {
    if (status === AUTH_STATE.SIGN_UP) {
      const result = await signUpMutateAsync(formData);
      if (result.token) {
        reset();
        setLocalStorageItem(result.token);
        setStatus(AUTH_STATE.LOGIN);
      }
    } else {
      const result = await loginMutateAsync(formData);
      if (result.token) {
        reset();
        setLocalStorageItem(result.token);
        router.push("/");
      }
    }
  },
  [status, reset, router, signUpMutateAsync, loginMutateAsync]
);
  • React Hook Form을 μ‚¬μš©ν•˜μ—¬ form을 μ œμ–΄ν•˜κ³ , 이메일과 λΉ„λ°€λ²ˆν˜Έμ˜ μœ νš¨μ„± 확인 및 μ—λŸ¬λ₯Ό λŒ€μ‘ν•©λ‹ˆλ‹€.
<input
  {...register("email", {
    required: "이메일은 ν•„μˆ˜μž…λ‹ˆλ‹€.",
    validate: (value) => {
      const isValidText = value.includes("@") && value.includes(".");
      return isValidText || "μ΄λ©”μΌμ—λŠ” @와 .이 ν¬ν•¨λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.";
    },
  })}
  required
  type="email"
  id="emailInput"
/>
<FormError text={errors.email?.message} />

νˆ¬λ‘ 생성, μˆ˜μ •, μ‚­μ œ

  • νˆ¬λ‘ μΆ”κ°€ 및 μ‚­μ œ λ²„νŠΌμ„ 클릭해 ν•  일을 μƒμ„±ν•˜κ±°λ‚˜ μ‚­μ œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • νˆ¬λ‘ μˆ˜μ • λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄ μˆ˜μ • λͺ¨λ“œκ°€ ν™œμ„±ν™”λ˜κ³ , μˆ˜μ • λ‚΄μš©μ„ μ œμΆœν•˜κ±°λ‚˜ μ·¨μ†Œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
// components/TodoForm.tsx
const { useCreateTodoMutation, useUpdateTodoMutation } = useTodoMutation();
const { mutate: createTodoMutate } = useCreateTodoMutation();
const { mutate: updateTodoMutate } = useUpdateTodoMutation();

const onValid = useCallback(
  (formData: FormData) => {
    if (isUpdating && id && handleToggleTodo) {
      handleToggleTodo();
      reset();
      updateTodoMutate({ id, title: formData.title, content: formData.content });
    } else {
      reset();
      createTodoMutate({ title: formData.title, content: formData.content });
    }
  },
  [updateTodoMutate, createTodoMutate, handleToggleTodo, isUpdating, id, reset]
);
  • μˆ˜μ •ν•œ νˆ¬λ‘μ˜ λ‚΄μš©μ€ 전체 νˆ¬λ‘ λͺ©λ‘ 및 νˆ¬λ‘ 상세에 μ‹€μ‹œκ°„μœΌλ‘œ λ°˜μ˜λ©λ‹ˆλ‹€.
  • μˆ˜μ •ν•œ νˆ¬λ‘μ˜ λ‚΄μš©μ„ μ‹€μ‹œκ°„μœΌλ‘œ λ°˜μ˜ν•˜κΈ° μœ„ν•΄ React Query의 invalidateQueries λ©”μ„œλ“œλ₯Ό 톡해 기쑴에 μ‘°νšŒν–ˆλ˜ 쿼리λ₯Ό λ¬΄νš¨ν™”μ‹œν‚€κ³  데이터λ₯Ό μƒˆλ‘œ μ‘°νšŒν•΄μ˜€λ„λ‘ ν•©λ‹ˆλ‹€.
  • invalidateQueries λ©”μ„œλ“œλŠ” 전달받은 쿼리 킀에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  쿼리λ₯Ό λ¬΄νš¨ν™”ν•©λ‹ˆλ‹€.
// queries/useTodoMutation.ts
const useUpdateTodoMutation = () => {
  return useMutation({
    mutationFn: (params: UpdateTodoParams) => TodoApi.updateTodo({ id: params.id, title: params.title, content: params.content }),
    onSuccess: (response) => {
      queryClient.invalidateQueries(QUERY_KEYS.TODO.GET_TODOS());
      queryClient.invalidateQueries(QUERY_KEYS.TODO.GET_TODO_BY_ID(response.data.id));
    },
    onError: (error) => {
      console.log("useUpdateTodo onError", error);
    },
  });
};

전체 νˆ¬λ‘ 쑰회 및 상세 쑰회

  • 전체 νˆ¬λ‘ λͺ©λ‘κ³Ό 각각의 νˆ¬λ‘μ˜ 상세 λ‚΄μš©μ„ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
// components/TodoList.tsx
const TodoList = () => {
  const { useGetTodosQuery } = useTodoQuery();
  const { data: todosData, isLoading } = useGetTodosQuery();

  if (isLoading) {
    return "Loading...";
  }

  return <div>{todosData?.data && [...todosData?.data]?.reverse().map((item) => <TodoItem key={item.id} id={item.id} title={item.title} createdAt={item.createdAt} />)}</div>;
};

// components/TodoDetail.tsx
const TodoDetail = () => {
  const { useGetTodoByIdQuery } = useTodoQuery();
  const { data: todoData } = useGetTodoByIdQuery({ id: String(router.query.id) });

  return (
    <div>
      {todoData?.data && !isUpdating && (
        <TodoDetailItem title={todoData.data.title} content={todoData.data.content} createdAt={todoData.data.createdAt} updatedAt={todoData.data.updatedAt} />
      )}
    </div>
  );
};

API μŠ€νŽ™

login

  • POST /users/login
  • Parameter
    • email: string
    • password: string

signUp

  • POST /users/create
  • Parameter
    • email: string
    • password: string

getTodos

  • GET /todos
  • Headers
    • Authorization: login token

getTodoById

  • GET /todos/:id
  • Headers
    • Authorization: login token

createTodo

  • POST /todos
  • Parameter
    • title: string
    • content: string
  • Headers
    • Authorization: login token

updateTodo

  • PUT /todos/:id
  • Parameter
    • title: string
    • content: string
  • Headers
    • Authorization: login token

deleteTodo

  • DELETE /todos/:id
  • Headers
    • Authorization: login token

About

πŸ“š μ›ν‹°λ“œ ν”„λ¦¬μ˜¨λ³΄λ”© ν”„λ‘ νŠΈμ—”λ“œ μ±Œλ¦°μ§€ 1μ›” μ‚¬μ „λ―Έμ…˜

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published