NestJS와 Redis로 5분 만에 안전한 SMS 인증(OTP) 시스템 구축하기
![]()
SMS 인증 연동, 복잡한 서류 제출 때문에 막막하셨나요?
토이 프로젝트나 스타트업 MVP를 개발하며 서비스에 'SMS 인증' 기능을 붙이려다 답답함을 느낀 적 있으신가요? 기존 문자 API 서비스들은 가입 단계부터 사업자등록증, 이용증명원 같은 복잡한 서류를 요구합니다. 게다가 발신번호 사전등록 절차까지 거치려면 실제 코드를 짜기도 전에 며칠의 시간을 허비하게 됩니다.
이 글에서는 서류 제출 없이 가입 후 5분 만에 즉시 사용할 수 있는 EasyAuth(이지어스) API와 NestJS, Redis를 활용해 안전하고 확장성 있는 자체 SMS 인증(OTP) 시스템을 구축하는 방법을 알아봅니다.
Solution Overview: 우리가 만들 시스템
이 튜토리얼에서는 다음 흐름을 가진 인증 시스템을 만듭니다:
- 사용자가 휴대폰 번호를 입력해 인증을 요청합니다.
- NestJS 서버에서 6자리 난수(OTP)를 생성하고 Redis에 3분(180초) 만료 설정(TTL)과 함께 저장합니다.
- EasyAuth API(
POST /send)를 호출하여 사용자에게 SMS를 즉시 발송합니다. - 사용자가 입력한 인증번호를 Redis에 저장된 값과 검증(Verify)합니다.
> 💡 잠깐! Redis 구축이 귀찮다면?
> 만약 자체 Redis 서버를 띄우기 부담스럽다면, EasyAuth에서 기본 제공하는 POST /verify 엔드포인트를 사용해 보세요! 개발자는 별도의 저장소 없이도 EasyAuth가 제공하는 API 두 개(send, verify)만으로 상태 없는(Stateless) 인증 시스템을 완성할 수 있습니다.
Step-by-Step Implementation
1. 패키지 설치
Redis와 통신하기 위해 ioredis를, EasyAuth API 호출을 위해 axios를 설치합니다.
npm install ioredis axios
2. 전체 완성 코드 (Complete Code)
실제 프로덕션 환경에서도 바로 응용할 수 있는 AuthService 구현 코드입니다.
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import Redis from 'ioredis';
import axios from 'axios';
@Injectable()
export class AuthService {
private redisClient: Redis;
private readonly EASYAUTH_API_URL = 'https://api.easyauth.co.kr';
private readonly API_KEY = 'your_easyauth_api_key_here'; // 환경변수로 관리하세요
constructor() {
// 기본 localhost:6379 연결. 운영 환경에 맞게 수정하세요.
this.redisClient = new Redis();
}
async sendOtp(phoneNumber: string) {
// 1. 6자리 랜덤 인증번호 생성
const otp = Math.floor(100000 + Math.random() * 900000).toString();
try {
// 2. Redis에 저장 (TTL: 3분 = 180초)
await this.redisClient.set(`otp:${phoneNumber}`, otp, 'EX', 180);
// 3. EasyAuth를 통한 SMS 발송
// 서류 제출이나 사전 발신번호 등록 없이 즉시 발송 가능합니다!
await axios.post(`${this.EASYAUTH_API_URL}/send`, {
to: phoneNumber,
text: `[내서비스] 인증번호는 [${otp}] 입니다.`
}, {
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
}
});
return { message: '인증번호가 발송되었습니다.' };
} catch (error) {
throw new HttpException('SMS 발송에 실패했습니다.', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async verifyOtp(phoneNumber: string, code: string) {
// 1. Redis에서 인증번호 조회
const storedOtp = await this.redisClient.get(`otp:${phoneNumber}`);
if (!storedOtp) {
throw new HttpException('인증번호가 만료되었거나 존재하지 않습니다.', HttpStatus.BAD_REQUEST);
}
// 2. 인증번호 일치 여부 확인
if (storedOtp !== code) {
throw new HttpException('인증번호가 일치하지 않습니다.', HttpStatus.BAD_REQUEST);
}
// 3. 검증 성공 시 재사용 방지를 위해 즉시 삭제
await this.redisClient.del(`otp:${phoneNumber}`);
return { message: '인증이 완료되었습니다.' };
}
}
3. Controller 연동
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('send')
async send(@Body('phoneNumber') phoneNumber: string) {
return this.authService.sendOtp(phoneNumber);
}
@Post('verify')
async verify(
@Body('phoneNumber') phoneNumber: string,
@Body('code') code: string
) {
return this.authService.verifyOtp(phoneNumber, code);
}
}
Tips & Best Practices
-
SMS 어뷰징(Bombing) 방지
악의적인 사용자가 API를 반복 호출해 문자 발송 비용을 폭탄 맞게 하는 것을 방지해야 합니다. Redis를 사용해 동일한 휴대폰 번호나 IP에 대해 **"1분 내 1회 요청 가능"**과 같은 Rate Limiting 로직을 추가하는 것을 강력히 권장합니다. -
보안을 고려한 OTP 삭제
위 코드처럼 인증에 성공한 OTP는this.redisClient.del()을 통해 즉시 삭제해야 재사용(Replay Attack)을 막을 수 있습니다.
결론
NestJS의 깔끔한 구조와 Redis의 만료(TTL) 기능을 결합하면 훌륭한 자체 OTP 시스템을 만들 수 있습니다.
무엇보다 1인 개발자나 스타트업의 MVP 단계에서 가장 큰 허들이었던 복잡한 서류 작업과 통신사 심사를 건너뛰세요. 기존 서비스 대비 절반 수준인 건당 15~25원의 합리적인 가격, 가입 시 제공되는 10건의 무료 테스트 혜택까지! 서류 없이 5분 만에 시작할 수 있는 **[EasyAuth(이지어스)]**로 개발에만 집중해 보세요.