Skip to content

프리온보딩 첫번째과제(투두리스트)를 TypeScript로 reconstruct.

Notifications You must be signed in to change notification settings

dltkdals224/pre-onboarding-todo-list-reconstruction

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

프리온보딩 Assignment - TodoList Reconstruction

프로젝트 기간 : 2022년 12월 1일 ~ 2022년 12월 9일

진행 인원 : 1인 (개인)


⌨️ 실행 방법

$ git clone https://github.com/dltkdals224/pre-onboarding-todo-list-reconstruction.git
$ npm install
$ npm run dev

🧚🏻‍♂️ 적용 기술 및 채택 근거

1. TypeScript

  • 타입 안정성 제공

TypeScript는 정적타입을 지원하여 컴파일 단계에서 오류를 포착할 수 있게 한다.
명시적인 정적 타입 지정은 개발자의 의도를 명확하게 코드로 기술할 수 있게 한다.
이는 코드의 가독성을 높이고 디버깅을 쉽게 한다.


2. router 권한 처리

import {
BrowserRouter as Router,
Routes,
Route as RoutePermission,
Route,
} from "react-router-dom";
import RequireAuth from "../utils/RequireAuth";
import { ROUTE_URL } from "../constants/Route";
import NotFound from "../components/common/NotFound";
import Auth from "../pages/auth";
import TodoList from "../pages/todoList";
const Routers = () => {
return (
<Router>
<Routes>
<RoutePermission
element={
<RequireAuth isAuthRequired={false} redirectUrl={ROUTE_URL.TODO} />
}
>
<Route path={ROUTE_URL.AUTH} element={<Auth />} />
</RoutePermission>
<RoutePermission
element={
<RequireAuth isAuthRequired={true} redirectUrl={ROUTE_URL.AUTH} />
}
>
<Route path={ROUTE_URL.TODO} element={<TodoList />} />
</RoutePermission>
<Route path={ROUTE_URL.ALL} element={<NotFound />} />
</Routes>
</Router>
);
};
export default Routers;

import { Outlet, Navigate } from "react-router-dom";
import Cookies from "universal-cookie";
interface Props {
isAuthRequired: boolean;
redirectUrl: string;
}
const RequireAuth = ({ isAuthRequired, redirectUrl }: Props) => {
const cookies = new Cookies();
const ACCESS_TOKEN = cookies.get("ACCESS_TOKEN");
if (isAuthRequired && !ACCESS_TOKEN) {
return <Navigate to={redirectUrl} replace />;
}
if (!isAuthRequired && !!ACCESS_TOKEN) {
return <Navigate to={redirectUrl} replace />;
}
return <Outlet />;
};
export default RequireAuth;

  • Route as RoutePermission

기본 라우팅에 대해 접근 권한에 따른 리다이렉트 처리를 목적으로 한다.
RequireAuth.tsx에 권한 필요여부와 리다이렉트 url을 넘겨 해당 컴포넌트 내부에서 동작하도록 함.

  • RequireAuth.tsx

권한 필요 여부와 ACCESS_TOKEN 소유 여부에 따라 페이지 접근을 허가하거나, redirectUrl로 추방하는 역할을 한다.
ACCESS_TOKEN은 BE와 소통이 불가능한 상태에서, 만료 시간을 지키기위해 cookie storage에서 관리.


3. react-hook-form

import { useForm, FieldValues } from "react-hook-form";
import styled, { css } from "styled-components";
import { signInApi } from "../../apis/auth";
import ReportError from "../../utils/ReportError";
import { setAccessToken } from "../../utils/HandleAccessToken";
import { SIGNIN_INPUT_VALIDATION } from "../../constants/Authentication";
const SignIn = ({ isDefaultForm }: { isDefaultForm: boolean }) => {
const {
register,
handleSubmit,
formState: { isSubmitting, isDirty, errors },
} = useForm();
const onSubmit = async (data: FieldValues) => {
await new Promise((e) => setTimeout(e, 300));
try {
const response = await signInApi(data.email, data.password);
setAccessToken(response.data.access_token);
} catch (error: unknown) {
ReportError(error);
}
};
return (
<Section isDefaultForm={isDefaultForm}>
<SignInForm onSubmit={handleSubmit(onSubmit)}>
<SignInTitle>Sign in</SignInTitle>
<SignInInput
autoComplete="username"
placeholder="Email"
aria-invalid={isDirty || errors.email ? "true" : "false"}
{...register("email", SIGNIN_INPUT_VALIDATION.email)}
/>
<ErrorMessage>{`${errors.email?.message ?? ""}`}</ErrorMessage>
<SignInInput
type="password"
autoComplete="new-password"
placeholder="Password"
aria-invalid={isDirty || errors.passwrod ? "true" : "false"}
{...register("password", SIGNIN_INPUT_VALIDATION.password)}
/>
<ErrorMessage>{`${errors.password?.message ?? ""}`}</ErrorMessage>
<SignInButton disabled={isSubmitting}>Sign In</SignInButton>
</SignInForm>
</Section>
);
};
export default SignIn;

export const SIGNIN_INPUT_VALIDATION = {
email: {
required: true,
pattern: {
value: /\S+@\S+\.\S+/,
message: "이메일 형식에 맞지 않습니다.",
},
},
password: {
required: {
value: true,
message: "비밀번호를 입력해주세요.",
},
},
};
export const SIGNUP_INPUT_VALIDATION = {
email: {
required: true,
pattern: {
value: /\S+@\S+\.\S+/,
message: "이메일 형식에 맞지 않습니다.",
},
},
password: {
required: {
value: true,
message: "비밀번호를 입력해주세요.",
},
minLength: {
value: 8,
message: "8자리 이상의 비밀번호를 입력해주세요.",
},
},
passwordReconfirm: {
required: true,
minLength: {
value: 8,
message: "8자리 이상의 비밀번호를 입력해주세요.",
},
},
};

react-hook-form은 기본적으로 비제어 컴포넌트 방식으로 구현되어있기에 렌더링 이슈를 해결하면서도,
form의 데이터와 상태를 Provider 아래라면 어느 곳에서든지 props drilling 없이 사용할 수 있다.


  • formState

isSubmitting을 button의 disabled에 걸어 중복제출을 막으며,
isDirty를 통해 form이 제출된 이후 input의 변화에 대해서도 errors(부적절한 제출 형태)를 감지할 수 있도록 처리.

  • INPUT_VALIDATION

input의 입력에 대해 필요한 옵션과 error message를 constants에서 관리하여 수정이 용이하도록 처리.


4. react-query

import { useQuery } from "react-query";
import { getTodoApi } from "../../apis/todo";
const useGetTodoListQuery = () => {
return useQuery({
queryKey: ["todoList"],
queryFn: () => {
return getTodoApi();
},
});
};
export default useGetTodoListQuery;

  • queryKey

queryKey를 통해 여타 useMutation의 onSuccess에서 refetch요청 두어 비동기 통신 관련 상태를 처리할 수 있게 함.

react-query를 사용하지 않고도 해당 부분을 구현할 수 있지만,
TodoText 수정 중 isCompleted 값의 변경이나 데이터 자체를 삭제하는 과정에 있어서
페이지의 전환없이 데이터의 변경을 보여주는 과정이 편리하므로 react-query를 채택하여 사용.


5. custom-hook 처리

/* eslint-disable react-hooks/exhaustive-deps */
import { useLayoutEffect, useRef, useState } from "react";
const useFocus = (defaultFocused = false) => {
const ref = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(defaultFocused);
useLayoutEffect(() => {
if (!ref.current) {
return;
}
const onFocus = () => setIsFocused(true);
const onBlur = () => setIsFocused(false);
if (isFocused) {
ref.current.focus();
}
ref.current.addEventListener("focus", onFocus);
ref.current.addEventListener("blur", onBlur);
}, [isFocused]);
return { ref, isFocused, setIsFocused };
};
export default useFocus;

import { useCallback, useState } from "react";
export const useFormShown = (
initialValue: boolean
): [boolean, Function, Function] => {
const [isDefaultForm, setIsDefaultForm] = useState<boolean>(
initialValue ?? true
);
const setSignInForm = useCallback(() => {
setIsDefaultForm(true);
}, []);
const setSignUpForm = useCallback(() => {
setIsDefaultForm(false);
}, []);
return [isDefaultForm, setSignInForm, setSignUpForm];
};

반복되는 로직을 간편하게 불러와 사용할 수 있도록 custom-hook 처리.


  • useCallback

이 때, useCallback을 통해 특정 함수를 새로 만들지 않고 재사용할 수 있도록 처리한다.


6. optimization

export default React.memo(TodoItem);

  • React.memo

TodoList Page내 다중 TodoItem중 단일 개체에서 수정이 이루어질 때, 모든 개체가 리렌더링 되는 문제.
React.memo를 사용하여 해당되지 않는 컴포넌트들은 리렌더링이 발생하지 않도록 처리.


🔨 사용 기술

HTML5 CSS3 React TypeScript

axios styled-components react-query react-hook-form react-router-dom universal-cookie


📦 폴더 구조

📂 src
│  ├─ App.tsx
│  ├─ index.tsx
│  ├─ apis
│  │  ├─ auth.ts
│  │  ├─ client.ts
│  │  └─ todo.ts
│  ├─ components
│  │  ├─ Auth
│  │  │  ├─ Overlay.tsx
│  │  │  ├─ SignIn.tsx
│  │  │  └─ SignUp.tsx
│  │  ├─ common
│  │  │  ├─ Loader.tsx
│  │  │  └─ NotFound.tsx
│  │  └─ TodoList
│  │     ├─ Logout.tsx
│  │     ├─ TodoInput.tsx
│  │     └─ TodoItem.tsx
│  ├─ constants
│  │  ├─ Authentication.ts
│  │  ├─ Paragraph.ts
│  │  ├─ Route.ts
│  │  └─ Types.ts
│  ├─ hooks
│  │  ├─ shared
│  │  │  ├─ useCreateTodoListQuery.ts
│  │  │  ├─ useDeleteTodoListQuery.ts
│  │  │  ├─ useEditTodoListQuery.ts
│  │  │  └─ useGetTodoListQuery.ts
│  │  ├─ useFocus.ts
│  │  ├─ useFormShown.ts
│  │  └─ useInput.ts
│  ├─ pages
│  │  ├─ auth
│  │  │  ├─ index.tsx
│  │  │  └─ style.ts
│  │  └─ todoList
│  │     ├─ index.tsx
│  │     └─ style.ts
│  ├─ routers
│  │  └─ index.tsx
│  ├─ styles
│  │  ├─ GlobalStyle.tsx
│  │  └─ Theme.tsx
│  └─ utils
│     ├─ HandleAccessToken.ts
│     ├─ ReportError.tsx
│     └─ RequireAuth.tsx
└─ tsconfig.json

About

프리온보딩 첫번째과제(투두리스트)를 TypeScript로 reconstruct.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published