Project

[javascript] 네이버 클론 코딩 + 블로그 4: JWT 발급

끊임없이 개발하는 새럼 2025. 3. 12. 10:19

🚀 현재 프로젝트 환경 정리

 네이버 스타일 로그인 & 블로그 시스템 구현 중
 사용된 기술 및 개발 도구 정리


📌 프로젝트 개요

  • 프로젝트명: 네이버 스타일 로그인 & 블로그 클론 코딩
  • DB: 로컬 스토리지 (LocalStorage) 사용
  • 프레임워크: React (JSX 기반 UI 구성)
  • 백엔드: Node.js
  • 개발 언어: Javascript
  • 개발툴: Visual Studio Code

 


 

 

JWT 발급을 위해 Node.js가 필요했다.
Node.js를 설치한 후, server.js 파일을 생성하고 [node server.js] 명령어로 서버를 실행했다.

 

server.js

const express = require("express");
const jwt = require("jsonwebtoken");
const cors = require("cors");
const cookieParser = require("cookie-parser");

const app = express();
const PORT = 5000;
const SECRET_KEY = "ss"; // 🔒 JWT 서명 키

app.use(express.json());
app.use(cookieParser());
app.use(
  cors({
    origin: "http://localhost:3000", // 프론트엔드 URL
    credentials: true, // 쿠키 허용
  })
);

// ✅ 유저 데이터 저장 (DB 대신 배열 사용)
let users = [
  { id: 1, username: "admin", password: "1234" },
  { id: 2, username: "user", password: "password" },
];

// ✅ 회원가입 API
app.post("/register", (req, res) => {
  const { username, password } = req.body;

  // 아이디 중복 확인
  const existingUser = users.find((u) => u.username === username);
  if (existingUser) {
    return res.status(400).json({ message: "이미 존재하는 아이디입니다." });
  }

  // 새로운 유저 추가
  const newUser = { id: Date.now(), username, password };
  users.push(newUser);

  res.json({ message: "회원가입 성공! 로그인하세요." });
});

// ✅ 로그인 API (JWT 발급)
app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const user = users.find((u) => u.username === username && u.password === password);

  if (!user) {
    return res.status(401).json({ message: "아이디 또는 비밀번호가 잘못되었습니다." });
  }

  // ✅ JWT 생성
  const token = jwt.sign({ id: user.id, username: user.username }, SECRET_KEY, { expiresIn: "1d" });

  // ✅ HttpOnly 쿠키에 저장
  res.cookie("token", token, { httpOnly: true, secure: false, maxAge: 86400000 });

  // ✅ 클라이언트에서 저장할 유저 정보 반환
  res.json({
    message: "로그인 성공!",
    user: { id: user.id, username: user.username },
    token,
  });
});

// ✅ 인증 확인 API
app.get("/auth", (req, res) => {
  const token = req.cookies.token;
  if (!token) return res.status(401).json({ message: "인증되지 않은 사용자입니다." });

  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) return res.status(403).json({ message: "토큰이 유효하지 않습니다." });
    res.json({ user: decoded });
  });
});

// ✅ 로그아웃 API
app.post("/logout", (req, res) => {
  res.clearCookie("token");
  res.json({ message: "로그아웃 완료" });
});

app.listen(PORT, () => console.log(`🚀 서버 실행 중: http://localhost:${PORT}`));

 

 

Register.js

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./Register.css";

function Register() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [confirmPassword, setConfirmPassword] = useState("");
  const navigate = useNavigate();

  const handleRegister = async (e) => {
    e.preventDefault();
  
    const newUser = { username, password };
    const response = await fetch("http://localhost:5000/register", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(newUser),
    });
  
    if (response.ok) {
      alert("회원가입 성공! 로그인해주세요.");
      localStorage.setItem("loggedInUser", JSON.stringify(newUser)); // ✅ 사용자 정보 저장
      navigate("/login");
    } else {
      alert("회원가입 실패");
    }
  };

  return (
    <div className="register-container">
      <h1>회원가입</h1>
      <form onSubmit={handleRegister}>
        <input
          type="text"
          placeholder="아이디"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="비밀번호"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="비밀번호 확인"
          value={confirmPassword}
          onChange={(e) => setConfirmPassword(e.target.value)}
          required
        />
        <button type="submit">회원가입</button>
      </form>
      <p>
        이미 계정이 있으신가요? <span onClick={() => navigate("/login")}>로그인</span>
      </p>
    </div>
  );
}

export default Register;

 

 

Login.js

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./Login.css";

function Login() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [isIpSecurityOn, setIsIpSecurityOn] = useState(true);
  const navigate = useNavigate();

  const handleLogin = async (e) => {
    e.preventDefault();
  
    const response = await fetch("http://localhost:5000/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password }),
    });
  
    const data = await response.json();
  
    if (response.ok) {
      alert("로그인 성공!");
      localStorage.setItem("token", data.token); // ✅ JWT 저장
      localStorage.setItem("loggedInUser", JSON.stringify({ username })); // ✅ 사용자 정보 저장
      navigate("/dashboard");
    } else {
      alert("로그인 실패: " + data.message);
    }
  };

  return (
    <div className="login-container">
      <div className="login-box">
        <div className="login-tabs">
          <span className="active-tab">ID/전화번호</span>
          <span>일회용 번호</span>
          <span>QR코드</span>
        </div>

        <form onSubmit={handleLogin}>
          <input
            type="text"
            placeholder="아이디 또는 전화번호"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            className="login-input"
          />
          <input
            type="password"
            placeholder="비밀번호"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="login-input"
          />

          <div className="options">
            <label>
              <input type="checkbox" /> 로그인 상태 유지
            </label>
            <span className="ip-security" onClick={() => setIsIpSecurityOn(!isIpSecurityOn)}>
              IP보안 <b>{isIpSecurityOn ? "ON" : "OFF"}</b>
            </span>
          </div>

          <button type="submit" className="login-button">로그인</button>
        </form>

        <div className="links">
          <span onClick={() => navigate("/find-password")}>비밀번호 찾기</span> |
          <span>아이디 찾기</span> |
          <span onClick={() => navigate("/register")}>회원가입</span>
        </div>
      </div>
    </div>
  );
}

export default Login;

 

Dashboard.js

import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./Dashboard.css"; // 스타일 적용

function Dashboard() {
  const navigate = useNavigate();
  const [loggedInUser, setLoggedInUser] = useState(null);

  // ✅ 로그인 유지 확인 (JWT 검증)
  useEffect(() => {
    const user = JSON.parse(localStorage.getItem("loggedInUser"));
    if (!user) {
      alert("로그인이 필요합니다.");
      navigate("/login");
    } else {
      setLoggedInUser(user);
    }
  }, [navigate]);

  // ✅ 로그아웃 요청
  const handleLogout = async () => {
    await axios.post("http://localhost:5000/logout", {}, { withCredentials: true });
    alert("로그아웃 되었습니다.");
    navigate("/");
  };

  return (
    <div className="dashboard-container">

      {/* 네비게이션 바 */}
      <div className="nav-bar">
        <h1 className="nav-logo">NAVER</h1>
        <input type="text" className="search-bar" placeholder="검색어를 입력하세요" />
        <button className="search-button">검색</button>
      </div>

      {/* 메인 콘텐츠 */}
      <div className="main-content">

        {/* 사용자 정보 */}
        <div className="user-info">
          <h2>환영합니다, {loggedInUser?.username}님!</h2>
          <button onClick={handleLogout} className="logout-button">로그아웃</button>
        </div>

        {/* 블로그 이동 */}
        <div className="blog-section">
          <h3>내 블로그</h3>
          <p>내 블로그에 글을 작성하고 관리하세요.</p>
          <button onClick={() => navigate("/blog")} className="blog-button">블로그로 이동</button>
        </div>
      </div>
    </div>
  );
}

export default Dashboard;

 

로그인 및 회원가입 시, 사용자 정보를 서버로 전송하여 해당 기능을 실행했다.

로그인이 성공하면 JWT를 발급받고, 이를 httpOnly 쿠키에 저장하여 로그인 상태를 유지하도록 했다.

 

현재는 DB 없이 로컬 스토리지를 사용하는 방식이므로,
서버를 재시작하면 메모리에 저장된 사용자 정보가 초기화되고,

쿠키도 만료되면서 로그인 정보가 사라지는 문제가 발생한다.

이를 방지하려면 DB를 사용해 사용자 정보를 저장해야 하지만,
현재 개발 과정에서는 큰 문제가 없으므로, 우선 DB 없이 계속 진행할 계획이다.