민프
[Nest.js] NCP를 이용하여 인증문자 SMS를 보내보자 (feat. Redis, Simple & Easy Notification Service) 본문
[Nest.js] NCP를 이용하여 인증문자 SMS를 보내보자 (feat. Redis, Simple & Easy Notification Service)
민프야 2025. 4. 7. 20:01이번 포스팅에서는 Nestjs에서 NCP - Simple & Easy Notification Service 을 이용해서 SMS을 보내보도록 하겠습니다.
사전 준비 목록
- NAVER CLOUD Plaform 계정 생성
- 결제 수단 등록 (마이페이지 -> 결제관리 -> 결제수단 -> 결제 수단 등록)
- 인증키 관리 메뉴로 이동 후 신규 API 인증키 생성 버튼을 눌러 키를 발급 받는다.
1. VPC - Simple & Easy Notification Service - 프로젝트 생성
https://console.ncloud.com/sens/project
위 링크에 접근하여 프로젝트 생성을 눌러서 프로젝트 생성을 해줍니다.
저는 SMS, Biz Message둘 다 사용할 예정이라서 둘 다 체크하고 만들겠습니다.
2. 발신번호 등록
아래 절차대로 발신번호를 등록하면 API를 사용할 준비가 완료된 것 입니다.
3. ENV 파일 정리
NCP_ACCESS_KEY='마이페이지 - 인증키 관리 - Access KEY ID'
NCP_SECRET_KEY='마이페이지 - 인증키 관리 - Secret KEY'
NCP_SERVICE_ID='SENS - 프로젝트 - 프로젝트 선택 시 SMS 서비스 ID'
NCP_SMS_FROM='등록된 번호'
4. 코드 작성
코드를 작성할 때 가이드에 맞게 작성해야하는데
signature 생성하는 것과 API 요청 시 헤더에 들어갈 내용들을 가이드에 맞게 넣어야 한다.
async sendSms(phoneNumber: string, message: string) {
const timestamp = Date.now().toString();
const method = 'POST';
const url = `/sms/v2/services/${process.env.NCP_SERVICE_ID}/messages`;
const baseUrl = 'https://sens.apigw.ntruss.com';
const accessKey = process.env.NCP_ACCESS_KEY || '';
const secretKey = process.env.NCP_SECRET_KEY || '';
const from = process.env.NCP_SMS_FROM || '';
/*
NPC - makeSignature 함수
- HMAC SHA256: crypto 모듈을 사용하여 HMAC SHA256 알고리즘으로 서명을 생성합니다.
- 서명 생성: HTTP 메서드, URL, 타임스탬프, 액세스 키를 조합하여 서명을 만듭니다.
- Base64 인코딩: 생성된 서명을 Base64로 인코딩하여 반환합니다.
NPC - axios 헤더 설정
- Content-Type: application/json; charset=utf-8로 설정하여 JSON 형식의 데이터를 전송합니다.
- x-ncp-apigw-timestamp: 현재 시간을 밀리초로 나타내어 헤더에 포함합니다.
- x-ncp-iam-access-key: 네이버 클라우드에서 발급받은 액세스 키를 사용합니다.
- x-ncp-apigw-signature-v2: makeSignature 함수로 생성한 서명을 사용합니다.
*/
const signature = this.makeSignature(
method,
url,
timestamp,
accessKey,
secretKey,
);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const response = await axios.post(
`${baseUrl}${url}`,
{
type: 'SMS',
from,
content: message,
messages: [{ to: phoneNumber }],
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
'x-ncp-apigw-timestamp': timestamp,
'x-ncp-iam-access-key': accessKey,
'x-ncp-apigw-signature-v2': signature,
},
},
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.log(`SMS sent to ${phoneNumber}: ${response.data}`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.error('Failed to send SMS', error.message);
throw new InternalServerErrorException(
'SMS 전송 중 오류가 발생했습니다.',
);
}
}
private makeSignature(
method: string,
url: string,
timestamp: string,
accessKey: string,
secretKey: string,
) {
const space = ' ';
const newLine = '\n';
const hmac = crypto.createHmac('sha256', secretKey);
hmac.update(
[method, space, url, newLine, timestamp, newLine, accessKey].join(''),
);
return hmac.digest('base64');
}
5. 결과
결과는 좋았습니다.
더 나아가서 서비스에 인증 기능을 구현하고 나면 누구나 한 번쯤 이런 고민을 하게 됩니다.
결과적으로 SMS는 정상적으로 전송되었지만,
서버는 누가 어떤 인증번호를 받았는지 기억하지 못한다면?
올바르게 인증번호를 입력해도 인증 자체가 무의미해지죠.
그래서
“특정 사용자가 받은 인증번호를 어떻게 서버에서 안전하고 확실하게 관리하고 인증까지 완료하는지”
이 문제를 해결하기 위해, 우리는 인증번호를 임시로 저장할 공간이 필요합니다.
바로 여기서 등장하는 것이 Redis(레디스)입니다.
간단하게 Redis에 대해서 설명드려보자면
Redis는 키-값(Key-Value) 저장소로,
데이터를 메모리(RAM)에 저장해 매우 빠르게 읽고 쓸 수 있는 인메모리 데이터베이스입니다.
쉽게 말해, 휘발성 메모장인데 속도는 엄청나게 빠르고, 필요한 순간 저장하고 꺼내 쓸 수 있는 임시 기억 창고라고 보시면 됩니다.
이 Redis를 이용해 아래와 같은 흐름으로 구현해보도록 하겠습니다.
1. 사용자가 전화번호 입력 → 인증번호 생성
2. 인증번호 Redis에 저장 (TTL 설정, 예: 3분)
3. SMS 발송 (NCP)
4. 사용자가 인증번호 입력 → 서버가 Redis에서 해당 인증번호 검증
5. 일치하면 회원가입 → DB 저장, JWT 발급
6. Redis
6-1. Redis 설치
brew install redis
npm install @nestjs-modules/ioredis ioredis
6-1. Redis 서버 실행 및 클라이언트 실행
redis-server
Redis 서버가 제대로 실행되고 있는지 확인하려면, 다른 터미널 창에서 Redis 클라이언트를 실행하여 연결할 수 있습니다:
redis-cli
6-2. Redis 동작 확인
redis-cli ping
PONG이 나오면 성공 입니다.
6-3. 코드 수정
수정 된 부분은 Redis에 번호에 대한 인증번호를 저장하고, 인증번호를 보내줄 떄 Redis에 저장 된 인증번호를 보내주는 코드가 추가/수정되었습니다. (유효기간은 300초로 설정하였습니다. )
// NCP SMS 전송
async sendSms(phoneNumber: string) {
const timestamp = Date.now().toString();
const method = 'POST';
const url = `/sms/v2/services/${process.env.NCP_SERVICE_ID}/messages`;
const baseUrl = 'https://sens.apigw.ntruss.com';
const accessKey = process.env.NCP_ACCESS_KEY || '';
const secretKey = process.env.NCP_SECRET_KEY || '';
const from = process.env.NCP_SMS_FROM || '';
// 인증번호 생성 (예: 6자리 랜덤 숫자)
const certificationNumber = Math.floor(
100000 + Math.random() * 900000,
).toString();
// 인증번호 저장
await this.storeCertificationNumber(phoneNumber, certificationNumber);
// Redis에서 인증번호 조회
const storedCertificationNumber = await this.redisClient.get(phoneNumber);
// 메시지에 인증번호 포함
const fullMessage = `[회원가입 인증번호]: ${storedCertificationNumber}`;
/*
NPC - makeSignature 함수
- HMAC SHA256: crypto 모듈을 사용하여 HMAC SHA256 알고리즘으로 서명을 생성합니다.
- 서명 생성: HTTP 메서드, URL, 타임스탬프, 액세스 키를 조합하여 서명을 만듭니다.
- Base64 인코딩: 생성된 서명을 Base64로 인코딩하여 반환합니다.
NPC - axios 헤더 설정
- Content-Type: application/json; charset=utf-8로 설정하여 JSON 형식의 데이터를 전송합니다.
- x-ncp-apigw-timestamp: 현재 시간을 밀리초로 나타내어 헤더에 포함합니다.
- x-ncp-iam-access-key: 네이버 클라우드에서 발급받은 액세스 키를 사용합니다.
- x-ncp-apigw-signature-v2: makeSignature 함수로 생성한 서명을 사용합니다.
*/
const signature = this.makeSignature(
method,
url,
timestamp,
accessKey,
secretKey,
);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const response = await axios.post(
`${baseUrl}${url}`,
{
type: 'SMS',
from,
content: fullMessage,
messages: [{ to: phoneNumber }],
},
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
'x-ncp-apigw-timestamp': timestamp,
'x-ncp-iam-access-key': accessKey,
'x-ncp-apigw-signature-v2': signature,
},
},
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.log(`SMS sent to ${phoneNumber}: ${response.data}`);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logger.error('Failed to send SMS', error.message);
throw new InternalServerErrorException(
'SMS 전송 중 오류가 발생했습니다.',
);
}
}
// 전화번호에 대한 인증 번호 저장 메서드
async storeCertificationNumber(
phoneNumber: string,
certificationNumber: string,
) {
const expirationTime = 300; // 인증번호 유효기간 (초)
await this.redisClient.set(
phoneNumber,
certificationNumber,
'EX',
expirationTime,
);
}
6-4. 결과
Redis에 저장된 번호가 잘 나온 것 을 확인하실 수 있습니다.