본문 바로가기
Clone Coding

Node.js + React JWT 인증 구현하기

by zzuny-code 2026. 2. 25.
반응형

https://www.youtube.com/watch?v=VePjN663uwc

 

프로젝트 초기 세팅

  • 먼저 React 클라이언트를 생성
npx create-react-app@latest jwttest

 

서버 세팅

1. 서버 폴더 생성 및 패키지 설치

# 1. server 폴더 생성
mkdir server

# 2. 생성한 폴더로 이동
cd server

# 3. npm 초기화 (package.json 생성)
npm init -y

# 4. 필요한 패키지 설치
npm i express nodemon dotenv cors jsonwebtoken cookie-parser

 

각 패키지의 역할

  • express : Node.js 웹 서버 프레임워크
  • nodemon : 파일 변경 시 서버를 자동으로 재시작해주는 개발용 도구
  • dotenv : .env 파일에서 환경변수를 불러오는 라이브러리
  • cors : 클라이언트(React, 포트 3000)와 서버(포트 8123)가 다른 출처이기 때문에 교차 출처 요청을 허용하기 위해 필요
  • jsonwebtoken : JWT 토큰 생성 및 검증 라이브러리
  • cookie-parser : 요청에 담긴 쿠키를 파싱해서 req.cookies로 사용할 수 있게 해주는 미들웨어

 

 

2. server/package.json 확인 및 스크립트 수정

  • 설치가 완료되면 server/package.json의 dependencies가 아래와 같이 구성된다.
"dependencies": {
  "cookie-parser": "^1.4.7",
  "cors": "^2.8.6",
  "dotenv": "^17.3.1",
  "express": "^5.2.1",
  "jsonwebtoken": "^9.0.3",
  "nodemon": "^3.1.11"
}

 

  • 추가로 시작 명령어를 수정해준다.
  • dev 스크립트를 통해 개발 중에는 nodemon으로 서버를 실행하면 코드를 수정할 때마다 서버를 수동으로 재시작하지 않아도 된다.
"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js"
},

 

 

 

3. server/.env  환경 변수 설정

  • .env 파일은 포트번호, JWT 시크릿 키처럼 외부에 노출되면 안 되는 값들을 관리하는 파일이다. 추후 .gitignore에 반드시 추가해서 깃허브에 올라가지 않도록 주의해야 한다.
PORT=8123

 

 

 

4. server/index.js 서버 진입점 작성

  • cors 설정에서 credentials: true를 반드시 설정해야 클라이언트에서 쿠키를 포함한 요청을 보낼 수 있다.
  • React 쪽에서도 요청 시 withCredentials: true 옵션을 함께 설정해줘야 한다.
const express = require('express');
const dotenv = require('dotenv');
const cookieParser = require('cookie-parser');
const cors = require('cors');

const app = express();
dotenv.config();

const port = process.env.PORT || 3002;

app.use(cors({ // 교차 출처 허용
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST'],
    credentials: true
})); 
app.use(express.json()); // JSON 데이터 읽기용
app.use(cookieParser()); // 쿠키 읽기용

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

 

 

server  폴더 안에서 실행  >>      npm run dev

 

5. server/Database.js 테스트용 데이터베이스 생성

  • 간단하게 배열로 유저 데이터를 만들어 사용한다.
module.exports = [
    { id: 1, username: 'testuser1', email: 'testuser1@test.com', password: '1111' },
    { id: 2, username: 'testuser2', email: 'testuser2@test.com', password: '2222' },
    { id: 3, username: 'testuser3', email: 'testuser3@test.com', password: '3333' },
    { id: 4, username: 'testuser4', email: 'testuser4@test.com', password: '4444' },
]

실제 서비스에서는 비밀번호를 평문으로 저장하면 절대 안 된다.
bcrypt 같은 라이브러리로 해싱해서 저장하는 것이 원칙이다.

 

 

6. server/controller/index.js 컨트롤러 분리

  • 로직을 index.js에 전부 작성하면 코드가 복잡해지기 때문에, controller 폴더를 따로 생성해서 라우트별 핸들러 함수를 분리해 관리한다.
  • 이런 구조를 관심사의 분리(Separation of Concerns) 라고 하며, 코드 유지보수가 훨씬 편해진다.
const userDatabase = require('../Database');
const jwt = require('jsonwebtoken');

const login = (req, res) => {}
const accessToken = (req, res) => {}
const refreshToken = (req, res) => {}
const loginSuccess = (req, res) => {}
const logout = (req, res) => {}

module.exports = { login, accessToken, refreshToken, loginSuccess, logout }

 

함수역할

login 이메일/비밀번호 검증 후 Access Token, Refresh Token 발급
accessToken Access Token의 유효성을 검증
refreshToken Refresh Token을 이용해 새로운 Access Token 재발급
loginSuccess 로그인 성공 여부 확인 및 유저 정보 반환
logout 쿠키에서 토큰 삭제 처리

 

 

 

7. server/index.js 라우트 연결

  • server/index.js에 컨트롤러를 불러와서 각 경로에 연결
const {login, accessToken, refreshToken, loginSuccess, logout} = require('./controller');

app.post('/login', login)
app.get('/accesstoken', accessToken)
app.get('/refreshtoken', refreshToken)
app.get('/login/success', loginSuccess)
app.post('/logout', logout)

 

라우트 구성을 정리

POST /login 로그인 요청
GET /accesstoken Access Token 검증
GET /refreshtoken Refresh Token으로 토큰 재발급
GET /login/success 로그인 상태 확인
POST /logout 로그아웃

 

https://www.youtube.com/watch?v=NIKYWadDAbo

 

npm i axios
npm install --save-dev @types/react @types/react-dom

 

로그인 & JWT 토큰 발급 흐름 정리

  • 전체 흐름 : 사용자가 이메일/비밀번호를 입력 → 서버에서 검증 → Access Token + Refresh Token 발급 → 쿠키에 담아 응답

 

server/.env

  • .env 값 앞뒤 공백 주의
PORT=8123
ACCESS_SECRET=accesssecret
REFRESH_SECRET=refreshsecret

 

server/controller/index.js

  • 분리해뒀던 함수 중 login(이메일/비밀번호 검증 후 Access Token, Refresh Token 발급) 함수를 컨트롤 해준다.

1. 요청에서 사용자 정보 추출

  • 클라이언트가 POST 요청으로 보낸 body에서 이메일과 비밀번호를 꺼낸다.
const {email, password} = req.body;

 

2. 사용자 조회

⚠️ 강의에서 filter()를 사용했는데 filter()는 조건에 맞는 요소를 배열로 반환하기 때문에, userInfo는 항상 배열이다.
빈 배열 []도 JavaScript에서는 truthy로 평가되기 때문에, 사용자가 없어도 if(!userInfo)가 절대 실행되지 않는다.
올바르게 수정하려면 find()를 사용하는 게 적합하다.
// const userInfo = userDatabase.filter(item => {
//     return item.email === email 
// })

const userInfo = userDatabase.find(item => item.email === email);

 

3. Access Token 발급

구조 : jwt.sign(payload, secretKey, options)
  • payload: 토큰 안에 담길 사용자 정보 (민감한 정보는 절대 넣지마)
  • secretKey: .env에서 불러온 비밀 키
  • expiresIn '1m': 1분 후 만료 → Access Token은 짧게 설정하는 것이 보안상 중요
const accessToken = jwt.sign(
    { id: userInfo.id, username: userInfo.username, email: userInfo.email },
    process.env.ACCESS_SECRET,
    { expiresIn: '1m', issuer: 'About Tech' }
);

 

4. Refresh Token 발급

  • Access Token이 만료됐을 때 새로 발급받기 위한 토큰이다.
  • 유효기간이 길기 때문에 서버 DB에 저장해두고 검증하는 것이 일반적이다.
const refreshToken = jwt.sign(
    { id: userInfo.id, username: userInfo.username, email: userInfo.email},
    process.env.REFRESH_SECRET,
    { expiresIn: '24h', issuer: 'About Tech', }
);

 

5. 쿠키에 토큰 저장

  • httpOnly: true → JavaScript에서 쿠키 접근 불가 → XSS 공격 방어
  • secure: false → HTTPS가 아닌 환경(로컬)에서도 동작하도록 설정. 실제 배포 시엔 반드시 true로 변경해야 한다
res.cookie('accessToken', accessToken, { secure: false, httpOnly: true })
res.cookie('refreshToken', refreshToken, { secure: false, httpOnly: true })

 

 

6. 완성코드

const login = (req, res, next) => {

    // 1. 요청에서 사용자 정보 추출
    const {email, password} = req.body;

    // 2. 사용자 조회
    const userInfo = userDatabase.find(item => item.email === email);

    if(!userInfo){
        res.status(403).json("Not Authorized")
    }else{
        try{
        
            // access Token 발급
            const accessToken = jwt.sign({
                id: userInfo.id,
                username: userInfo.username,
                email: userInfo.email,
            }, process.env.ACCESS_SECRET,{
                expiresIn: '1m',
                issuer: 'About Tech',
            })

            // refresh Token 발급
            const refreshToken = jwt.sign({
                id: userInfo.id,
                username: userInfo.username,
                email: userInfo.email,
            }, process.env.REFRESH_SECRET,{
                expiresIn: '24h',
                issuer: 'About Tech',
            })

            // token 전송
            res.cookie('accessToken', accessToken, {
                secure: false,
                httpOnly: true,
            })
            res.cookie('refreshToken', refreshToken, {
                secure: false,
                httpOnly: true,
            })

            res.status(200).json("Login Success")

        }catch(errer){
            res.status(500).json("Server Error")
        }
    }


}

 

 

 

현재 비밀번호 검증은 생략된 상태다. 실제 서비스에서는 bcrypt로 해시된 비밀번호를 비교해야 한다.

const isPasswordValid = await bcrypt.compare(password, userInfo.password);

 


 

 

https://www.youtube.com/watch?v=rh8Ptrw8LpU

 

 

https://www.jwt.io/ 

 

JSON Web Tokens - jwt.io

JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).

www.jwt.io

 

 

Access Token으로 사용자 인증

로그인 시 발급받은 accessToken이 브라우저 쿠키에 저장되어 있다면,

이를 다시 서버로 보내 "현재 로그인한 사용자가 누구인지" 특정할 수 있다.

 

1. Client: 서버에 토큰 인증 요청 (App.js)

  • 먼저 클라이언트에서 서버의 /accessToken 엔드포인트로 GET 요청을 보낸다.
  • 이때 중요한 점 : withCredentials: true 옵션이 없으면 브라우저가 쿠키를 요청에 포함시키지 않아 서버에서 토큰을 읽을 수 없다.
const accessToken = () => {
    axios({
      method: 'GET',    
      url: 'http://localhost:8123/accessToken',
      withCredentials: true, // 쿠키 전송을 위한 필수 설정
    })
}

// ... JSX 부분
<a onClick={accessToken} className='App-link'>
  get Access Token
</a>

 

2. Server: 라우터 설정 (index.js)

  • 서버에서는 해당 경로로 들어오는 요청을 컨트롤러 함수와 연결
app.get('/accesstoken', accessToken)

 

3. Server: 컨트롤러 로직 작성 (controller/index.js)

  • 쿠키에 담긴 토큰을 꺼내 검증하고, 데이터베이스에서 사용자 정보를 조회하는 핵심 로직이다.
    • req.cookies로 httpOnly 쿠키에 저장된 토큰을 꺼내고, jwt.verify()로 위변조 여부와 만료 여부를 동시에 검증한다.
    • 검증 성공 시 payload에서 꺼낸 data.id로 DB를 한 번 더 조회하는데,
    • 이는 토큰 발급 이후 탈퇴하거나 권한이 변경된 사용자를 걸러낼 수 있어 안전하다.
const jwt = require('jsonwebtoken');

const accessToken = (req, res) => {
    try {
        // 1. 쿠키에서 accessToken 읽기
        const token = req.cookies.accessToken; 
        
        // 2. JWT를 이용해 토큰 검증 및 디코딩
        const data = jwt.verify(token, process.env.ACCESS_SECRET); 
        
        // 3. 토큰 내부의 id를 이용해 유저 정보 조회 (userDatabase는 임시 DB)
        const userData = userDatabase.find(item => item.id === data.id);

        if (!userData) {
            return res.status(404).json("사용자를 찾을 수 없습니다.");
        }

        // 4. 보안을 위해 비밀번호 제거 후 응답
        const { password, ...others } = userData;
        res.status(200).json(others);

    } catch (error) {
        // 토큰 만료나 조작 시 에러 발생
        res.status(500).json(error);
    }
}

 

💡 주요 포인트 요약

  • req.cookies: req.cookies를 사용하려면 server/index.js에 app.use(cookieParser())가 등록되어 있어야 한다.
  • jwt.verify(): 클라이언트가 보낸 토큰이 서버가 발행한 것이 맞는지, 유효기간이 지나지 않았는지 체크한다.
  • 보안(Security): 사용자 정보를 반환할 때 password와 같은 민감한 정보는 반드시 **Destructuring(구조 분해 할당)**을 통해 제외하고 보내야 한다.

 

토큰을 다 지우고 새로 로그인 후 get Access Token 버튼을 클릭하면 사용자 정보를 응답받게 된다.

response {"id":3,"username":"testuser3","email":"testuser3@test.com"}

 

 

같은 방법으로 refreshToken 도 작성해본다. 사용자 정보에 접근할 수 있는 accessToken의 갱신 용도다.

클라이언트에서 refreshToken을 실행하면 access token을 발급받아 사용자정보를 알 수 있게 한다.

 

1. Client: 서버에 토큰 refresh 요청 (App.js)

  • 먼저 클라이언트에서 서버의 /refreshToken 엔드포인트로 GET 요청을 보낸다.
const refreshToken = () => {
    axios({
      method: 'GET',
      url: 'http://localhost:8123/refreshToken',
      withCredentials: true,
    })
}

// ... JSX 부분
<a onClick={refreshToken} className='App-link'>
	get Refresh Token
</a>

 

2. Server: 라우터 설정 (index.js)

  • 서버에서는 해당 경로로 들어오는 요청을 컨트롤러 함수와 연결
app.get('/refreshtoken', refreshToken)

 

3. Server: 컨트롤러 로직 작성 (controller/index.js)

const refreshToken = (req, res) => {
    try{
        const token = req.cookies.refreshToken; // 쿠키에서 refreshToken 읽기
        const data = jwt.verify(token, process.env.REFRESH_SECRET); // refreshToken 검증
        
        const userData = userDatabase.find(item => item.id === data.id); // userDatabase에서 사용자 정보 찾기

        // access Token 발급
        const accessToken = jwt.sign({
            id: userData.id,
            username: userData.username,
            email: userData.email,
        }, process.env.ACCESS_SECRET,{
            expiresIn: '1m',
            issuer: 'About Tech',
        })

        // token 전송
        res.cookie('accessToken', accessToken, {
            secure: false,
            httpOnly: true,
        })

        res.status(200).json("Access Token Reissued")

    }catch(error){
        res.status(500).json('Token Error');
    }
}

 

로그인 상태 확인 & 로그아웃

loginSuccess (controller/index.js)

  • 앱이 처음 로드될 때 쿠키의 Access Token을 검증해서 로그인 상태인지 확인한다.
const loginSuccess = (req, res) => {
    try{
        const token = req.cookies.accessToken; // 쿠키에서 accessToken 읽기
        const data = jwt.verify(token, process.env.ACCESS_SECRET); // accessToken 검증
        
        const userData = userDatabase.find(item => item.id === data.id); // userDatabase에서 사용자 정보 찾기   

        res.status(200).json(userData); // 사용자 정보 응답        
        
    }catch(error){
        res.status(500).json('Token Error');
    }
}

 

 

logout (controller/index.js)

const logout = (req, res) => {
    try {
        res.cookie('accessToken', '');
        res.status(200).json("Logout Success");
    } catch (error) {
        res.status(500).json('Token Error');
    }
}

 

 

App.js에서 로그인 상태 관리

const [isLogin, setIsLogin] = useState(false);
const [user, setUser] = useState({});


useEffect(() => {
    try{
      axios({
        method: 'GET',
        url: 'http://localhost:8123/login/success',
        withCredentials: true,  
      }) 
      .then(result => {
        if(result.data){
          setIsLogin(true);
          setUser(result.data);
        }
      })
      .catch((error) => {
        console.log(error);
      })
    }catch(error){
      console.log(error);
    }
}, [])

// ... JSX 부분
{isLogin ? (
  <>
    <h3>{user.username} 님이 로그인했습니다.</h3>
    <button 
      onClick={logout}
      className='loginButton'
    >로그아웃</button>          
  </>
 ) :(
  <Login setUser={setUser} setIsLogin={setIsLogin} />
)}

 

 

로그인 컴포넌트 (src/components/Login.jsx)

const login = () => {    
    axios({
      url: "http://localhost:8123/login",
      method: "POST",
      withCredentials: true,
      data: {
        email: email,
        password: password,
      },
    }).then((result) => {
      if (result.status === 200) {
        window.open('/', '_self')
      }
    });
};

// ... JSX 부분
<button onClick={login} className="loginButton">Login</button>

 

testuser1으로 로그인 한 후 로그아웃하면 accesstoken 사라짐

 

전체 흐름 요약

  1. 로그인 → Access Token(1분) + Refresh Token(24h) 발급, httpOnly 쿠키에 저장
  2. 페이지 로드 시 /login/success로 Access Token 검증 → 로그인 상태 유지
  3. Access Token 만료 시 /refreshtoken으로 Refresh Token을 이용해 재발급
  4. 로그아웃 시 쿠키의 Access Token을 비워서 인증 해제

💡 로그아웃 이후에는 Refresh Token으로 Access Token을 갱신한 뒤 다시 로그인할 수 있다.

 
 
 
 
 
반응형