1. json-server 설치 및 환경 구성하기
json-server를 이용해서 간단한 로그인 인증 서버를 구현해 보겠습니다. 우선 아래의 명령어로 json-server을 설치해줍니다.
npm install -g json-server
그리고 프로젝트의 루트에 db 폴더를 생성하고 그 안에 db.json을 만들어 줍니다. db.json에는 로그인 인증을 위해 아래와 같이 간단한 유저 데이터를 추가합니다.
{
"users": [
{ "id": 1, "username": "admin", "password": "admin123", "role": "admin" },
{ "id": 2, "username": "user", "password": "user123", "role": "user" }
],
"posts": [
{ "id": 1, "title": "First Post", "content": "This is the first post.", "author": "admin" },
{ "id": 2, "title": "Second Post", "content": "This is the second post.", "author": "user" }
]
}
이제 json-server 설치는 끝났습니다. 아래의 명령어로 json-server를 구동시켜주면 http://localhost:3009/users와 http://localhost:3009/posts를 통해 users와 posts 데이터를 받아올 수 있습니다.
json-server --watch db/db.json --port 3009
2. Next.js 프로젝트 생성하기
프로젝트 개요
- json-server로
3009포트에 RESTful API를 실행한 상태에서 Next.js를 이용해 로그인 기능을 구현합니다. - 로그인 후에는 인증된 사용자를 위한 페이지로 이동합니다.
- json-server에서 사용할 db.json을 생성해 임시 데이터베이스로 활용합니다.
아직 Next.js 프로젝트가 설치되어 있지 않다면 아래의 명령어로 프로젝트를 생성해주세요. 이번 로그인 인증 서버 구현 예제에서는 타입스크립트를 사용합니다.
npx create-next-app {project_name} --typescript
프로젝트 구조
프로젝트의 구조는 다음과 같습니다.
project-directory/ │ ├── .db/ │ └── db.json ├── components/ │ └── LoginForm.tsx ├── entities/ │ └── services/ │ └── auth.ts ├── lib/ │ └── verifyToken.ts ├── pages/ │ └── api/ │ ├── auth.ts │ ├── posts.ts │ └── profile.ts │ └── login.tsx │ └── profile.tsx ├── next-env.d.ts ├── package.json ├── tsconfig.json └── README.md
관련 패키지 설치하고 프로젝트 초기화하기
package.json에 아래의 패키지들을 추가하고 초기화해주세요.
{
"name": "nextjs-auth-project",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"axios": "^1.2.0",
"jsonwebtoken": "^9.0.2",
"next": "latest",
"react": "latest",
"react-dom": "latest"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^18.7.14",
"@types/react": "^18.0.19",
"@types/react-dom": "^18.0.6",
"typescript": "^4.8.3"
}
}
타입 스크립트 설정을 위해 tsconfig.json 파일을 열고 아래의 내용도 추가해줍니다.
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
next-env.d.ts 파일에 아래의 내요이 없다면 추가해주세요. next-env.d.ts 파일은 Next.js 프로젝트에서 타입스크립트를 사용할 때 필요한 타입 정의를 포함하여 프로젝트 내에서 Next.js API를 쉽게 사용할 수 있도록 돕는 중요한 환경 설정 파일입니다. 보통 자동으로 생성되며 개발자가 직접 수정할 필요는 없습니다.
/// <reference types="next" /> /// <reference types="next/types/global" />
3. 인증 미들웨어와 서비스 구현하기
verifyToken 미들웨어 만들기
lib폴더에 verifyToken.ts 파일을 다음과 같이 추가합니다. verifyToken는 JWT를 검증하기 위한 미들웨어입니다.
import { NextRequest } from "next/server";
import jwt from "jsonwebtoken";
const SECRET_KEY = "your_secret_key";
interface JwtPayload {
id: number;
role: string;
}
export interface CustomNextApiRequest extends NextRequest {
user?: JwtPayload;
}
export default async function verifyToken(req: CustomNextApiRequest): Promise<boolean> {
const authHeader = req.headers.get("authorization");
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.substring(7, authHeader.length); // "Bearer " 이후의 문자열을 추출
try {
const decoded = jwt.verify(token, SECRET_KEY) as JwtPayload;
req.user = decoded;
return true;
} catch (err) {
return false;
}
} else {
return false;
}
}
로그인 API 호출 모듈 만들기
services 폴더에 auth.ts 파일을 다음과 같이 추가합니다. auth.ts 파일은 로그인 API를 호출하는 auth 서비스 모듈입니다.
export interface LoginResponse {
id: number;
username: string;
role: string;
token: string;
}
export async function login(username: string, password: string): Promise<LoginResponse> {
const response = await fetch("/api/auth", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error("Invalid username or password");
}
return await response.json();
}
4. Next.js API 라우트 구현하기
Next.js의 app 폴더에 아래와 같이 api 파일을 추가합니다. page 방식이라면 pages 폴더에 추가해줍니다.
app/api/auth/route.ts
// app/api/auth/route.ts
import { NextRequest, NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import axios from "axios";
const SECRET_KEY = "your_secret_key";
const TOKEN_EXPIRY = "1h"; // 1시간
interface User {
id: number;
username: string;
password: string;
role: string;
}
interface LoginResponse {
id: number;
username: string;
role: string;
token: string;
}
export async function POST(request: NextRequest) {
const { username, password }: { username: string; password: string } = await request.json();
try {
// json-server로부터 사용자 데이터를 가져오기
const { data: users } = await axios.get<User[]>("http://localhost:3009/users");
const user = users.find((u) => u.username === username && u.password === password);
if (user) {
const token = jwt.sign({ id: user.id, role: user.role }, SECRET_KEY, {
expiresIn: TOKEN_EXPIRY,
});
const response: LoginResponse = {
id: user.id,
username: user.username,
role: user.role,
token,
};
return NextResponse.json(response, { status: 200 });
} else {
return NextResponse.json({ message: "Invalid username or password" }, { status: 401 });
}
} catch (error) {
console.error("Error fetching users:", error);
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
}
}
app/api/profile/route.ts
// app/api/profile/route.ts
import { NextRequest, NextResponse } from "next/server";
import verifyToken, { CustomNextApiRequest } from "@/lib/verifyToken";
interface UserProfile {
id: number;
username: string;
role: string;
}
export async function GET(request: NextRequest) {
const customRequest = request as unknown as CustomNextApiRequest;
const isValidToken = await verifyToken(customRequest);
if (!isValidToken) {
return NextResponse.json({ message: "Unauthorized" }, { status: 403 });
}
const profile: UserProfile = {
id: customRequest.user!.id,
username: customRequest.user!.role === "admin" ? "Admin User" : "Regular User",
role: customRequest.user!.role,
};
return NextResponse.json(profile, { status: 200 });
}
로그인 및 프로필 페이지 만들기
LoginForm.tsx
아래의 폴더에 LoginForm.tsx 컴포넌트 파일을 만들어줍니다. 로그인용 컴포넌트로 사용합니다.
// components/LoginForm.tsx
import React, { useState } from "react";
import { login, LoginResponse } from "@/services/auth";
interface LoginFormProps {
onLoginSuccess: (token: string) => void;
}
const LoginForm: React.FC<LoginFormProps> = ({ onLoginSuccess }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setError(null);
try {
const response: LoginResponse = await login(username, password);
localStorage.setItem("token", response.token);
onLoginSuccess(response.token);
} catch (e) {
setError("Invalid username or password");
}
};
return (
<form onSubmit={handleSubmit}>
<h2 className="text-2xl">Login</h2>
{error && <p style={{ color: "red" }}>{error}</p>}
<div>
<label>
Username:
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="border h-10" />
</label>
</div>
<div>
<label>
Password:
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border h-10"
/>
</label>
</div>
<button type="submit">Login</button>
</form>
);
};
export default LoginForm;
로그인 페이지 만들기
아래에 로그인 페이지를 추가합니다.
// app/login/page.tsx
"use client";
import { useRouter } from "next/navigation";
import React from "react";
import LoginForm from "@/components/LoginForm";
const LoginPage = () => {
const router = useRouter();
const handleLoginSuccess = (token: string) => {
router.push("/profile");
};
return (
<div>
<LoginForm onLoginSuccess={handleLoginSuccess} />
</div>
);
};
export default LoginPage;
프로필 페이지 만들기
로그인 후 이동할 프로필 페이지를 만들어줍니다.
// app/profile/page.tsx
"use client";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
interface UserProfile {
id: number;
username: string;
role: string;
}
const ProfilePage = () => {
const router = useRouter();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchProfile = async () => {
const token = localStorage.getItem("token");
if (!token) {
router.push("/login");
return;
}
const response = await fetch("/api/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const profileData: UserProfile = await response.json();
setProfile(profileData);
} else {
setError("Failed to fetch profile.");
router.push("/login");
}
};
fetchProfile();
}, [router]);
const handleLogout = () => {
localStorage.removeItem("token");
router.push("/login");
};
if (error) {
return <div>Error: {error}</div>;
}
if (!profile) {
return <div>Loading...</div>;
}
return (
<div>
<h1 className="text-2xl">Profile Page</h1>
<p>
<strong>ID:</strong> {profile.id}
</p>
<p>
<strong>Username:</strong> {profile.username}
</p>
<p>
<strong>Role:</strong> {profile.role}
</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
};
export default ProfilePage;

마치며
이번 프로젝트에서는 json-server와 Next.js를 이용해 간단한 로그인 인증 서버를 구현해 보았습니다. 이를 통해 JWT를 활용한 인증 미들웨어의 중요성을 이해하고, 다양한 페이지를 통해 인증된 사용자에게만 특정 정보를 제공하는 방법을 학습할 수 있었던 것 같습니다.