[ 개발환경 ] typescript + nextjs (app router) + tailwindcss ...
기존 admin 페이지에서 token이 존재하지 않으면, 로그인이 안되게 프로세스를 바꾸었습니다.
[ 이유 ]
1. admin 기능들이 복잡해짐.
2. admin 관리자가 늘어날 예정.
보안의 필요성이 느껴져, 토큰을 관리해주는 로직을 추가하게 되었습니다.
// _App.tsx 폴더에 sessionProvieder을 추가한다.
...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<AuthContext>
...
{children}
...
</AuthContext>
</body>
</html>
);
}
// AuthContext.tsx
"use client";
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export default function AuthContext({ children }: Props) {
return <SessionProvider>{children}</SessionProvider>;
}
AuthContext의 파일 안에서, SessionProvider를 사용하였습니다. 이를 통해 애플리케이션 전체에서 NextAuth 세션 관리를 일관되게 처리할 수 있습니다. 이 방식은 클라이언트 측에서 세션을 제공하는 역할을 합니다.
SessionProvider는 NextAuth에서 제공하는 컴포넌트로, 클라이언트 측에서 세션 상태를 관리하고 제공하는 역할을 합니다. SessionProvider는 애플리케이션의 모든 컴포넌트가 세션 상태에 접근할 수 있도록 React Context를 사용하여 세션 정보를 공유합니다.
// middleware.ts
import { withAuth } from "next-auth/middleware";
export default withAuth({
callbacks: {
authorized: ({ token, req }) => {
if (token) return true;
return false;
},
},
pages: {
signIn: "/sign-in",
},
});
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)", // 특정 경로를 제외한 모든 경로에 대해 인증을 강제하려면
],
};
withAuth는 NextAuth에서 제공하는 미들웨어로, Next.js 애플리케이션에서 특정 경로에 대한 인증을 강제하는 데 사용됩니다. 이 미들웨어를 사용하면 인증되지 않은 사용자가 보호된 페이지에 접근하려고 할 때 로그인 페이지로 리디렉션됩니다.
[ 주요 기능 ]
1. 인증 강제: 특정 경로에 대해 인증을 요구할 수 있습니다.
2. 리디렉션: 인증되지 않은 사용자가 보호된 경로에 접근하려고 하면 로그인 페이지로 리디렉션합니다.
3. 유연한 설정: 보호할 경로를 설정 파일이나 코딩을 통해 유연하게 정의할 수 있습니다.
// next-auth.d.ts
import NextAuth, { DefaultSession, DefaultUser } from "next-auth";
declare module "next-auth" {
interface Session {
accessToken?: string;
}
interface User {
token?: string;
}
}
NextAuth에서 제공하는 기본 타입들을 확장하는 역할을 합니다. 이를 통해 프로젝트의 타입 정의를 커스터마이징하여, 특정 요구 사항에 맞는 타입 정보를 추가할 수 있습니다.
[ 주요 목적 ]
- 타입 확장: NextAuth에서 기본적으로 제공하는 Session 및 User 인터페이스를 확장하여 커스텀 속성을 추가할 수 있습니다.
- 타입 안전성: 프로젝트 전반에 걸쳐 타입 안전성을 보장합니다. TypeScript를 사용하면 코드에서 잠재적인 타입 오류를 컴파일 단계에서 잡을 수 있습니다.
- 개발자 경험 향상: 확장된 타입 정의를 통해 코드 작성 시 보다 구체적인 타입 정보를 제공하여, 코드 자동 완성 및 타입 검사를 보다 효율적으로 할 수 있습니다.
// [...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "email", type: "text" },
password: { label: "password", type: "password" },
},
authorize: async (credentials) => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_URL}/sign-in`,
{
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
}
);
const result = await res.json();
if (result.success && result.data) {
return {
...result.data,
token: result.data.token,
};
}
return null;
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.accessToken = user.token;
}
return token;
},
async session({ session, token }) {
if (token) {
session.accessToken = token.accessToken as string;
}
return session;
},
},
pages: {
signIn: "/sign-in",
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
[ 역할 ]
- providers: 인증 제공자를 정의합니다. 여기서는 CredentialsProvider를 사용하여 이메일과 비밀번호로 인증합니다.
- authorize 함수: 사용자가 제출한 자격 증명을 검증하고, 성공하면 사용자 객체를 반환합니다.
- callbacks:
1. jwt: JWT 토큰을 생성하거나 갱신할 때 호출됩니다. 사용자 객체가 있으면 토큰에 accessToken을 추가합니다.
2. session: 세션이 생성될 때 호출됩니다. 세션 객체에 accessToken을 추가합니다.- pages: 커스텀 로그인 페이지 경로를 지정합니다.
// 서버사이드로 세션 확인 및 상태 관리
import { getServerSession } from "next-auth";
import "../../globals.css";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import SignInForm from "./_components/SignInForm";
export default async function SignIn() {
const session = await getServerSession(authOptions);
if (session?.user) {
return redirect("/");
}
return (
<article className="w-full h-screen flex items-center justify-center bg-bg-1 px-4">
<SignInForm />
</article>
);
}
[ 장점 ]
- 보안: 서버사이드에서 세션을 확인하면 클라이언트로 민감한 데이터가 전달되기 전에 유효성을 검증할 수 있습니다. 이는 클라이언트가 데이터를 조작할 수 없도록 하여 보안을 강화합니다.
- SEO: 서버사이드에서 세션을 확인하면 검색 엔진 크롤러가 페이지를 올바르게 인덱싱할 수 있습니다. 이는 특히 콘텐츠가 인증된 사용자에게만 표시되어야 하는 경우에 유용합니다.
- 빠른 리다이렉션: 서버사이드에서 세션을 확인하면 클라이언트가 페이지를 로드하기 전에 적절한 리다이렉션을 수행할 수 있습니다. 이는 불필요한 페이지 로딩을 방지하고 사용자 경험을 향상시킵니다.
[ 단점 ]
- 복잡성: 서버사이드 렌더링과 관련된 로직은 클라이언트사이드 렌더링보다 더 복잡할 수 있습니다.
- 성능: 서버사이드에서 세션을 확인하는 것은 서버 리소스를 사용하므로, 클라이언트사이드에서 확인하는 것보다 성능이 떨어질 수 있습니다.
// 클라이언트 사이드로 세션 확인 및 상태 관리
"use client";
import { useSession, signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
export default function SignInForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const { data: session, status } = useSession();
useEffect(() => {
if (status === "authenticated") {
router.push("/dashboard");
}
}, [status, router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signIn("credentials", {
redirect: false,
email,
password,
});
if (result?.error) {
console.error("Login error:", result.error);
alert("로그인에 실패하였습니다.");
} else {
router.push("/dashboard");
alert("로그인에 성공하였습니다.");
}
};
if (status === "loading") {
return <div>Loading...</div>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Email
<input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
</div>
<div>
<label>
Password
<input
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</label>
</div>
<button type="submit">Sign in</button>
</form>
);
}
[ 장점 ]
- 단순성: 클라이언트사이드에서 세션을 확인하는 로직은 더 간단하고 구현하기 쉽습니다.
- 성능: 클라이언트사이드에서 세션을 확인하면 서버의 부하를 줄이고,사용자가 페이지를 더 빠르게 로드할 수 있습니다.
- 반응성: 클라이언트사이드에서 세션을 확인하면 상태 변화에 대해 더 빠르게 반응할 수 있습니다. 예를 들어, 사용자가 로그아웃하거나 세션이 만료되었을 때 즉시 반응할 수 있습니다.
[ 단점 ]
- 보안: 클라이언트사이드에서 세션을 확인하면 클라이언트가 데이터를 조작할 수 있는 가능성이 있습니다. 따라서 중요한 검증은 서버사이드에서 수행해야 합니다.
- SEO: 클라이언트사이드에서 세션을 확인하면 검색 엔진 크롤러가 페이지를 올바르게 인덱싱하지 못할 수 있습니다. 이는 특히 콘텐츠가 인증된 사용자에게만 표시되어야 하는 경우에 문제가 될 수 있습니다.