회원가입 컴포넌트에서와 마찬가지로 로그인 관련 비지니스 로직이 전부 로그인 Form
컴포넌트 내에 포함된 상태라 가독성이 나쁜 상태입니다.
회원가입 컴포넌트 리팩토링 진행 시 로그인과 스토어 초기화 커스텀 훅도 작성을 했었기 때문에 비교적 빠르게 리팩토링이 완료할 수 있을 거 같습니다.
'use client';
import React, { useState } from 'react';
import { signIn } from 'next-auth/react';
import { signInSchema } from '../../../../schema/userValidationSchema';
import { useRouter } from 'next/navigation';
import { SafeParseReturnType, ZodIssue } from 'zod';
const Form: React.FC = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [isSignIng, setIsSignIng] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const rotuer = useRouter();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSignIng(true);
setError('');
const validatedFields = signInSchema.safeParse({
email: email,
password: password,
});
if (!validatedFields.success) {
const errorMessages = validatedFields.error.issues.map(
(issue: ZodIssue) => issue.message
);
const combinedMessage = errorMessages.join('\n');
setError(combinedMessage);
setIsSignIng(false);
return;
}
const result = await signIn('credentials', {
redirect: false,
email: validatedFields.data.email,
password: validatedFields.data.password,
});
if (result?.status === 200) {
rotuer.push('/');
} else {
setError(result?.error || '');
}
setIsSignIng(false);
};
return (
<>
<div className="text-red-600 mb-4 errorMessage text-center">{error}</div>
<form
onSubmit={handleSubmit}
className="flex flex-col items-center w-full"
>
<div className="flex flex-col items-center w-full">
<input
name="email"
type="email"
value={email}
placeholder="이메일"
autoComplete="username"
onChange={(e) => setEmail(e.target.value)}
className="px-4 h-14 bg-gray-50 dark:text-black border-gray-200 border mb-6 outline-none w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3 "
/>
<input
name="password"
type="password"
value={password}
placeholder="패스워드"
autoComplete="current-password"
onChange={(e) => setPassword(e.target.value)}
className="px-4 h-14 bg-gray-50 dark:text-black border-gray-200 border outline-none w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3 "
/>
</div>
<div className="flex flex-col items-center w-full my-8">
<button
aria-label="로그인하기"
type="submit"
disabled={isSignIng}
className="h-12 bg-zinc-900 dark:bg-white dark:text-black dark:hover:bg-zinc-300 text-white transition duration-200 ease-in-out w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3"
>
{isSignIng ? '로그인 중' : '로그인하기'}
</button>
</div>
</form>
</>
);
};
export default Form;
다음과 같이 로그인 관련 파일 트리를 구성했습니다.
src/
├── _utils/
│ ├── getUserDataFireStore.ts
│ ├── initializeUserDataFireStore.ts
├── app/
│ ├── account/
│ │ └── login/
│ │ └── components/
│ │ └── Form.tsx
│ │ └── page.tsx
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/
│ │ └── route.ts
├── hooks/
│ ├── useSignInUser.ts
│ └── useStore.ts
├── schema/
│ └── userValidationSchema.ts
└── utils/
├── getUserCartItems.ts
└── getUserWishlistItems.ts
route.ts
파일의 경우 NextAuth 도입할 때 생성한 파일이지만 로그인 및 회원가입 관련 리팩토링을 하면서 수정이 여러 번 발생하고 있습니다.
useUserSignIn
커스텀 훅의 경우, 초기 생성 후 로그인 Form
에 적용에서 조금 혼동이 왔던 부분이 있었습니다.
NextAuth의 로그인 기능과, FireBase의 로그인 기능이 각각 따로따로 있는 상태에서 한 번 더 useSignIn
으로 감싸다보니 에러가 발생할 때 어디에서 발생했는지 찾기가 힘들었습니다.
또한, 에러가 UI는 표시 되지 않고 console에서만 확인되는 문제도 있었습니다.
그래서 errorMessage
를 Form
에서 관리를 했다가 useUserSignIn
에서 관리하게 되었습니다.
(회원가입 리팩토링 포스팅에서도 이 부분을 적용시켰습니다. errorMessage를 useUserSignUp으로 옮긴 코드로 수정했습니다.)
initializeUserStore()
과 goHome()
은 커스텀 훅 내의 함수로, 로그인이 성공적이면 실행 됩니다.
initializeUserStore()
는 사용자의wishlistItems
와cartItems
를 가져오는 역할을 합니다.
goHome()
은 로그인 완료 시 메인 화면으로 클라이언트 측 라우팅을 수행합니다.
처음에는 Form
에서 userSignIn()
이 성공적인지 확인한 다음 실행시키도록 Form
에 포함되어 있었으나 로그인이라는 비지니스 로직에서 필수인 부분으로 생각되어 userSignIn()
내에서 NextAuth signIn()
이 성공적이면 실행하도록 변경하게 되었습니다.
'use client';
import React, { useState } from 'react';
import useSignInUser from '@/hooks/useSignInUser';
const Form: React.FC = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const { status: signInStatus, signInUser, errorMessage } = useSignInUser();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
signInUser(email, password);
};
return (
<>
<div className="text-red-600 mb-4 errorMessage text-center">
{errorMessage}
</div>
<form
onSubmit={handleSubmit}
className="flex flex-col items-center w-full"
>
<div className="flex flex-col items-center w-full">
<input
name="email"
type="email"
value={email}
placeholder="이메일"
autoComplete="username"
onChange={(e) => setEmail(e.target.value)}
className="px-4 h-14 bg-gray-50 dark:text-black border-gray-200 border mb-6 outline-none w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3 "
/>
<input
name="password"
type="password"
value={password}
placeholder="패스워드"
autoComplete="current-password"
onChange={(e) => setPassword(e.target.value)}
className="px-4 h-14 bg-gray-50 dark:text-black border-gray-200 border outline-none w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3 "
/>
</div>
<div className="flex flex-col items-center w-full my-8">
<button
aria-label="로그인하기"
type="submit"
disabled={signInStatus === 'loading'}
className="h-12 bg-zinc-900 dark:bg-white dark:text-black dark:hover:bg-zinc-300 text-white transition duration-200 ease-in-out w-11/12 sm:w-4/5 md:w-1/2 lg:w-2/5 xl:w-1/3"
>
{signInStatus === 'loading' ? '로그인 중' : '로그인하기'}
</button>
</div>
</form>
</>
);
};
export default Form;
import { signInSchema } from '@/schema/userValidationSchema';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import useRouterPush from './useRouterPush';
import useStore from './useStore';
const useSignInUser = () => {
const [status, setStatus] = useState<string>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
const { initializeUserStore } = useStore();
const { goHome } = useRouterPush();
const signInUser = async (email: string, password: string) => {
setStatus('loading');
const validatedFields = signInSchema.safeParse({
email: email,
password: password,
});
if (!validatedFields.success) {
const errorMessages = validatedFields.error.issues.map(
(issue) => issue.message
);
const combinedMessage = errorMessages.join('\n');
setStatus('error');
setErrorMessage(combinedMessage);
return;
}
const result = await signIn('credentials', {
redirect: false,
email,
password,
});
if (result?.ok) {
setStatus('success');
initializeUserStore(email);
goHome();
} else {
setStatus('error');
if (result?.error) setErrorMessage(result?.error);
}
};
return { status, signInUser, errorMessage };
};
export default useSignInUser;
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { FirebaseError } from 'firebase/app';
import { FirebaseAuthError } from '@/error/firebaseAuthError';
import signInFirebase from '@/_utils/signInFirebase';
const authOptions: NextAuthOptions = {
jwt: {
secret: process.env.NEXTAUTH_SECRET,
},
providers: [
CredentialsProvider({
name: 'showfinnmore',
credentials: {
email: { label: 'email', type: 'text' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials || !credentials.email || !credentials.password)
return null;
try {
const result = await signInFirebase(
credentials.email,
credentials.password
);
if (result) {
const { user } = result;
return {
id: user.uid,
name: user.displayName,
email: user.email,
};
} else {
return null;
}
} catch (error) {
if (error instanceof Error) {
const firebaseError = error as FirebaseError;
const errorCode = firebaseError.code;
const message = firebaseError.message;
throw new FirebaseAuthError(errorCode, message);
} else {
throw new Error('알 수 없는 에러가 발생했습니다.');
}
}
},
}),
],
pages: {
signIn: '/account/login',
},
callbacks: {
async redirect({ url, baseUrl }) {
return baseUrl;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
import { resetCartItems, setCartItems } from '@/slices/cartSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { CartItems, WishlistItems } from '@/types/globalTypes';
import { resetWishlistItems, setWishlistItems } from '@/slices/wishListSlice';
import getUserCartItems from '@/utils/getUserCartItems';
import getUserWishlistItems from '@/utils/getUserWishlistItems';
const useStore = () => {
const dispatch = useAppDispatch();
const resetStore = () => {
dispatch(resetCartItems());
dispatch(resetWishlistItems());
};
const initializeUserStore = async (email: string) => {
const cartItems: CartItems = await getUserCartItems(email);
const wishlistItems: WishlistItems = await getUserWishlistItems(email);
dispatch(setCartItems(cartItems));
dispatch(setWishlistItems(wishlistItems));
};
return { resetStore, initializeUserStore };
};
export default useStore;
import getUserDataFireStore from '@/_utils/getUserDataFireStore';
import { CartItems } from '@/types/globalTypes';
import { DocumentData } from 'firebase/firestore';
const getUserCartItems = async (email: string) => {
try {
const userSnapShot: DocumentData = await getUserDataFireStore(email);
const cartItems: CartItems = userSnapShot.cartItems;
return cartItems;
} catch (error) {
console.error('getUserCartItemsFireStore Error: ', error);
throw new Error(`failed to get user's cart items`);
}
};
export default getUserCartItems;
import { DocumentData } from 'firebase/firestore';
import getUserDataFireStore from '../_utils/getUserDataFireStore';
import { WishlistItems } from '@/types/globalTypes';
const getUserWishlistItems = async (email: string) => {
try {
const userSnapShot: DocumentData = await getUserDataFireStore(email);
const wishlistItems: WishlistItems = userSnapShot.wishlistItems;
return wishlistItems;
} catch (error) {
console.error('getUserWishlistItemsFireStore Error: ', error);
throw new Error(`failed to get user's wishlist items`);
}
};
export default getUserWishlistItems;
import {
DocumentReference,
DocumentSnapshot,
getDoc,
} from 'firebase/firestore';
import { db } from '@/app/firebaseConfig';
import { doc } from 'firebase/firestore';
const getUserDataFireStore = async (email: string) => {
try {
const userDocumentReference: DocumentReference = doc(db, 'users', email);
if (!userDocumentReference)
throw new Error('Failed to get user document reference');
const userSnapShot: DocumentSnapshot = await getDoc(userDocumentReference);
if (!userSnapShot.exists()) {
throw new Error(`UserSnapshot isn't exists`);
}
return userSnapShot.data();
} catch (error) {
console.error('Failed to get user snapshot:', error);
throw new Error('Failed to get user snapshot');
}
};
export default getUserDataFireStore;
이번 리팩토링에서는 오류 핸들링 부분에서 상당히 애를 먹었습니다.
사용된 모든 함수에서 try-catch 블록을 사용하고 있었고, 그 안에서 발생하는 수많은 throw 문 때문에 코드를 추적하는 일이 매우 복잡했습니다.
이번 리팩토링에서 오류 핸들링의 중요함을 느끼게 됐습니다..