Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

NextAuth와 Redis로 Next.js 블로그 보안 댓글 시스템 구축하기

이 튜토리얼에서는 블로그에 댓글 기능을 직접 구축해 보겠습니다. 사용할 기술 스택은 다음과 같습니다.

  1. Next.js 13 (App Directory 기준)
  2. NextAuth (인증 처리)
  3. Upstash Redis (댓글 저장)
  4. SWR (댓글 캐싱 및 갱신)

그럼 바로 시작해 보겠습니다.

NextAuth로 인증 시스템 구축하기

아무나 마음대로 댓글을 달 수 있게 두면 안 되겠죠? 악의적인 사용자가 스크립트를 돌려 블로그에 스팸 댓글을 도배할 수도 있습니다. 그래서 댓글 작성을 허용하기 전에 먼저 인증 시스템부터 만들어야 합니다. 여기서는 NextAuth를 사용하겠습니다.

프로젝트에 next-auth를 설치합니다.

pnpm install next-auth

App Directory 내부의 디렉터리 구조는 다음과 같습니다.

.
├── app
│ ├── api
│ │ └── auth
│ │ └── [...nextauth]
│ │ └── route.ts
│ ├── blog
│ │ ├── page.tsx
│ │ └── [...slug]
│ │ └── page.tsx
│ ├── components
│ │ └── LoginButton.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx

이제 인증 API 라우트를 설정합니다.

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
 
const handler = NextAuth({
 providers: [
 GithubProvider({
 clientId: process.env.GITHUB_CLIENT_ID!,
 clientSecret: process.env.GITHUB_CLIENT_SECRET!,
 }),
 ],
 callbacks: {
 async session({ session, token }) {
 if (session && session.user && token.sub) {
 session.user.sub = token.sub;
 }
 return session;
 },
 },
});
 
export { handler as GET, handler as POST };

GitHub 개발자 설정 페이지에서 새 OAuth 애플리케이션을 생성하면 GITHUB_CLIENT_IDGITHUB_CLIENT_SECRET 값을 발급받을 수 있습니다.

NextAuth와 Redis로 Next.js 블로그 보안 댓글 시스템 구축하기

NextAuth를 사용하면 next-auth에서 제공하는 signInsignOut 함수를 통해 어떤 클라이언트 컴포넌트에서든 로그인과 로그아웃을 처리할 수 있습니다. 다만 그 전에, 애플리케이션 전체를 감싸는 Context Provider를 먼저 설정해야 합니다.

// app/layout.tsx
"use client";
 
import "./globals.css";
 
import { SessionProvider } from "next-auth/react";
 
export default function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
 <html lang="en">
 <SessionProvider>
 <body>{children}</body>
 </SessionProvider>
 </html>
 );
}

SessionProvider를 설정하면 애플리케이션 내 모든 클라이언트 컴포넌트에서 세션 상태에 접근할 수 있습니다.

세션 상태는 next-auth가 제공하는 useSession 훅으로 확인할 수 있습니다. 아래는 로그인 버튼의 예시입니다.

"use client";
 
import { signIn, signOut, useSession } from "next-auth/react";
 
export default function LoginButton() {
 const { data: session } = useSession();
 if (session) {
 return (
 <div>
 Signed in as {session.user?.name} using {session.user?.email} <br />
 <button onClick={() => signOut()}>Sign out</button>
 </div>
 );
 }
 return (
 <div>
 Not signed in <br />
 <button onClick={() => signIn()}> Sign in </button>
 </div>
 );
}

이것으로 인증 시스템 준비가 끝났습니다. 이제 댓글을 Redis 데이터베이스에 저장할 서버 측 라우트를 만들어 보겠습니다.

Redis 데이터베이스 설정하기

  1. Upstash에 접속해 Redis 데이터베이스를 생성합니다.
  2. 사용자와 가까운 리전을 선택하고 TLS 암호화 옵션을 활성화하세요.
  3. @upstash/redis 패키지를 설치합니다.
pnpm install @upstash/redis

NextAuth와 Redis로 Next.js 블로그 보안 댓글 시스템 구축하기

  1. 발급된 토큰들을 .env.local 파일에 복사해 넣습니다.

이제 아래 디렉터리 구조에 맞춰 API 엔드포인트를 작성해 보겠습니다.

.
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ │ ├── comment
│ │ │ ├── delete
│ │ │ │ └── route.ts
│ │ │ ├── get
│ │ │ │ └── route.ts
│ │ │ └── post
│ │ │ └── route.ts
│ │ └── lib
│ │ ├── getUser.ts
│ │ └── redis.ts
  1. Redis 클라이언트 인스턴스를 생성합니다.
// app/api/lib/redis.ts
import { Redis } from "@upstash/redis";
 
/*
process.env를 통해 환경 변수에서
UPSTASH_REDIS_REST_URL과 UPSTASH_REDIS_REST_TOKEN을 불러옵니다.
*/
const redis = Redis.fromEnv();
 
export default redis;

이걸로 끝입니다. Redis 클라이언트 설정이 완료되었고, 이제 애플리케이션에서 사용할 API 라우트만 만들면 됩니다.

댓글 저장에는 Redis 리스트(List) 자료구조를 활용하겠습니다. 리스트는 스택처럼 동작할 수 있어 가장 최근 댓글이 항상 맨 위에 표시됩니다. 물론 정렬 로직을 클라이언트 측에서 직접 구현할 수도 있지만, Redis가 이미 이런 자료구조를 제공하고 있다면 그걸 활용하는 게 현명하겠죠.

핵심 로직은 다음과 같습니다.

  1. 댓글 생성: redis.lpush(referer, comment). referer라는 키를 가진 리스트에 댓글을 push합니다.

  2. 댓글 전체 조회: redis.lrange(referer, 0, -1)

  3. 댓글 삭제: redis.lrem(referer, 0, comment). referer 키의 리스트에서 해당 comment와 일치하는 모든 항목을 삭제합니다.

API 준비가 완료되었습니다. 이제 프론트엔드와 백엔드를 연결해 보겠습니다.

클라이언트 측 구현하기

생각보다 어렵지 않습니다. 방금 만든 엔드포인트에 fetch로 요청을 보내기만 하면 됩니다. 댓글의 캐싱과 재검증(revalidation)을 위해 swr이라는 라이브러리를 사용하겠습니다. React Query 같은 다른 라이브러리를 사용해도 무방합니다.

요청을 처리할 onSubmit()onDelete() 핸들러를 포함하는 useComment() 훅을 만듭니다.

"use client";
 
import { useState } from "react";
 
import type { Comment } from "@/app/interfaces/interfaces";
import useSWR from "swr";
 
const fetcher = (url: string) => fetch(url).then((res) => res.json());
 
const useComment = () => {
 const [text, setText] = useState("");
 const { data: comments, mutate } = useSWR<Comment[]>(
 "/api/comment/get",
 fetcher,
 {
 fallbackData: [],
 },
 );
 const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
 e.preventDefault();
 try {
 await fetch("/api/comment/post", {
 method: "POST",
 body: JSON.stringify({ text }),
 headers: {
 "Content-Type": "application/json",
 },
 });
 setText("");
 await mutate();
 } catch (error) {
 console.log(error);
 }
 };
 const onDelete = async (comment: Comment) => {
 try {
 await fetch("/api/comment/delete", {
 method: "POST",
 body: JSON.stringify({ comment }),
 headers: {
 "Content-Type": "application/json",
 },
 });
 await mutate();
 } catch (error) {
 console.log(error);
 }
 };
 return { text, setText, comments, onSubmit, onDelete };
};
 
export default useComment;

이것으로 댓글 섹션이 완성되었습니다. 이제 댓글을 조회하고, 작성하고, 삭제할 수 있습니다. 다음 단계는 댓글 입력창과 댓글 목록 컴포넌트를 만드는 것입니다. 참고할 수 있는 전체 예시 코드도 함께 살펴보세요.