NestJS와 Redis로 5분 만에 안전한 SMS 인증(OTP) 시스템 구축하기

2026년 4월 10일3분 소요

NESTJS-REDIS-OTP

SMS 인증 연동, 복잡한 서류 제출 때문에 막막하셨나요?

토이 프로젝트나 스타트업 MVP를 개발하며 서비스에 'SMS 인증' 기능을 붙이려다 답답함을 느낀 적 있으신가요? 기존 문자 API 서비스들은 가입 단계부터 사업자등록증, 이용증명원 같은 복잡한 서류를 요구합니다. 게다가 발신번호 사전등록 절차까지 거치려면 실제 코드를 짜기도 전에 며칠의 시간을 허비하게 됩니다.

이 글에서는 서류 제출 없이 가입 후 5분 만에 즉시 사용할 수 있는 EasyAuth(이지어스) API와 NestJS, Redis를 활용해 안전하고 확장성 있는 자체 SMS 인증(OTP) 시스템을 구축하는 방법을 알아봅니다.


Solution Overview: 우리가 만들 시스템

이 튜토리얼에서는 다음 흐름을 가진 인증 시스템을 만듭니다:

  1. 사용자가 휴대폰 번호를 입력해 인증을 요청합니다.
  2. NestJS 서버에서 6자리 난수(OTP)를 생성하고 Redis에 3분(180초) 만료 설정(TTL)과 함께 저장합니다.
  3. EasyAuth API(POST /send)를 호출하여 사용자에게 SMS를 즉시 발송합니다.
  4. 사용자가 입력한 인증번호를 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

  1. SMS 어뷰징(Bombing) 방지
    악의적인 사용자가 API를 반복 호출해 문자 발송 비용을 폭탄 맞게 하는 것을 방지해야 합니다. Redis를 사용해 동일한 휴대폰 번호나 IP에 대해 **"1분 내 1회 요청 가능"**과 같은 Rate Limiting 로직을 추가하는 것을 강력히 권장합니다.

  2. 보안을 고려한 OTP 삭제
    위 코드처럼 인증에 성공한 OTP는 this.redisClient.del()을 통해 즉시 삭제해야 재사용(Replay Attack)을 막을 수 있습니다.


결론

NestJS의 깔끔한 구조와 Redis의 만료(TTL) 기능을 결합하면 훌륭한 자체 OTP 시스템을 만들 수 있습니다.

무엇보다 1인 개발자나 스타트업의 MVP 단계에서 가장 큰 허들이었던 복잡한 서류 작업과 통신사 심사를 건너뛰세요. 기존 서비스 대비 절반 수준인 건당 15~25원의 합리적인 가격, 가입 시 제공되는 10건의 무료 테스트 혜택까지! 서류 없이 5분 만에 시작할 수 있는 **[EasyAuth(이지어스)]**로 개발에만 집중해 보세요.

SMS 인증을 쉽게 시작하세요

서류 없이 가입 즉시 API Key를 발급받고 바로 시작할 수 있습니다.
건당 25원, 가입 시 10건 무료!