API

Node.js 서버로 카카오 로그인을 구현해보자 [MongoDB/JWT]

rexondex 2024. 10. 30. 11:39

Node.js의 Express 웹 애플리케이션 프레임워크를 사용하면 RESTful API, 웹사이트, 단순한 웹 서버를 빠르게 구축할 수 있습니다.

 

카카오 REST API를 사용하여 카카오 API를 활용하는 웹 서버를 빠르게 구축하기 위해 Node 서버를 선택하였습니다.

 

JWT 토큰은 API와 Node.js 서버 간의 인증 및 권한 부여 과정에서 중요한 역할을 하며, 사용자 정보를 안전하게 전달하고 서버의 부하를 줄이는 데 기여합니다.


JWT (JSON Web Token) 의 역할은 무엇인가요?

 

  • 헤더는 토큰의 유형과 서명 알고리즘 정보를 포함합니다.
  • 페이로드는 사용자 정보 및 기타 클레임을 포함합니다.
  • 헤더와 페이로드를 조합한 후, 비밀 키를 사용하여 서명을 생성합니다. 이 서명은 JWT의 무결성을 검증하는 데 사용됩니다. 비밀 키는 토큰에 포함되지 않으며, 서버 측에서만 유지됩니다.

서버는 생성한 JWT 토큰을 클라이언트에게 응답으로 보냅니다. 클라이언트는 이 토큰을 로컬 스토리지나 쿠키에 저장하여 이후 요청 시 서버에 포함시켜 사용합니다.

 

JWT 토큰 안에는 유효기간(exp)을 설정할 수 있습니다. 만약 엑세스 토큰이 만료되면, 클라이언트는 사용자에게 다시 로그인하도록 요청하거나 리프레시 토큰을 통해 새로운 액세스 토큰을 발급받을 수 있습니다.


MongoDB를 사용한 이유?

 

  • 카카오 로그인 API를 통해 사용자 정보를 수집하게 되면, MongoDB의 BSON 형식을 이용하여 유연하게 사용자 데이터를 저장할 수 있습니다. 사용자 정보는 JSON과 유사한 형태로 저장되므로, 다양한 속성을 손쉽게 관리할 수 있습니다.
  • 사용자가 증가함에 따라 데이터도 급격히 증가할 수 있습니다. MongoDB는 샤딩(sharding)을 지원하여 수평적 확장이 용이합니다. 이를 통해 필요할 때 서버를 추가하여 데이터베이스의 성능을 높일 수 있습니다.
  • 카카오 로그인 API에서 받은 사용자 정보는 다양한 필드를 가질 수 있습니다. MongoDB는 스키마가 필요 없거나 유연한 스키마를 지원하므로, 애플리케이션의 요구 사항이 변경되더라도 데이터베이스 구조를 쉽게 조정할 수 있습니다.

이러한 장점을 통해 Node.js와 MongoDB를 조합하여 비동기 처리에 강점을 가지는 개발 환경을 구성할 수 있습니다.

이는 개발자들이 빠르게 프로토타입을 만들고 개발할 수 있는 환경을 제공하여, 카카오 API기능을 빠르게 구현하고 테스트하는 데 유리합니다.


:: 시작하기 전에 ::

# 준비물 :

  • Node.js 설치 및 Node.js를 실행할 수 있는 콘솔 환경
  • MongoDB 설치 완료
  • Visual Studio Code 등의 코드 편집기

# 주의사항 :

  • Git을 사용한다면 ".gitignore"에 .env를 예외 등록하세요. ".env" 파일에는 JWT 비밀 키, 카카오 REST API키, 데이터베이스 주소가 포함됩니다.
// .gitignore 파일 작성
.env

1. Node.js와 필요한 패키지 설치

mkdir kakao-login-server // 프로젝트 폴더 생성
cd kakao-login-server // 생성한 폴더로 이동

// 터미널에 입력 (node.js와 npm이 설치되어있어야함)
npm init -y
npm install express mongoose axios dotenv jsonwebtoken cookie-parser

 

2. 환경 변수 설정 ( 아래쪽에 JWT 시크릿 키를 생성하는 법도 기재 해두었습니다 )

/* 루트 경로에서 .env 파일 생성, 내용 추가 */
KAKAO_CLIENT_ID=your_kakao_client_id // 카카오 REST API 키
REDIRECT_URI=your_redirect_uri // 카카오 리다이렉트 URI
MONGODB_URI=your_mongodb_uri // 몽고DB 주소
JWT_SECRET=your_jwt_secret // JWT 시크릿 키
PORT=3000 // 포트 : 3000 (기본값)

 

3. MongoDB 모델 생성

/* 
MongoDB 모델 생성
models/User.js : models 폴더에 현재 코드 User.js를 작성합니다.
*/

const mongoose = require('mongoose');

// 사용자 스키마 정의
const userSchema = new mongoose.Schema({
    kakaoId: {
        type: String,
        required: true,
        unique: true, // 카카오 ID는 고유해야 함
    },
    email: {
        type: String,
        required: false, // 이메일 필드는 선택 사항 (카카오 디벨로퍼 설정 필요)
    }, 
    profile: {
        type: String,
        required: false, // 프로필 이미지 필드는 선택 사항 (카카오 디벨로퍼 설정 필요)
    },
    createdAt: {
        type: Date,
        default: Date.now, // 기본 생성일
    },
});

// 사용자 모델 생성
const User = mongoose.model('User', userSchema);

module.exports = User;

 

4. 인증 서비스 작성

/*
인증 서비스 작성
services/authService.js : services폴더에 authService.js에 현재 코드를 작성합니다.
*/

const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');

// Mongoose 사용자 스키마 정의
const userSchema = new mongoose.Schema({
    kakaoId: { type: String, required: true, unique: true } // 카카오 고유 ID
}, { timestamps: true }); // 생성 및 수정 시간을 자동으로 기록

const User = mongoose.model('User', userSchema); // 사용자 모델 생성

// 사용자 정보를 DB에 저장하는 함수
async function saveUser(userInfo) {
    const kakaoId = userInfo.id;
    const existingUser = await User.findOne({ kakaoId });

    if (!existingUser) {
        // 새 사용자 저장
        const newUser = new User({
            kakaoId,
            email: userInfo.kakao_account.email
        });

        await newUser.save();
    }
    return kakaoId; // 사용자 ID 반환
}

// JWT 토큰 생성 함수
function generateToken(kakaoId) {
    const token = jwt.sign({ id: kakaoId }, process.env.JWT_SECRET, {
        expiresIn: '1h',
    });
    return token;
}

module.exports = {
    saveUser,
    generateToken,
    User, // User 모델도 내보내기
};

 

5. Express 서버 설정

/*
Express 서버 설정
루트 경로에 index.js를 작성합니다.
*/

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const axios = require('axios');
const cookieParser = require('cookie-parser');
const app = express();
const { saveUser, generateToken } = require('./services/authService'); // authService 임포트
const PORT = process.env.PORT || 3000;

app.use(cookieParser());

// 카카오 로그인 URL 생성 및 리다이렉션
app.get('/auth/kakao', (req, res) => {
    const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.KAKAO_CLIENT_ID}&redirect_uri=${process.env.REDIRECT_URI}&response_type=code&scope=account_email`;
    res.redirect(kakaoAuthUrl);
});

// MongoDB 연결
mongoose.connect(process.env.MONGODB_URI)
    .then(() => console.log('MongoDB connected'))
    .catch(err => console.error('MongoDB connection error:', err));


// 카카오 콜백 라우트
app.get('/auth/kakao/callback', async (req, res) => {
    const { code } = req.query;
    try {
        // 1. 인증 코드로 액세스 토큰 요청
        const tokenResponse = await axios.post('https://kauth.kakao.com/oauth/token', null, {
            params: {
                grant_type: 'authorization_code',
                client_id: process.env.KAKAO_CLIENT_ID,
                redirect_uri: process.env.REDIRECT_URI,
                code,
            },
        });

        const { access_token } = tokenResponse.data;

        // 2. 액세스 토큰으로 사용자 정보 요청
        const userResponse = await axios.get('https://kapi.kakao.com/v2/user/me', {
            headers: {
                Authorization: `Bearer ${access_token}`,
            },
        });

        const userInfo = userResponse.data;
        const kakaoId = await saveUser(userInfo); // 사용자 정보 DB에 저장
        const token = generateToken(kakaoId); // JWT 토큰 발급

        res.cookie('token', token); // 쿠키에 저장
        res.json({ token, userInfo }); // 사용자 정보 응답
    } catch (error) {
        console.error('카카오 로그인 실패:', error);
        res.status(500).json({ message: '카카오 로그인 중 오류가 발생했습니다.' });
    }
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

 

6. 서버 실행

// 서버 실행(터미널)
node index.js

 

7. 카카오 로그인 테스트

// 브라우저에서 카카오 로그인 시도
http://localhost:3000/auth/kakao

 


# .env의 JWT_SECRET= 항목에 들어갈 비밀 키 생성

# 생성된 키를 복사하여 사용합니다

/*
Node.js 명령어 :
64바이트의 랜덤 바이트를 생성하고, 16진수(hex)문자열로 변환하여 출력.
최종 128자리 16진수 문자열
*/
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

/*
OpenSSL 명령어 :
32바이트의 랜덤 바이트를 생성하고, 이를 16진수(hex) 문자열로 변환하여 출력.
최종 64자리 16진수 문자열
*/
openssl rand -hex 32

[ 실행 결과 ]

서버 실행 / 몽고DB 연결됨

 

브라우저에서 'http://localhost:3000/auth/kakao' 로 접속했을 때 

카카오 로그인 화면이 뜹니다. 로그인 진행

{
  "token": "abcd1234",
  "userInfo": {
    "id": 00000000,
    "connected_at": "2024-11-11T01:11:11Z",
    "kakao_account": {
      "has_email": true,
      "email_needs_agreement": false,
      "is_email_valid": true,
      "is_email_verified": true,
      "email": "abcd1234@email.com"
    }
  }
}

 

이런 형식의 JSON 문자열이 제대로 응답되었다면 성공입니다. 이 문자열은 방금 로그인한 유저정보입니다.

저는 파라미터로 email을 받도록 추가했기 때문에 email 관련 응답도 들어있습니다.

파라미터를 추가하려면 카카오 디벨로퍼에서도 추가할 요소에 체크해두어야 합니다.

 

MongoDB 연결이 제대로 되어있으면, 데이터가 [localhost:27017> myDatabase> users](예시)처럼 DB에 저장됩니다.

몽고DB Compass 화면

 


[ 후기 ]

Node.js서버와 MongoDB를 조합하고 JWT토큰을 사용하여 카카오 API 로그인을 구현해보았습니다.

 

Spring Boot에서는 애플리케이션의 처리 및 흐름을 설명하는 개념을 주로 "로직"이라는 용어를 사용해 비즈니스 로직을 강조했었는데,

Node.js에서는 "플로우"라는 용어를 사용해 비동기 처리 및 요청/응답 흐름을 강조하는 차이점이 있었습니다.

 

Node.js는 단일 스레드 비동기 처리 방식을 기반으로 설계되어 있어, "플로우"라는 개념이 중요해졌다고 합니다.

 

Node.js와 Express 프레임워크를 통해 빠르게 프로토타입을 만들 수 있었습니다.

MongoDB는 REST API의 응답 형식인 JSON 문자열을 유연하게 저장하고 데이터를 쉽게 관리할 수 있었습니다.