Next.js와 WordPress를 연동한 JWT 인증 구현 가이드

0

WordPress의 사용자 정보를 연동하여 Next.js 애플리케이션에 인증 기능을 구현하려면, WordPress를 인증 제공자 (Authentication Provider)로 활용하고 Next.js 애플리케이션이 이를 통해 사용자 인증을 처리하도록 설정해야 합니다. 이를 위해 JSON Web Tokens (JWT) 또는 OAuth 2.0과 같은 표준 인증 방식을 사용할 수 있습니다. 여기서는 JWT 기반 인증을 중심으로 알아보겠습니다.

전체적인 접근 방식

  • WordPress에 JWT 인증 설정
  • Next.js 애플리케이션에서 WordPress와 통신하여 인증 처리
  • Next.js API 라우트에 인증 미들웨어 적용
  • 클라이언트 측에서 인증 토큰 관리

각 단계를 자세히 살펴보겠습니다.

1. WordPress에 JWT 인증 설정

먼저, WordPress에서 JWT 인증을 설정해야 합니다. 이를 위해 JWT Authentication for WP REST API와 같은 플러그인을 사용할 수 있습니다.

1.1. JWT Authentication 플러그인 설치

  • WordPress 관리자 대시보드에 로그인합니다.
  • 플러그인 > 새로 추가로 이동합니다.
  • “JWT Authentication for WP REST API”를 검색하고 설치한 후 활성화합니다.

1.2. 플러그인 설정

플러그인을 활성화한 후, 다음 단계를 따라 설정합니다.

1. `wp-config.php` 파일 수정:

플러그인이 작동하려면 `wp-config.php` 파일에 비밀 키를 추가해야 합니다. 아래 코드를 `wp-config.php` 파일에 추가하세요. 비밀 키는 안전한 랜덤 문자열로 설정해야 합니다.

define('JWT_AUTH_SECRET_KEY', 'your_super_secret_key_here');
define('JWT_AUTH_CORS_ENABLE', true);

주의사항:

  • `JWT_AUTH_SECRET_KEY`는 절대 공개되지 않아야 하며, 복잡하고 예측하기 어려운 문자열이어야 합니다.
  • 환경 변수로 관리하는 것이 보안에 더 좋습니다. 예를 들어, `dotenv`를 사용하여 비밀 키를 관리할 수 있습니다.
2. .htaccess 파일 수정 (Apache 사용하는 경우):

`.htaccess` 파일에 다음 내용을 추가하여 JWT 인증 엔드포인트에 대한 접근을 허용합니다.

<IfModule mod_rewrite.c>
 RewriteEngine On
 RewriteCond %{HTTP:Authorization} ^(.*)
 RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
</IfModule>
3. 플러그인 문서 확인:

추가적인 설정이 필요한 경우, 플러그인 공식 문서를 참고하세요.

1.3. JWT 인증 테스트

Postman 또는 cURL을 사용하여 JWT 인증이 제대로 작동하는지 테스트합니다.

로그인 요청:
curl -X POST http://your-wordpress-site.com/wp-json/jwt-auth/v1/token \
  -d "username=your_username&password=your_password"
응답 예시:
{
  "token": "your_jwt_token_here",
  "user_email": "user@example.com",
  "user_nicename": "username",
  "user_display_name": "User Name"
}
인증된 요청 테스트:
curl -X GET http://your-wordpress-site.com/wp-json/wp/v2/posts \
  -H "Authorization: Bearer your_jwt_token_here"

정상적으로 인증된 사용자의 요청으로 처리되면, 포스트 데이터가 반환됩니다.

2. Next.js 애플리케이션에서 WordPress와 통신하여 인증 처리

이제 Next.js 애플리케이션이 WordPress의 JWT 인증을 활용하도록 설정합니다. 클라이언트 측에서 WordPress에 로그인 요청을 보내고, 받은 JWT 토큰을 저장하여 API 요청 시 사용하도록 합니다.

2.1. 필요한 패키지 설치

Next.js에서 HTTP 요청을 처리하고, JWT 토큰을 관리하기 위해 몇 가지 패키지를 설치합니다.

npm install axios
npm install --save-dev @types/axios
  • axios: HTTP 요청을 간편하게 처리할 수 있는 라이브러리입니다.
  • @types/axios: TypeScript용 타입 정의 파일입니다.

2.2. 클라이언트 측 로그인 컴포넌트 작성

WordPress의 JWT 인증을 활용하여 사용자가 로그인할 수 있는 컴포넌트를 작성합니다.

`components/Login.tsx`
import { useState } from "react";
import axios from "axios";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = async () => {
    try {
      const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
        username: email, // WordPress에서는 일반적으로 'username'을 사용
        password: password,
      });

      const { token } = response.data;

      // 토큰을 클라이언트 측에 저장 (예: localStorage)
      localStorage.setItem("token", token);

      alert("로그인 성공!");
    } catch (error: any) {
      console.error("로그인 오류:", error.response?.data?.message || error.message);
      alert("로그인 실패: " + (error.response?.data?.message || "알 수 없는 오류"));
    }
  };

  return (
    <div>
      <h2>로그인</h2>
      <input
        type="text"
        placeholder="이메일 또는 사용자 이름"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      /><br/>
      <input
        type="password"
        placeholder="비밀번호"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      /><br/>
      <button onClick={handleLogin}>로그인</button>
    </div>
  );
};

export default Login;
설명:
  • 사용자가 이메일(또는 사용자 이름)과 비밀번호를 입력하면 WordPress의 JWT 엔드포인트로 로그인 요청을 보냅니다.
  • 성공적으로 인증되면 받은 JWT 토큰을 `localStorage`에 저장합니다.

2.3. 클라이언트 측 로그아웃 및 토큰 삭제

사용자가 로그아웃할 수 있는 기능을 추가하여 저장된 토큰을 삭제합니다.

`components/Logout.tsx`
import { useEffect } from "react";

const Logout = () => {
  useEffect(() => {
    // 로그아웃 시 토큰 삭제
    localStorage.removeItem("token");
    alert("로그아웃 되었습니다.");
  }, []);

  return null;
};

export default Logout;

2.4. API 요청 시 JWT 토큰 포함

Next.js 애플리케이션의 API 요청 시 저장된 JWT 토큰을 `Authorization` 헤더에 포함시켜 인증된 요청으로 만듭니다.

예시: 데이터 가져오기 컴포넌트
import { useEffect, useState } from "react";
import axios from "axios";

const FetchData = () => {
  const [data, setData] = useState<any[]>([]);
  const [error, setError] = useState<string | null>(null);

  const fetchData = async () => {
    const token = localStorage.getItem("token");

    if (!token) {
      setError("로그인이 필요합니다.");
      return;
    }

    try {
      const response = await axios.get("/api/data", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      setData(response.data);
    } catch (err: any) {
      console.error("데이터 가져오기 오류:", err.response?.data?.error || err.message);
      setError(err.response?.data?.error || "데이터를 가져오는 중 오류가 발생했습니다.");
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  if (error) {
    return <div>{error}</div>;
  }

  return (
    <div>
      <h2>데이터 리스트</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default FetchData;
설명:
  • `localStorage`에서 JWT 토큰을 가져와 `Authorization` 헤더에 포함시킵니다.
  • 서버의 API 라우트(`/api/data`)는 이 토큰을 검증하여 인증된 사용자만 접근할 수 있도록 합니다.

3. Next.js API 라우트에 인증 미들웨어 적용

Next.js의 API 라우트에서 JWT 토큰을 검증하여 인증된 사용자만 데이터에 접근할 수 있도록 합니다.

3.1. JWT 검증 유틸리티 작성

JWT 토큰을 검증할 수 있는 유틸리티 함수를 작성합니다.

`utils/jwt.ts`
import jwt from "jsonwebtoken";

interface JwtPayload {
  userId: string;
  email: string;
  exp: number;
}

export const verifyJWT = (token: string): JwtPayload | null => {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
    return decoded;
  } catch (error) {
    console.error("JWT 검증 오류:", error);
    return null;
  }
};
설명:
  • `verifyJWT` 함수는 주어진 토큰을 검증하고, 유효하면 페이로드를 반환합니다. 그렇지 않으면 `null`을 반환합니다.
  • 주의: WordPress와 Next.js가 동일한 비밀 키를 사용해야 JWT 토큰을 올바르게 검증할 수 있습니다. 그러나 보안상의 이유로 이는 권장되지 않습니다. 따라서, 별도의 검증 방식을 고려해야 합니다.

3.2. 환경 변수 설정

Next.js 애플리케이션에서 WordPress JWT 토큰을 검증하려면, WordPress에서 사용하는 `JWT_AUTH_SECRET_KEY`와 동일한 비밀 키를 `NEXT_PUBLIC_JWT_SECRET` 환경 변수에 설정해야 합니다.

`.env.local`
NEXT_PUBLIC_JWT_SECRET=your_super_secret_key_here
  • 주의: `NEXT_PUBLIC_` 접두사는 클라이언트 측에서 접근할 수 있도록 합니다. 보안상의 이유로 비밀 키는 서버 측에서만 사용되어야 합니다. 따라서, 별도의 환경 변수를 사용하는 것이 좋습니다.
수정된 `utils/jwt.ts`

보안을 위해 서버 측에서만 사용할 수 있도록 환경 변수를 관리합니다. 따라서, `NEXT_PUBLIC_JWT_SECRET` 대신 `JWT_SECRET`을 사용합니다.

import jwt from "jsonwebtoken";

interface JwtPayload {
  userId: string;
  email: string;
  exp: number;
}

export const verifyJWT = (token: string): JwtPayload | null => {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
    return decoded;
  } catch (error) {
    console.error("JWT 검증 오류:", error);
    return null;
  }
};
`.env.local`
MONGODB_URI=mongodb://localhost:27017/mongo
JWT_SECRET=your_super_secret_key_here
  • 주의: `JWT_SECRET`은 서버 측에서만 사용되며, 클라이언트 측에서는 접근할 수 없습니다.

3.3. 인증 미들웨어 작성

API 라우트에서 JWT 토큰을 검증하는 미들웨어를 작성합니다.

`middlewares/auth.ts`
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/utils/jwt";

export const authenticate = (request: NextRequest): { userId: string; email: string } | null => {
  const authHeader = request.headers.get("Authorization");

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return null;
  }

  const token = authHeader.split(" ")[1];
  const decoded = verifyJWT(token);

  return decoded;
};
설명:
  • `authenticate` 함수는 요청의 `Authorization` 헤더에서 Bearer 토큰을 추출하고, 이를 검증합니다.
  • 유효한 토큰이면 페이로드를 반환하고, 그렇지 않으면 `null`을 반환합니다.

3.4. 기존 API 라우트 수정

이제 기존의 `app/api/data/route.ts` 파일에 인증 미들웨어를 적용합니다.

수정된 `app/api/data/route.ts`
import { NextRequest, NextResponse } from "next/server";
import mongooseConnect from "@/lib/mongoose";
import Data from "@/models/annotation";
import { authenticate } from "@/middlewares/auth";

export async function GET(request: NextRequest) {
  const user = authenticate(request);

  if (!user) {
    return NextResponse.json({ error: "인증되지 않았습니다." }, { status: 401 });
  }

  try {
    await mongooseConnect();
    const data = await Data.find({});
    return NextResponse.json(data, { status: 200 });
  } catch (error) {
    console.error("GET Error:", error);
    return NextResponse.json({ error: "데이터를 가져오는 중 오류 발생" }, { status: 500 });
  }
}

export async function POST(request: NextRequest) {
  const user = authenticate(request);

  if (!user) {
    return NextResponse.json({ error: "인증되지 않았습니다." }, { status: 401 });
  }

  try {
    const body = await request.json();
    await mongooseConnect();
    const newData = new Data(body);
    await newData.save();
    return NextResponse.json(newData, { status: 201 });
  } catch (error) {
    console.error("POST Error:", error);
    return NextResponse.json({ error: "데이터를 저장하는 중 오류 발생" }, { status: 500 });
  }
}
설명:
  • 각 API 핸들러(`GET`, `POST`)에서 `authenticate` 함수를 호출하여 인증을 확인합니다.
  • 인증되지 않은 사용자는 401 상태 코드와 함께 오류 메시지를 반환합니다.
  • 인증된 사용자는 기존 로직을 수행합니다.

4. 클라이언트 측에서 인증 토큰 관리

클라이언트 측에서 JWT 토큰을 안전하게 관리하고, 필요할 때 API 요청에 포함시켜야 합니다. 여기서는 `localStorage`를 사용하지만, 보안상의 이유로 `HttpOnly` 쿠키를 사용하는 것이 더 안전합니다. 그러나, Next.js의 API 라우트에서는 `HttpOnly` 쿠키를 직접 설정할 수 없으므로, 이 예제에서는 `localStorage`를 사용합니다.

4.1. 로그인 컴포넌트 예제

앞서 작성한 `Login` 컴포넌트는 WordPress의 JWT 엔드포인트를 사용하여 로그인하고, 받은 토큰을 `localStorage`에 저장합니다.

`components/Login.tsx`
import { useState } from "react";
import axios from "axios";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = async () => {
    try {
      const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
        username: email, // WordPress에서는 'username'을 사용
        password: password,
      });

      const { token } = response.data;

      // 토큰을 클라이언트 측에 저장 (예: localStorage)
      localStorage.setItem("token", token);

      alert("로그인 성공!");
    } catch (error: any) {
      console.error("로그인 오류:", error.response?.data?.message || error.message);
      alert("로그인 실패: " + (error.response?.data?.message || "알 수 없는 오류"));
    }
  };

  return (
    <div>
      <h2>로그인</h2>
      <input
        type="text"
        placeholder="이메일 또는 사용자 이름"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      /><br/>
      <input
        type="password"
        placeholder="비밀번호"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      /><br/>
      <button onClick={handleLogin}>로그인</button>
    </div>
  );
};

export default Login;

4.2. 데이터 요청 시 토큰 포함

데이터를 가져오거나 저장할 때 JWT 토큰을 포함하여 인증된 요청으로 만듭니다.

`components/FetchData.tsx`
import { useEffect, useState } from "react";
import axios from "axios";

const FetchData = () => {
  const [data, setData] = useState<any[]>([]);
  const [error, setError] = useState<string | null>(null);

  const fetchData = async () => {
    const token = localStorage.getItem("token");

    if (!token) {
      setError("로그인이 필요합니다.");
      return;
    }

    try {
      const response = await axios.get("/api/data", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      setData(response.data);
    } catch (err: any) {
      console.error("데이터 가져오기 오류:", err.response?.data?.error || err.message);
      setError(err.response?.data?.error || "데이터를 가져오는 중 오류가 발생했습니다.");
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  if (error) {
    return <div>{error}</div>;
  }

  return (
    <div>
      <h2>데이터 리스트</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default FetchData;
설명:
  • `localStorage`에서 JWT 토큰을 가져와 `Authorization` 헤더에 포함시킵니다.
  • API 요청 시 이 토큰을 통해 인증을 수행합니다.

5. 보안 고려 사항

5.1. HTTPS 사용

JWT 토큰은 민감한 정보이므로, HTTPS를 사용하여 데이터 전송 시 암호화하는 것이 필수적입니다. 특히, 프로덕션 환경에서는 반드시 HTTPS를 사용해야 합니다.

5.2. 토큰 저장 방식

  • localStorage: 간편하지만, XSS 공격에 취약할 수 있습니다.
  • HttpOnly Cookies: XSS 공격에 강하지만, CSRF 공격에 취약할 수 있습니다. CSRF 방어를 위해 추가적인 방어 메커니즘을 구현해야 합니다.
  • 추천: 가능하면 `HttpOnly` 쿠키를 사용하고, CSRF 방어를 위해 CSRF 토큰을 도입하세요. 그러나, Next.js에서는 API 라우트에서 `HttpOnly` 쿠키를 설정하는 방식으로 구현할 수 있습니다.

5.3. 토큰 만료 및 갱신

JWT 토큰은 만료 시간을 설정하여 보안을 강화해야 합니다. 위 예제에서는 1시간으로 설정했습니다. 토큰이 만료되면, 사용자가 다시 로그인하거나 Refresh Token을 통해 새로운 Access Token을 발급받을 수 있도록 구현해야 합니다.

5.4. 비밀 키 관리

`JWT_SECRET`는 매우 중요한 정보이므로, 절대 공개되지 않도록 주의해야 합니다. 환경 변수로 관리하고, `.env.local` 파일은 절대 버전 관리 시스템(Git 등)에 포함되지 않도록 `.gitignore`에 추가하세요.

5.5. 비밀번호 보안

  • 비밀번호는 반드시 해싱하여 저장해야 합니다. 위 예제에서는 WordPress의 JWT 인증을 사용하므로, WordPress가 비밀번호를 해싱하여 저장합니다.
  • 강력한 비밀번호 정책을 적용하여 보안을 강화하세요.

5.6. 입력 검증

사용자로부터 입력받는 모든 데이터는 철저히 검증하고, 필요한 경우 정규 표현식을 사용하여 유효성을 검사하세요.

5.7. 에러 메시지 관리

에러 메시지에 민감한 정보를 포함하지 않도록 주의하세요. 공격자가 시스템의 내부 구조를 파악하는 데 도움이 될 수 있습니다.

5.8. Rate Limiting

API 엔드포인트에 Rate Limiting을 적용하여 DDoS 공격을 방어하세요. 이를 위해 `express-rate-limit`와 같은 미들웨어를 사용할 수 있습니다.

최종 디렉토리 구조

다음은 전체 프로젝트의 디렉토리 구조 예시입니다:

my-app/
  app/
    api/
      auth/
        login/
          route.ts
        signup/
          route.ts
      data/
        route.ts
  components/
    Login.tsx
    Logout.tsx
    FetchData.tsx
  lib/
    mongoose.ts
  middlewares/
    auth.ts
  models/
    annotation.ts
  types/
    annotation.ts
    global.d.ts
  utils/
    jwt.ts
  .eslintrc.json
  .env.local
  tsconfig.json
  package.json
  ...

전체 코드 예제

1. `models/annotation.ts`

import mongoose, { Schema, Document, Model, model } from "mongoose";
import { Annotation, Maskings } from "../types/annotation";

export interface List {
  id: number;
  src: string;
  annotations: Annotation[];
  maskings: Maskings[][];
}

export interface DataDocument extends Document {
  id: string;
  list: List[];
}

const AnnotationSchema: Schema<Annotation> = new Schema({
  x: { type: Number, required: true },
  y: { type: Number, required: true },
  width: { type: Number, required: true },
  height: { type: Number, required: true },
  classType: { type: String, required: true },
  label: { type: String, required: true },
  color: { type: String, required: true },
});

const MaskingsSchema: Schema<Maskings> = new Schema({
  x: { type: Number, required: true },
  y: { type: Number, required: true },
}, { _id: false });

const ListSchema: Schema<List> = new Schema({
  id: { type: Number, required: true },
  src: { type: String, required: true },
  annotations: { type: [AnnotationSchema], required: true },
  maskings: { type: [[MaskingsSchema]], required: true },
});

const DataSchema: Schema<DataDocument> = new Schema({
  id: { type: String, required: true, unique: true },
  list: { type: [ListSchema], required: true },
});

export default mongoose.models.Data || model<DataDocument>("Data", DataSchema);

2. `models/User.ts`

import mongoose, { Schema, Document, Model, model } from "mongoose";

export interface IUser extends Document {
  email: string;
  password: string;
}

const UserSchema: Schema<IUser> = new Schema({
  email: { type: String, required: true, unique: true, lowercase: true },
  password: { type: String, required: true },
});

export default mongoose.models.User || model<IUser>("User", UserSchema);

3. `app/api/auth/signup/route.ts`

import { NextRequest, NextResponse } from "next/server";
import axios from "axios";
import mongooseConnect from "@/lib/mongoose";
import User from "@/models/User";
import bcrypt from "bcryptjs";

export async function POST(request: NextRequest) {
  try {
    const { email, password } = await request.json();

    if (!email || !password) {
      return NextResponse.json({ error: "이메일과 비밀번호는 필수입니다." }, { status: 400 });
    }

    await mongooseConnect();

    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return NextResponse.json({ error: "이미 등록된 이메일입니다." }, { status: 400 });
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = new User({
      email,
      password: hashedPassword,
    });

    await newUser.save();

    return NextResponse.json({ message: "유저가 성공적으로 등록되었습니다." }, { status: 201 });
  } catch (error) {
    console.error("Signup Error:", error);
    return NextResponse.json({ error: "유저 등록 중 오류가 발생했습니다." }, { status: 500 });
  }
}

4. `app/api/auth/login/route.ts`

import { NextRequest, NextResponse } from "next/server";
import axios from "axios";

export async function POST(request: NextRequest) {
  try {
    const { email, password } = await request.json();

    if (!email || !password) {
      return NextResponse.json({ error: "이메일과 비밀번호는 필수입니다." }, { status: 400 });
    }

    // WordPress JWT Authentication 엔드포인트로 로그인 요청
    const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
      username: email, // WordPress에서는 'username'을 사용
      password: password,
    });

    const { token } = response.data;

    return NextResponse.json({ token }, { status: 200 });
  } catch (error: any) {
    console.error("Login Error:", error.response?.data?.message || error.message);
    return NextResponse.json({ error: "로그인 중 오류가 발생했습니다." }, { status: 500 });
  }
}
설명:
  • `login` API는 WordPress의 JWT 엔드포인트에 로그인 요청을 보내고, 받은 토큰을 클라이언트에 반환합니다.
  • 이 방식은 Next.js 애플리케이션과 WordPress 간의 인증 연동을 단순화합니다.

5. `utils/jwt.ts`

import jwt from "jsonwebtoken";

interface JwtPayload {
  userId: string;
  email: string;
  exp: number;
}

export const verifyJWT = (token: string): JwtPayload | null => {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
    return decoded;
  } catch (error) {
    console.error("JWT 검증 오류:", error);
    return null;
  }
};

6. `middlewares/auth.ts`

import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/utils/jwt";

export const authenticate = (request: NextRequest): { userId: string; email: string } | null => {
  const authHeader = request.headers.get("Authorization");

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return null;
  }

  const token = authHeader.split(" ")[1];
  const decoded = verifyJWT(token);

  return decoded;
};

7. `app/api/data/route.ts`

import { NextRequest, NextResponse } from "next/server";
import mongooseConnect from "@/lib/mongoose";
import Data from "@/models/annotation";
import { authenticate } from "@/middlewares/auth";

export async function GET(request: NextRequest) {
  const user = authenticate(request);

  if (!user) {
    return NextResponse.json({ error: "인증되지 않았습니다." }, { status: 401 });
  }

  try {
    await mongooseConnect();
    const data = await Data.find({});
    return NextResponse.json(data, { status: 200 });
  } catch (error) {
    console.error("GET Error:", error);
    return NextResponse.json({ error: "데이터를 가져오는 중 오류 발생" }, { status: 500 });
  }
}

export async function POST(request: NextRequest) {
  const user = authenticate(request);

  if (!user) {
    return NextResponse.json({ error: "인증되지 않았습니다." }, { status: 401 });
  }

  try {
    const body = await request.json();
    await mongooseConnect();
    const newData = new Data(body);
    await newData.save();
    return NextResponse.json(newData, { status: 201 });
  } catch (error) {
    console.error("POST Error:", error);
    return NextResponse.json({ error: "데이터를 저장하는 중 오류 발생" }, { status: 500 });
  }
}

8. `types/global.d.ts`

import mongoose from "mongoose";

declare global {
  var mongoose: {
    conn: mongoose.Mongoose | null;
    promise: Promise<mongoose.Mongoose> | null;
  };
}

export {};

9. `lib/mongoose.ts`

import mongoose from 'mongoose';

const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mindcrash';

if (!MONGODB_URI) {
  throw new Error('Please define the MONGODB_URI environment variable');
}

let cached = global.mongoose;

if (!cached) {
  cached = global.mongoose = { conn: null, promise: null };
}

async function mongooseConnect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      bufferCommands: false,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongooseInstance) => {
      return mongooseInstance;
    });
  }
  cached.conn = await cached.promise;
  return cached.conn;
}

export default mongooseConnect;

10. `.eslintrc.json`

{
  "extends": ["next/core-web-vitals", "next/typescript"],
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "@typescript-eslint/no-explicit-any": "off"
  },
  "overrides": [
    {
      "files": ["**/*.d.ts"],
      "rules": {
        "no-var": "off"
      }
    }
  ]
}

11. `tsconfig.json`

{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "types": []
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"],
  "exclude": ["node_modules"]
}

6. 클라이언트 컴포넌트 예제

6.1. `components/Login.tsx`

import { useState } from "react";
import axios from "axios";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handleLogin = async () => {
    try {
      const response = await axios.post("/api/auth/login", {
        email,
        password,
      });

      const { token } = response.data;

      // 토큰을 클라이언트 측에 저장 (예: localStorage)
      localStorage.setItem("token", token);

      alert("로그인 성공!");
    } catch (error: any) {
      console.error("로그인 오류:", error.response?.data?.error || error.message);
      alert("로그인 실패: " + (error.response?.data?.error || "알 수 없는 오류"));
    }
  };

  return (
    <div>
      <h2>로그인</h2>
      <input
        type="text"
        placeholder="이메일 또는 사용자 이름"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      /><br/>
      <input
        type="password"
        placeholder="비밀번호"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      /><br/>
      <button onClick={handleLogin}>로그인</button>
    </div>
  );
};

export default Login;

6.2. `components/FetchData.tsx`

import { useEffect, useState } from "react";
import axios from "axios";

const FetchData = () => {
  const [data, setData] = useState<any[]>([]);
  const [error, setError] = useState<string | null>(null);

  const fetchData = async () => {
    const token = localStorage.getItem("token");

    if (!token) {
      setError("로그인이 필요합니다.");
      return;
    }

    try {
      const response = await axios.get("/api/data", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      setData(response.data);
    } catch (err: any) {
      console.error("데이터 가져오기 오류:", err.response?.data?.error || err.message);
      setError(err.response?.data?.error || "데이터를 가져오는 중 오류가 발생했습니다.");
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  if (error) {
    return <div>{error}</div>;
  }

  return (
    <div>
      <h2>데이터 리스트</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default FetchData;

결론

위의 단계를 따르면 WordPress의 사용자 정보를 연동하여 Next.js 애플리케이션에 인증 기능을 성공적으로 구현할 수 있습니다. 요약하면:

  • WordPress에 JWT 인증 설정: `JWT Authentication for WP REST API` 플러그인을 설치하고 설정하여 JWT 기반 인증을 활성화합니다.
  • Next.js에서 WordPress와 통신: 로그인 요청을 WordPress의 JWT 엔드포인트로 보내고, 받은 토큰을 클라이언트 측에 저장합니다.
  • Next.js API 라우트에 인증 적용: API 라우트에서 JWT 토큰을 검증하여 인증된 사용자만 접근할 수 있도록 합니다.
  • 클라이언트 측에서 토큰 관리: 클라이언트 측에서 토큰을 안전하게 저장하고, API 요청 시 토큰을 포함시켜 인증된 요청을 보냅니다.

추가 고려 사항:

  • 보안 강화: HTTPS 사용, 토큰 저장 방식 선택, CSRF 방어, 토큰 만료 및 갱신 등 보안 관련 사항을 철저히 고려하세요.
  • 환경 변수 관리: 비밀 키와 같은 민감한 정보는 환경 변수로 관리하고, 버전 관리 시스템에 포함되지 않도록 `.gitignore`에 추가하세요.
  • 사용자 경험 개선: 토큰 만료 시 자동 로그아웃, Refresh Token 도입 등을 통해 사용자 경험을 개선할 수 있습니다.

Leave a Reply