Next.js와 PostgreSQL을 연동하여 사용자의 인증 시스템을 구현하는 방법을 다룹니다.
이 프로젝트에서는 NextAuth
를 사용하여 세션 관리를 수행하고, PostgreSQL 데이터베이스에 사용자 정보를 저장합니다.
아래 글에서 PostgresSQL 데이터베이스를 연동하는 내용을 정리 합니다.
지난 포스트
데이터 베이스 구성
PostgreSQL 데이터 베이스 구성이 우선 되어야 있어야 합니다.
이미 구성은 되어 있다고 가정하고 테이블 구성 내용부터 정리합니다.
테이블 생성
테이블을 생성하는 SQL 쿼리입니다.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
테이블 조회
생성된 테이블을 조회하는 SQL 쿼리입니다.
SELECT * FROM public.users
ORDER BY id ASC
코드 작성
디렉토리 구조
프로젝트의 디렉토리 구조는 다음과 같습니다.
/app
├── /api
│ └── /auth
│ └── [...nextauth]
│ └── route.ts // NextAuth 설정 파일
│ └── signup/route.ts // 회원 가입 api
├── /auth
│ └── signin/page.tsx // 로그인 페이지
├── /auth
│ └── signup/page.tsx // 회원 가입 페이지
├── /admin
│ └── page.tsx // 관리자 페이지
├── /lib
│ └── db.ts // PostgreSQL 연결 설정
├── /public
│ └── page.tsx // 공개 페이지
├── /layout.tsx // 전역 레이아웃
└── /page.tsx // 메인 페이지
/.env // 환경 설정
1. .env
환경 설정
PostgreSQL 데이터베이스 연결 정보를 작성합니다.
DATABASE_URL=postgres://username:password@localhost:5432/mydatabase
2. db.ts
– PostgreSQL 연결 설정
먼저, PostgreSQL 데이터베이스에 연결하는 db.ts
파일을 작성합니다. pg
패키지를 사용하여 연결을 설정합니다.
// /lib/db.ts
import { Client } from 'pg';
const client = new Client({
connectionString: process.env.DATABASE_URL, // 환경 변수에서 DB URL 가져오기
});
client.connect();
export { client };
3. /api/auth/[…nextauth]/route.ts
– NextAuth 설정 파일
NextAuth를 설정하는 route.ts
파일입니다. 이 파일은 사용자 인증을 처리하고 세션을 생성합니다. DB에서 사용자 정보를 조회하고 인증을 처리하는 로직을 추가합니다.
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { Client } from "pg"; // PostgreSQL 클라이언트 임포트
import bcrypt from "bcrypt"; // 비밀번호 해싱을 위해 bcrypt 사용
const client = new Client({
connectionString: process.env.DATABASE_URL, // PostgreSQL 데이터베이스 URL
});
// 데이터베이스 연결 함수
async function connectDB() {
await client.connect();
}
// NextAuth 설정
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "text", placeholder: "you@example.com" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
await connectDB(); // 데이터베이스 연결
// 사용자의 이메일로 데이터베이스에서 사용자 조회
const res = await client.query('SELECT * FROM users WHERE email = $1', [credentials?.email]);
const user = res.rows[0];
// 사용자 인증 로직
if (user && (await bcrypt.compare(credentials?.password, user.password))) {
return { email: user.email }; // 인증 성공 시 사용자 이메일 반환
}
return null; // 유효하지 않은 경우 null 반환
},
}),
],
pages: {
signIn: '/auth/signin',
},
secret: process.env.NEXTAUTH_SECRET,
});
// HTTP 메서드에 따라 처리하는 핸들러 설정
export { handler as GET, handler as POST };
4. signup/page.tsx
– 회원 가입 페이지
회원 가입 페이지에서는 사용자가 입력한 정보를 PostgreSQL 데이터베이스에 저장합니다. 비밀번호는 저장 전에 bcrypt
를 사용해 암호화됩니다.
// /app/auth/signup/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function SignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// API 요청하여 사용자 등록
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (res.ok) {
// 성공적으로 등록된 경우 로그인 페이지로 리다이렉트
router.push('/auth/signin');
} else {
const { message } = await res.json();
setError(message); // 오류 메시지 표시
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold text-center mb-6">Sign Up</h1>
{error && <p className="text-red-500 text-center mb-4">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<button
type="submit"
className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-300"
>
Sign Up
</button>
</form>
</div>
</div>
);
}
5. /api/auth/signup/route.ts
– 회원가입 API
회원가입 API는 사용자가 입력한 이메일과 암호화된 비밀번호를 PostgreSQL 데이터베이스에 저장합니다.
// /app/api/auth/signup/route.ts
import { NextResponse } from 'next/server';
import bcrypt from 'bcrypt';
import { client } from '@/app/lib/db'; // 데이터베이스 클라이언트 임포트
export async function POST(req: Request) {
const { email, password } = await req.json();
// 이메일 및 비밀번호 유효성 검사
if (!email || !password) {
return NextResponse.json({ message: 'Email and password are required.' }, { status: 400 });
}
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
// 사용자 추가 쿼리
const query = 'INSERT INTO users (email, password) VALUES ($1, $2) RETURNING id';
try {
const res = await client.query(query, [email, hashedPassword]);
return NextResponse.json({ message: 'User registered successfully.', userId: res.rows[0].id }, { status: 201 });
} catch (error) {
console.error(error);
return NextResponse.json({ message: 'User registration failed. Email might already be in use.' }, { status: 500 });
}
}
6. signin/page.tsx
– 로그인 페이지
로그인 페이지는 NextAuth
를 사용하여 사용자 인증을 처리합니다.
// /app/auth/signin/page.tsx
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signIn('credentials', {
redirect: false,
email,
password,
});
if (result?.error) {
setError(result.error);
} else {
router.push('/'); // 로그인 성공 시 홈 페이지로 이동
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
{error && <p className="text-red-500 text-center mb-4">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
className="w-full py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition duration-300"
>
Sign In
</button>
</form>
<div className="mt-4 text-center">
<p className="text-gray-600">Don't have an account?</p>
<button
onClick={() => router.push('/auth/signup')}
className="text-blue-600 hover:underline"
>
Sign Up
</button>
</div>
</div>
</div>
);
}
테스트
최초 화면

로그인 화면

회원가입 화면

회원가입을 진행합니다.

로그인 시도


Admin Page

마치며
이렇게 Next.js와 PostgreSQL을 연동하여 사용자 인증 및 회원가입 시스템을 구축할 수 있습니다.