본문으로 건너뛰기
최서희Frontend Engineer
← 블로그

Frontend

[유데미 러닝크루] Next.js 스터디 3주차 – Next.js와 Lucia로 로그인부터 세션 관리까지, 인증 시스템 구현

· 3분 읽기

📑 목차

1. 3주차 진행 범위

📝 세션 5 ~ 11 강의까지 진행

[Udemy 강의 소개]

이번 스터디에서 수강하는 강의는 【한글자막】 Next.js 15 & React - 완벽 가이드이다. 이 강의에서는 Next.js 15를 기초부터 학습하고, 앱 라우터 & 페이지 라우터를 활용하여 풀스택 앱을 구축하는 방법을 배울 수 있다.

💡 강의 정보

  • 강의명: 【한글자막】 Next.js 15 & React - 완벽 가이드
  • 강의 시간: 40시간
  • 수강생: 5,021명
  • 강의 평가: ⭐ 4.7/5 (585명 평가)
  • 언어: 한국어 자막 제공

📌 강의 주요 내용

✅ Next.js 15의 앱 라우터 & 페이지 라우터 ✅ 서버/클라이언트 컴포넌트 이해 ✅ 서버 액션 기반 데이터 처리 ✅ SSR·파일 기반 라우팅·인증 기능

2. 3주차 학습 내용 정리 — Next.js와 Lucia를 활용한 인증 구현

목표: 회원가입 → 로그인 → 보호 페이지 접근 → 로그아웃 흐름 완성

설치 — 필요한 패키지

npm i lucia @lucia-auth/adapter-sqlite better-sqlite3 bcrypt

폴더 구조 예시

app/
  (auth)/
    layout.js
    login/page.jsx
    signup/page.jsx
  actions/
    auth-actions.js
components/
  auth-form.jsx
  logout-button.jsx
lib/
  db.js
  user.js
  auth.js

1️⃣ 사용자 계정 생성 (Signup)

  • app/actions/auth-actions.js에 서버 액션 정의 ("use server")
  • formData에서 email, password 추출 후 유효성 검사
  • lib/user.js를 통해 DB 저장(이메일 중복 체크, bcrypt 해시)

서버 액션: signup

// app/actions/auth-actions.js
"use server";
import { createUser, getUserByEmail } from "@/lib/user";
import { createAuthSession } from "@/lib/auth";
import bcrypt from "bcrypt";

export async function signup(prevState, formData) {
  const email = (formData.get("email") || "").toString().trim();
  const password = (formData.get("password") || "").toString();

  // 유효성 검사
  if (!email.includes("@")) return { ok:false, message:"유효한 이메일을 입력하세요." };
  if (password.length < 8) return { ok:false, message:"비밀번호는 8자 이상이어야 합니다." };

  // 중복 체크
  const exists = await getUserByEmail(email);
  if (exists) return { ok:false, message:"이미 가입된 이메일입니다." };

  const hash = await bcrypt.hash(password, 10);
  const user = await createUser({ email, passwordHash: hash });

  // 세션 생성 (자동 로그인)
  await createAuthSession(user.id);
  return { ok:true, message:"가입 완료" };
}

DB 유틸: 사용자 저장/조회

// lib/db.js
import Database from "better-sqlite3";
export const db = new Database("app.db");

// lib/user.js
import { db } from "./db";

export async function createUser({ email, passwordHash }) {
  const stmt = db.prepare("INSERT INTO users (email, password_hash) VALUES (?, ?)");
  const info = stmt.run(email, passwordHash);
  return { id: info.lastInsertRowid, email };
}

export async function getUserByEmail(email) {
  const row = db.prepare("SELECT id, email, password_hash FROM users WHERE email = ?").get(email);
  return row || null;
}

폼 + 오류 출력 (client)

// components/auth-form.jsx
"use client";
import { useFormState } from "react-dom";
import { signup, login } from "@/app/actions/auth-actions";

export default function AuthForm({ mode = "login" }) {
  const action = mode === "login" ? login : signup;
  const [state, formAction] = useFormState(action, { ok:false, message:"" });

  return (
    <form action={formAction}>
      <input name="email" type="email" placeholder="email" required />
      <input name="password" type="password" placeholder="password (8+)" required />
      <button type="submit">{mode === "login" ? "로그인" : "회원가입"}</button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

2️⃣ 로그인 (Login)

  • getUserByEmail()로 사용자 조회 → bcrypt.compare로 해시 검증
  • 성공 시 세션 생성 후 리디렉션
// app/actions/auth-actions.js (일부)
export async function login(prevState, formData) {
  const email = (formData.get("email") || "").toString().trim();
  const password = (formData.get("password") || "").toString();

  const user = await getUserByEmail(email);
  if (!user) return { ok:false, message:"이메일 또는 비밀번호가 올바르지 않습니다." };

  const ok = await bcrypt.compare(password, user.password_hash);
  if (!ok) return { ok:false, message:"이메일 또는 비밀번호가 올바르지 않습니다." };

  await createAuthSession(user.id);
  return { ok:true, message:"로그인 성공" };
}

3️⃣ Lucia를 활용한 인증 세션 관리

  • lib/auth.js에서 Lucia 초기화 + 세션 쿠키 생성/검증
  • 보호 페이지: (auth) 그룹의 layout.js에서 세션 검증 후 접근 제한
// lib/auth.js  (Lucia v3 예시)
import { Lucia } from "lucia";
import { nextjs } from "lucia/middleware";
import { betterSqlite3 } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { cookies } from "next/headers";

export const auth = new Lucia(betterSqlite3(db), {
  sessionCookie: { attributes: { secure: process.env.NODE_ENV === "production" } },
  middleware: nextjs()
});

export async function createAuthSession(userId) {
  const session = await auth.createSession(userId, {});
  const cookie = auth.createSessionCookie(session.id);
  cookies().set(cookie.name, cookie.value, cookie.attributes);
}

export async function destroySession() {
  const sessionId = cookies().get(auth.sessionCookieName)?.value;
  if (!sessionId) return;
  await auth.invalidateSession(sessionId);
  const blank = auth.createBlankSessionCookie();
  cookies().set(blank.name, blank.value, blank.attributes);
}

export async function verifyAuth() {
  const sessionId = cookies().get(auth.sessionCookieName)?.value ?? null;
  if (!sessionId) return { user:null, session:null };
  return auth.validateSession(sessionId); // { user, session }
}
// app/(auth)/layout.js  — 보호 라우트
import { verifyAuth } from "@/lib/auth";
export default async function AuthLayout({ children }) {
  const { user } = await verifyAuth();
  if (!user) {
    // 비로그인: 로그인 페이지로 리다이렉트 (간단 예시)
    return (<div>로그인이 필요합니다. <a href="/login">로그인</a></div>);
  }
  return (<section>{children}</section>);
}

4️⃣ 로그아웃

  • 세션 무효화 + 쿠키 삭제 → 로그인 페이지로 이동
// app/actions/auth-actions.js (일부)
import { destroySession } from "@/lib/auth";

export async function logout() {
  "use server";
  await destroySession();
}
// components/logout-button.jsx
"use client";
import { logout } from "@/app/actions/auth-actions";

export default function LogoutButton(){
  return <form action={logout}>
    <button type="submit">로그아웃</button>
  </form>;
}

5️⃣ 로그인/회원가입 모드 전환

  • ?mode=login / ?mode=signup 쿼리로 폼 모드 전환
// app/login/page.jsx  — 로그인 페이지
import AuthForm from "@/components/auth-form";
export default function Page(){ return (<AuthForm mode="login" />); }

// app/signup/page.jsx — 회원가입 페이지
import AuthForm from "@/components/auth-form";
export default function Page(){ return (<AuthForm mode="signup" />); }

🔥 최종 정리 — 기능 점검

  • ✅ 회원가입 → 자동 로그인(세션 생성)
  • ✅ 보호 페이지 접근 확인((auth)/layout.js 검증)
  • ✅ 로그아웃 후 접근 차단
  • ✅ 로그인 재시도 정상 동작
  • ✅ 개발자도구 Application > Cookies에서 세션 쿠키 존재 확인

메모

할 일상태
URL 쿼리 파라미터로 폼 모드 전환완료
로그아웃 액션 추가완료
Lucia 설치 및 설정완료
로그인 액션 추가완료
폴더 구조 정리완료

댓글

GitHub 계정으로 댓글을 남길 수 있어요.