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 문자열을 유연하게 저장하고 데이터를 쉽게 관리할 수 있었습니다.

 

스프링부트에서 10달러를 결제하는 페이팔 결제 버튼을 만들어 보겠습니다.

 

준비물

  • 페이팔 회원가입, 계좌가 인증된 상태여야 합니다 (2~3일 소요)
  • 페이팔 API (샌드박스 ON)
  • 스프링부트 서버 로직
  • 웹 프론트엔드 리소스 (HTML/CSS/JS)

 

페이팔 API 설정 페이지입니다 (Sandbox 활성화)

 

Sandbox모드는 API 테스트와 개발을 할 수 있도록 제공되는 테스트 환경입니다.

샌드박스 모드에서 실제 결제는 이루어지 않으며 가상의 계좌를 설정할 수 있다고 합니다.


[ 결과물 스크린샷 ]

1. 페이팔 결제 버튼 클릭

 

2. 로그인

 

3. 10달러를 결제하는 페이팔 결제 화면

 

경고문 번역 : 한국 계정끼리는 결제할 수 없습니다.

 


:: 로직 설명 및 후기 ::

<p>결제를 시작하려면 아래 버튼을 클릭하세요.</p>
<form action="/api/paypal/pay" method="post">
    <!-- 필요한 값들을 서버로 전송 -->
    <input type="hidden" name="total" value="10.00">
    <input type="hidden" name="currency" value="USD">
    <button type="submit" class="button">Pay with PayPal</button>
</form>

 

이것은 페이팔 결제 api를 호출하는 index.html의 버튼입니다.

누르면 "/api/paypal/pay" 주소를 POST 메서드로 호출합니다.

 

@RestController
@RequestMapping("/api/paypal")
public class PayPalController {

    private final PayPalService payPalService;

    @Autowired
    public PayPalController(PayPalService payPalService) {
        this.payPalService = payPalService;
    }

    @PostMapping("/pay")
    public ResponseEntity<?> pay(@RequestParam("total") Double total,
                                 @RequestParam("currency") String currency) {
        try {
            Payment payment = payPalService.createPayment(
                    total,
                    currency,
                    "paypal",  // 결제 방법
                    "sale",    // 결제 intent
                    "결제 설명",
                    "http://localhost:8080/api/paypal/cancel",  // 취소 URL
                    "http://localhost:8080/api/paypal/success"  // 성공 URL
            );

            // PayPal 승인 URL로 리다이렉트
            for (Links link : payment.getLinks()) {
                if (link.getRel().equals("approval_url")) {
                    return ResponseEntity
                            .status(HttpStatus.FOUND)
                            .header(HttpHeaders.LOCATION, link.getHref())  // 리디렉션 헤더 설정
                            .build();
                }
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("PayPal 결제 승인 URL을 찾을 수 없습니다.");
        } catch (PayPalRESTException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("결제 중 오류 발생: " + e.getMessage());
        }
    }

    @GetMapping("/success")
    public ResponseEntity<?> successPayment(@RequestParam("paymentId") String paymentId,
                                            @RequestParam("PayerID") String payerId) {
        try {
            Payment payment = payPalService.executePayment(paymentId, payerId);
            if (payment.getState().equals("approved")) {
                return ResponseEntity.ok(Map.of("status", "success", "payment", payment));
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("결제가 승인되지 않았습니다.");
        } catch (PayPalRESTException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("결제 승인 중 오류 발생: " + e.getMessage());
        }
    }

    @GetMapping("/cancel")
    public ResponseEntity<?> cancelPayment() {
        return ResponseEntity.ok(Map.of("status", "cancelled"));
    }
}

 

여기는 PayPalController 컨트롤러 클래스입니다.

 

@PostMapping("pay")에서 요청을 받고 로직을 수행할 거에요.

PayPalService 클래스를 생성자 주입 방식으로 생성하여 pay 메서드에서 payPalService.createPayment() 로 사용했어요.

 

그러면 PayPalService 클래스의 createPayment() 메서드가 어떤 메서드인지 한번 봐야겠네요

 

@Service
public class PayPalService {

    @Value("${paypal.client.id}")
    private String clientId;

    @Value("${paypal.client.secret}")
    private String clientSecret;

    @Value("${paypal.mode}")
    private String mode;

    // 결제 생성
    public Payment createPayment(Double total, String currency, String method, String intent, String description, String cancelUrl, String successUrl) throws PayPalRESTException {

        // 금액 설정
        Amount amount = new Amount();
        amount.setCurrency(currency);
        amount.setTotal(String.format("%.2f", total));

        // 거래 정보 설정
        Transaction transaction = new Transaction();
        transaction.setDescription(description);
        transaction.setAmount(amount);

        // 결제 방법 설정
        Payer payer = new Payer();
        payer.setPaymentMethod(method.toString());

        // 결제 생성 요청 설정
        Payment payment = new Payment();
        payment.setIntent(intent.toString());
        payment.setPayer(payer);
        payment.setTransactions(Collections.singletonList(transaction));

        // 승인 및 취소 URL 설정
        RedirectUrls redirectUrls = new RedirectUrls();
        redirectUrls.setCancelUrl(cancelUrl);
        redirectUrls.setReturnUrl(successUrl);
        payment.setRedirectUrls(redirectUrls);

        // PayPal API 호출
        APIContext apiContext = new APIContext(clientId, clientSecret, mode);
        return payment.create(apiContext);
    }

    public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException {
        // 결제 요청
        Payment payment = new Payment();
        payment.setId(paymentId);

        // 결제 실행 요청
        PaymentExecution paymentExecution = new PaymentExecution();
        paymentExecution.setPayerId(payerId);

        // PayPal API 호출
        APIContext apiContext = new APIContext(clientId, clientSecret, mode);
        return payment.execute(apiContext, paymentExecution);
    }

}

 

클라이언트 아이디(API키)는 @Value("${example}")로 application.properties에 기재된 값을 가져와 사용합니다.

createPayment()는 7개의 파라미터를 전달받아 PayPal API를 호출하는 메서드네요.

 

한국에서 사용불가

 

샌드박스 모드여도 한국 계정간 결제할 수 없다는 창 때문에 결제 진행이 불가능했습니다.

원래대로 진행되었다면 PayPal 결제 승인 시 "/api/paypal/success" 로 리디렉션 됩니다.

@GetMapping("/success")
public ResponseEntity<?> successPayment(@RequestParam("paymentId") String paymentId,
                                        @RequestParam("PayerID") String payerId)

 

그러면 컨트롤러 클래스가 URL을 받아 successPayment 메서드에서 결제 성공 여부를 검증하고 결과를 반환합니다.

 

단순히 PayPal이 결제 승인했다고 바로 결과를 반환하지 않고, 서버에 저장된 데이터와 응답 결과가 일치하는지 한번 더 검증하는 절차를 거치게 됩니다.

이렇게 하면 결제 관련인만큼 절차는 복잡하지만 더 안전하게 처리할 수 있을 거에요.

 

그리고 마지막, 성공 시 응답으로 success.html을 반환하며 내용은 이렇습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>결제 성공</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 50px;
            text-align: center;
        }
        h1 {
            color: green;
        }
    </style>
</head>
<body>
<h1>결제가 성공적으로 완료되었습니다!</h1>
<p>감사합니다. 결제가 완료되었습니다.</p>
</body>
</html>

 

결제가 취소된 경우 cancel.html을 반환합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>결제 취소</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 50px;
            text-align: center;
        }
        h1 {
            color: red;
        }
    </style>
</head>
<body>
<h1>결제가 취소되었습니다.</h1>
<p>결제가 취소되었습니다. 다시 시도해주세요.</p>
</body>
</html>

[ 후기 ]

이렇게 10달러를 결제하는 페이팔 API를 호출하는 버튼을 만들어 보았습니다.

페이팔 자체는 한국에서 편리하게 사용할 수 있는 시스템은 아니라고 생각됩니다.

 

처음 결제 API 구현을 시도할 때는 글로벌적인 AWS 시스템을 사용해보는 김에 페이팔을 선택했었습니다.

지금 결제 API 구현을 시도한다면 아마 카카오페이를 가장 우선순위로 API를 구현 및 테스트 할 것입니다.

그리고 수수료는 높지만 카드사 통합 결제를 지원하는 "포트원" 이라는 국내 API도 있습니다. 여러 결제방법 중 요구사항에 적합하게 선택하면 될 것 같습니다.

 

결제 API에서 핵심은 단순히 외부 결제 API의 응답을 그대로 유저에게 반환하는 것이 아니라,

페이팔 API가 응답한 메시지 (성공/실패)를 바탕으로 페이팔에게 전달했던 파라미터와 현재 서버에 저장된 결제 정보를 대조하여 오류가 없는지 검증합니다.

여기서 결제정보의 무결성이 확인되면, 그때서야 유저에게 결제 결과를 반환합니다.

 

모든 정보가 정상적으로 전달되면 문제가 없겠지만, 데이터 누락이나 통신 오류가 발생할 수도 있기 때문에 검증 과정도 거치도록 로직을 작성합니다.

 

결제를 수행하는 API 로직을 이해하는 데 조금 더 많은 노력이 들었습니다.

데이터를 대조하고 검증하는 과정이 결제 API에서는 꼭 필요하다는 것을 알 수 있었습니다.

 

이번 게시물은 스프링부트로 카카오 지도 API를 사용하여 웹페이지에 지도를 띄우는 프로젝트입니다.

 

이전 게시물 : 카카오 로그인 버튼 구현하기 (스프링부트)

https://rexondex.tistory.com/27

 


 

현재 프로젝트 구조

 

https://apis.map.kakao.com/web/guide/

↑ ↑ ↑

(참고) 카카오 지도 Web API 가이드 링크

 


 

1) kakao-map.html (임의) 를 작성합니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Kakao 지도 시작하기</title>
</head>
<body>
<div id="map" style="width:500px;height:400px;"></div>
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=당신의 javascript api 키 입력"></script>
<script>
    var container = document.getElementById('map');
    var options = {
        center: new kakao.maps.LatLng(33.450701, 126.570667),
        level: 3
    };

    var map = new kakao.maps.Map(container, options);
</script>
</body>
</html>

 

카카오 지도 API 가이드에 전체 코드가 나와 있습니다

 

2) 자바스크립트 키 발급 / 사이트 도메인 설정

'앱 키' 에서 자바스크립트 키 확인
사이트 도메인에는 http://localhost:8080 을 추가했습니다

 

3) 스프링 컨트롤러 매핑

@Controller
public class HomeController {

    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/home")
    public String home() {
        return "home";
    }

    @GetMapping("/error")
    public String error() {
        return "error";
    }

    // 카카오 맵
    @GetMapping("/kakao-map")
    public String kakaoMap() {
        return "kakao-map";
    }
}

 

이전에 작성해두었던 3가지 겟매핑 아래에 카카오맵 html을 호출하도록 메소드를 하나 더 추가했습니다.

 


 

4) 브라우저에서 접속 및 확인

 

http://localhost:8080/kakao-map 에서 제대로 로드되었습니다.

 

단순히 띄우는 것 외에도 지도 라이브러리, 지도 URL 등 추가 기능을 지원합니다.

 

 

지도를 띄운 후에도 라이브러리나 URL을 사용하여 상용서비스에서 이미 카카오지도를 잘 활용하고 있는 것처럼,

링크 및 길찾기, 마커 등 다양한 활용이 가능합니다.

 

쇼핑몰 어플리케이션 이나 중고거래 플랫폼 등 활용가능한 API 자원 중 지도 API는 아직도 활발히 사용되고 있습니다.

물건을 주문 및 전송 할때나, 중고거래 위치를 확인하는 등 활용점은 매우 많다고 생각합니다.

 

카카오맵도 있지만 네이버 맵도 있습니다. 이전에 사용해 본 경험으로는 두 지도가 꽤 다른 매커니즘을 가지고 있어서 필요에 따라 선택하면 더 용이하게 어플리케이션을 개발할 수도 있겠습니다.

 

지도API를 사용하며 느낀점은 자바스크립트를 이용해 지도를 동적으로 조작하는 것도 가능하므로 자바스크립트를 자유로이 사용할 수 있는 역량도 중요하다고 생각했습니다.

 

자바스크립트 로직을 직접 설계하고 작성하는 능력을 키우면 웹표준을 다루는 데 있어서 훨씬 유리할 것 같았습니다.

 

현재 프로젝트 구조

 

카카오 API 문서에 따라 로그인버튼을 구현해보겠습니다.

 

1) index.html 을 작성합니다.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>index</title>
</head>
<body>
<h2>index.html</h2>
<button onclick="location.href='/login/kakao'">
    카카오 로그인
</button>
</body>
</html>

 

 

2)  로그인 이후 리다이렉트 될 home.html 을 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>home</title>
</head>
<body>
<h2>home.html</h2>
<h2>로그인 성공.</h2>
</body>
</html>

 

 

3) KakaoController 를 작성합니다.

@Controller
public class KakaoController {

	// 어플리케이션 프로퍼티스에서 값을 가져옴
    @Value("${KAKAO_CLIENT_ID}")
    private String KAKAO_CLIENT_ID;

	// 어플리케이션 프로퍼티스에서 값을 가져옴
    @Value("${KAKAO_SERVER_REDIRECT_URI}")
    private String KAKAO_SERVER_REDIRECT_URI;

    @GetMapping("/login/kakao")
    public void kakaoLoginRedirect(HttpServletResponse response) throws IOException {
        String clientId = KAKAO_CLIENT_ID; // 카카오 API 클라이언트 ID
        String redirectUri = KAKAO_SERVER_REDIRECT_URI; // 설정한 리디렉션 URI

        String url = "https://kauth.kakao.com/oauth/authorize?client_id=" + clientId
                + "&redirect_uri=" + redirectUri + "&response_type=code";

        response.sendRedirect(url); // 카카오로 리다이렉트
    }

    @GetMapping("/login/kakao/callback")
    public String kakaoCallback(@RequestParam("code") String code) throws JsonProcessingException {
        String clientId = KAKAO_CLIENT_ID;
        String redirectUri = KAKAO_SERVER_REDIRECT_URI;
        String tokenUrl = "https://kauth.kakao.com/oauth/token";

        // 파라미터 설정
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("redirect_uri", redirectUri);
        params.add("code", code);

        // RestTemplate으로 POST 요청
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

        // 카카오로 액세스 토큰 요청
        ResponseEntity<String> response = null;
        try {
            response = restTemplate.postForEntity(tokenUrl, request, String.class);
            if (response.getStatusCode() != HttpStatus.OK) {
                throw new RuntimeException("Failed to retrieve access token");
            }
        } catch (RestClientException e) {
            // 에러 처리 로직
            return "error";  // 에러 페이지로 리디렉션 또는 로그 기록
        }

        // JSON 파싱 후 액세스 토큰 추출 (Jackson 라이브러리 사용)
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(response.getBody());
        String accessToken = jsonNode.get("access_token") != null ? jsonNode.get("access_token").asText() : null;
        if (accessToken == null) {
            throw new RuntimeException("Access token not found in response");
        }

        // 액세스 토큰을 활용한 로직 구현 필요
        return "redirect:/home";
    }
}

 

 

컨트롤러에서 @Value("${KAKAO_CLIENT_ID}") 로 값을 가져오고 있으므로

application.properties에 이렇게 추가해 두었습니다.

저는 리다이렉트 주소를 /login/kakao/callback 으로 지정해뒀습니다.

디벨로퍼 페이지의 REDIRECT URI와 동일하게 설정되어야 합니다. (로컬환경에서 http://)

# REST API KEY
KAKAO_CLIENT_ID=카카오 디벨로퍼의 발급받은 REST API 키 값
# 카카오 디벨로퍼 페이지의 REDIRECT URI와 동일하게 설정
KAKAO_SERVER_REDIRECT_URI=http://localhost:8080/login/kakao/callback

카카오 디벨로퍼 리다이렉트 URI
컨트롤러 매핑
프로퍼티스

이렇게 모두 일치되어야 합니다!

 

 

4) template 하위에 있는 html 리소스들을 호출하는 HomeController를 작성합니다.

@Controller
public class HomeController {

    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/home")
    public String home() {
        return "home";
    }

    /* 에러페이지 매핑 필요한경우
    @GetMapping("/error")
    public String error() {
        return "error";
    }
    */

}

 

* 5) OAuth2와 함께 Spring Security를 적용했다면 다음 클래스도 추가합니다 *

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf
                        .disable()  // CSRF 보호 비활성화 (필요한 경우 활성화)
                )
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/index").permitAll()  // 경로는 인증 없이 접근 허용
                                .anyRequest().authenticated()  // 그 외 모든 요청은 인증 필요
                )
                .formLogin(withDefaults())  // 기본 로그인 폼 활성화
                .logout(withDefaults());  // 기본 로그아웃 활성화

        return http.build();
    }
}

 

localhost:8080에 접근했을 때 카카오 로그인을 호출하는 버튼이 있는 페이지 "index.html"으로의 접근을 제외하고

나머지 모든 경로에 대해 인증을 요구합니다.

 

// application.properties 에 작성해 두면 임시 시큐리티 로그인 정보가 됩니다.
spring.security.user.name=admin
spring.security.user.password=admin123

 

 

 

6) 브라우저에서 localhost:8080 에 접속해봅니다.

'/' 는 '/index'가 아니기때문에 시큐리티 로그인이 뜹니다

 

'localhost:8080/' 에서는 로그인 창이 뜨고 'localhost:8080/index'로 정확히 입력해야 로그인절차를 무시합니다.

'/' 주소에서 지금 서버는 이미 index.html 을 호출하도록 되어있지만 매핑되지 않은 경로이므로 '/login' 이 먼저 호출되었습니다. 로그인 후에 '/index'를 호출하여 index.html을 불러올 것입니다.

 

'/'도 .requestMatcher() 에 추가해두면 'localhost:8080/' 에서도 로그인절차를 무시할 수 있습니다.

모든 로그인절차를 무시하려면 .requestMatcher("/**").permitAll() 로 변경하면 됩니다.

// "/"와 "/index" 둘다 허용
.requestMatchers("/", "/index").permitAll()

// 루트 디렉토리의 바로 아래 경로만 허용 ( /example/test 는 허용하지 않음 )
.requestMatchers("/*").permitAll()

// 모든 경로(하위경로 포함) 허용
.requestMatchers("/**").permitAll()

 

localhost:8080/index 에서 로그인 버튼 확인

 

7) 로그인 버튼을 클릭해서 로그인 진행합니다.

카카오 로그인 화면

 

 

8) 로그인이 성공했다면 "/home"으로 이동합니다.

 

* 9) 로그인이 실패했을 경우에는 "/error"를 호출하도록 컨트롤러에 기입해놓았습니다. *

[처음으로 돌아가기]를 누르면 /index로 이동합니다.

 

/* 
	error.html 예시입니다.
*/

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>error</title>
</head>
<body>
<h2>로그인 과정에서 에러가 발생했습니다.</h2>
<button onclick="location.href='/index'">처음으로 돌아가기</button>
</body>
</html>

 

 


 

여기까지 카카오 로그인 버튼을 스프링부트 OAuth2를 통해 간단하게 구현해보았습니다.

카카오 로그인을 하면 사용자 로그인 정보를 사용자허락을 받고 파라미터로 받아올 수도 있습니다.

저는 이 부분은 현재 생략했습니다.

 

그리고 이렇게 작동이 된다는 것만 알게되었을 뿐 실제로는 신경쓸게 더 많았습니다.

토큰을 저장하고 안전하게 관리하는 법을 더 고민해봐야 할 것 같습니다.

 

// KakaoController 클래스에서...

        // 액세스 토큰을 활용한 로직 구현 필요
        return "redirect:/home";

 

라고 되어있는 부분이 있는데, 일단

(1)액세스 토큰을 받았으면 로그인이 완료된 것입니다.

(2)그래서 곧바로 return문을 통해 home으로 리다이렉트합니다.

 

이때 액세스 토큰을 활용한 추가 로직을 작성한다면

(1)액세스 토큰을 받고,

(2)이를 서버에서 안전하게 관리하기 위해 추가 로직을 구현합니다.

(3)그리고 return문을 통해 home으로 리다이렉트하게 됩니다.

 


 

이 프로젝트를 진행하면서 느낀 점)

 

프론트엔드 쪽에서 바로 API를 요청해 데이터를 받아올 수 있지만 서버가 필요한 이유는

API키, 보안키, 시크릿 키 등을 프론트쪽에서 처리하게 되면 정보가 드러나 위험하기 때문이라고 생각했습니다.

 

그래서 프론트엔드에서 API를 서버로 요청하고, 서버에서 클라이언트가 원하는 동작을 수행한 후,

클라이언트에 (가공된)응답 데이터or (가공된)응답 메시지를 전달하면 정보를 드러내지 않으면서

 

(1)클라이언트는 원하는 동작을 요청하고, (2)서버는 요청에 대해 응답만 했기 때문에 서버 밖에서는 서버가 어떤 로직을 거쳐서 응답메시지 또는 응답데이터를 만들어내는지 알 수 없습니다.

그러므로 어플리케이션에서 API키 등 보안과 관련된 절차와 로직을 숨길 수 있습니다.

이 프로젝트를 진행하면서 중요한 부분이라고 생각했습니다.

 

+ Recent posts