Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[길수진] week20 #1083

40 changes: 11 additions & 29 deletions components/common/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,31 @@ import styles from "./navbar.module.css";
import Profile from "./Profile/Profile";
import { ROUTE_PATHS } from "constants/route";
import Logo from "@/images/logo.svg";
import useFetch from "hooks/useFetch";
import useGetUser from "hooks/useGetUser";

type User = {
id: number;
created_at: string;
name: string;
image_source: string;
email: string;
auth_id: string;
};

function Navbar() {
const getUser = () => {
const { data, loading, error } = useFetch<{ data: User[] }>(
ROUTE_PATHS.user
);
const UserData = data?.data ?? [];

if (error) {
console.log(error);
}
const Navbar = () => {
const { data, isError, isPending } = useGetUser();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api 요청을 hook으로 관리하려는 시도 너무 좋은 것 같아요👍🏻


return { data: UserData, loading, error };
};

const { data: user, loading, error } = getUser();
if (isPending) {
return <></>;
}

return (
<nav className={styles.navbar}>
<div className={styles.wrap}>
<Link href={ROUTE_PATHS.home}>
<Logo width="133" height="24" alt="로고" priority />
</Link>
{user.length !== 0 ? (
{isError ? (
<Link href={ROUTE_PATHS.login}>로그인</Link>
) : (
<Link href={ROUTE_PATHS.home}>
<Profile email={user[0].email} imgUrl={user[0].image_source} />
<Profile email={data.email} imgUrl={data.image_source} />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저 같은 경우에는 보통 data라는 값으로 반환하기 보다는 hook에서 꺼내올때 user에 관한 정보면 userInfo라던지 조금 더 명확한 네이밍을 쓰는데 참고해보시면 좋을 것 같아요!

</Link>
) : (
<Link href={ROUTE_PATHS.login}>로그인</Link>
)}
</div>
</nav>
);
}
};

export default Navbar;
4 changes: 2 additions & 2 deletions constants/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export const ROUTE_PATHS = {
login: "/signin",
signup: "/signup",
folder: "/folder",
shared: "/shoard",
};
shared: "/shared",
} as const;
27 changes: 27 additions & 0 deletions hooks/useGetUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useQuery } from "@tanstack/react-query";
import instance from "lib/axios";
import useAuthStore from "store/authStore";
import { ROUTE_PATHS } from "constants/route";

interface UserResponse {
id: number;
name: string;
image_source: string;
email: string;
}

const fetchUser = async (): Promise<UserResponse> => {
const {
data: [user],
} = await instance.get("/users");
return user;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 api 폴더를 활용해봐도 좋을 것 같아요!


const useGetUser = () => {
return useQuery({
queryKey: ["user"],
queryFn: fetchUser,
});
};

export default useGetUser;
40 changes: 40 additions & 0 deletions hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useMutation } from "@tanstack/react-query";
import instance from "lib/axios";
import useAuthStore from "store/authStore";
import Cookies from "js-cookie";

interface User {
email: string;
password: string;
}

const useLogin = () => {
const setAccessToken = useAuthStore((state) => state.setAccessToken);

const mutation = useMutation({
mutationFn: (data: User) => {
return instance.post("/auth/sign-in", data);
},
onSuccess: (res) => {
// 토큰 저장
const { accessToken, refreshToken } = res.data;
setAccessToken(accessToken);
Cookies.set("refreshToken", refreshToken, { expires: 7 });
},
onError: (error) => {
throw error;
},
});

const login = async (data: User) => {
mutation.mutate(data);
};

return {
login,
isPending: mutation.isPending,
isSuccess: mutation.isSuccess,
};
};

export default useLogin;
47 changes: 47 additions & 0 deletions hooks/useSignUp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useMutation } from "@tanstack/react-query";

import instance from "lib/axios";

interface User {
email: string;
password: string;
}

const useSignUp = () => {
const checkEmailMutation = useMutation({
mutationFn: (email: string) => {
return instance.post("/users/check-email", { email });
},
onError: (error) => {
throw error;
},
});

const signUpMutation = useMutation({
mutationFn: (data: User) => {
return instance.post("/auth/sign-up", data);
},
onSuccess: (data) => {
// 페이지 이동
},
onError: (error) => {
throw error;
},
});

const signUp = async (email: string, password: string) => {
try {
await checkEmailMutation.mutateAsync(email);
await signUpMutation.mutateAsync({ email, password });
} catch (error) {
throw error;
}
};

return {
signUp,
isPending: checkEmailMutation.isPending || signUpMutation.isPending,
};
};

export default useSignUp;
21 changes: 12 additions & 9 deletions lib/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,31 @@ import type {
AxiosRequestConfig,
AxiosError,
} from "axios";
import { TOKEN } from "constants/auth";
import useAuthStore from "store/authStore";

const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: { "Content-Type": "application/json" },
});

const onRequest = (config: InternalAxiosRequestConfig) => {
// 매 요청마다 localStorage의 토큰을 조회해서 헤더에 추가한다.
if (localStorage.getItem(TOKEN.access)) {
const accessToken = JSON.parse(localStorage.getItem(TOKEN.access) ?? "");
config.headers.Authorization = accessToken ? `Bearer ${accessToken}` : "";
const accessToken = useAuthStore.getState().accessToken;

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}

return config;
};

const onError = (error: AxiosError) => {
// Unauthorized 응답을 받으면 가지고 있던 토큰을 제거한다.
if (error.isAxiosError && error.response?.status === 401) {
localStorage.removeItem(TOKEN.access);
console.log(error.response.status);
// Unauthorized 응답
if (error.isAxiosError) {
if (error.response?.status === 404) {
console.log("존재하지 않는 유저");
} else {
console.log("인증 오류");
}
Comment on lines +26 to +31
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

404와 같은 에러는 범용적인 에러라서 존재하지 않는 유저와 같이 특정 api에서 나는 에러를 처리하기 위한 텍스트를 적용하기에는 어렵습니다. 따라서 쫌 더 범용적으로 에러 텍스트를 처리할 수 있으면 좋을 것 같고 500과 같은 에러 또한 여기서 같이 처리해주면 좋을 것 같아요!

}
};

Expand Down
10 changes: 7 additions & 3 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ type AppPropsWithLayout = AppProps & {
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);

return getLayout(
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
{getLayout(
<>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</>
)}
</QueryClientProvider>
);
}
2 changes: 0 additions & 2 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ReactElement } from "react";
import Link from "next/link";
import Layout from "@/components/common/Layout/Layout";
import { ROUTE_PATHS } from "constants/route";
import type { NextPageWithLayout } from "./_app";
import styles from "./Home.module.css";
import Image from "next/image";
Expand Down
26 changes: 8 additions & 18 deletions pages/signin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Logo from "@/images/logo.svg";
import { ROUTE_PATHS } from "constants/route";
import { TOKEN } from "constants/auth";
import LoginCheck from "@/components/common/LoginCheck/LoginCheck";
import useLogin from "hooks/useLogin";

const SignIn = () => {
const {
Expand All @@ -27,26 +28,14 @@ const SignIn = () => {

const router = useRouter();

const postData = async (email: string, password: string) => {
const { login, isPending } = useLogin();

const onSubmit: SubmitHandler<Login> = async (data) => {
try {
const response = await instance.post("/sign-in", { email, password });
const result = response.data;
return result;
await login(data);
router.push(ROUTE_PATHS.home);
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
}
}
};

const onSubmit: SubmitHandler<Login> = async (data) => {
const { email, password } = data;
postData(email, password)
.then((res) => {
useLocalStorage(TOKEN.access, res.data.accessToken);
router.push(ROUTE_PATHS.folder);
})
.catch(() => {
setError("email", {
type: "400",
message: "이메일을 확인해 주세요.",
Expand All @@ -55,7 +44,8 @@ const SignIn = () => {
type: "400",
message: "비밀번호를 확인해 주세요.",
});
});
}
}
};

return (
Expand Down
37 changes: 18 additions & 19 deletions pages/signup/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useRouter } from "next/router";
import instance from "lib/axios";
import axios from "axios";
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import styles from "./signup.module.css";
import { Register, registerSchema } from "lib/zod/schema/RegisterSchema";
import InputField from "@/components/common/InputField/InputField";
import { ROUTE_PATHS } from "constants/route";
import useSignUp from "hooks/useSignUp";

const SignUp = () => {
const {
Expand All @@ -21,31 +21,30 @@ const SignUp = () => {

const router = useRouter();

const postData = async (email: string, password: string) => {
const { signUp } = useSignUp();

const onSubmit: SubmitHandler<Register> = async (data) => {
const { email, password } = data;

try {
const response = await instance.post("/sign-up", { email, password });
const result = response.data;
await signUp(email, password);
router.push(ROUTE_PATHS.login);
} catch (error) {
if (axios.isAxiosError(error)) {
throw error;
if (error.response?.status === 409) {
setError("email", {
type: "409",
message: "이미 사용 중인 이메일입니다.",
});
} else {
setError("email", {
message: "다시 시도해 주세요.",
});
}
}
}
};

const onSubmit: SubmitHandler<Register> = async (data) => {
const { email, password } = data;
postData(email, password)
.then(() => {
router.push(ROUTE_PATHS.folder);
})
.catch(() => {
setError("email", {
type: "400",
message: "이미 사용 중인 이메일입니다.",
});
});
};

return (
<div className={styles.container}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
Expand Down
22 changes: 22 additions & 0 deletions store/authStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface AuthState {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
}

const useAuthStore = create<AuthState>()(
persist(
(set) => ({
accessToken: null,
setAccessToken: (token) => set({ accessToken: token }),
}),
{
name: "auth-storage",
getStorage: () => localStorage,
}
)
);
Comment on lines +9 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zustand도 잘 활용해주셨네요 👍🏻

클라이언트 상태관리로 최근에 가장 관심받는 라이브러리가 하나가 zustand인데 특징들을 잘 파악해두면 좋을 것 같아요!


export default useAuthStore;