휴대폰 본인인증 완벽 구현 가이드
Angular + Ionic + Capacitor 프로젝트에서 아임포트(iamport)를 사용해 휴대폰 본인인증을 구현하는 방법을 실제 프로덕션 코드와 함께 설명한다. 웹과 네이티브 환경 분기 처리, Firebase Functions를 통한 검증, 개인정보 보호까지 모든 것을 다룬다.
목차
1. 아임포트란?
아임포트(iamport)는 한국의 대표적인 결제 및 본인인증 서비스다. 휴대폰 본인인증, 신용카드 결제, 계좌이체 등을 간편하게 구현할 수 있다.
1.1 본인인증 방식
- 휴대폰 본인인증: SMS 인증번호를 통한 인증
- 아이핀 본인인증: 공인인증서를 통한 인증
- 간편인증: 통신사 앱을 통한 인증
2. 아임포트 가입 및 설정
아임포트에 가입하고 본인인증을 사용하기 위한 설정을 진행한다.
2.1 아임포트 가입
- 1아임포트 홈페이지 접속
- 2회원가입 진행
- 3본인인증 서비스 신청
2.2 가맹점 식별 코드 확인
아임포트 대시보드에서 가맹점 식별 코드(USER_CODE)를 확인한다:
- 대시보드 → 시스템 설정 → 가맹점 식별코드
- 코드 복사 (예:
imp61314703)
2.3 REST API 키 발급
서버에서 인증 정보를 검증하려면 REST API 키가 필요하다:
- 대시보드 → 시스템 설정 → REST API 키
- API Key와 Secret Key 발급
3. 필요한 패키지 설치
아임포트를 사용하기 위해 필요한 패키지를 설치한다.
3.1 Capacitor 플러그인 설치
네이티브 앱에서 사용하려면 Capacitor 플러그인이 필요하다:
npm install iamport-capacitor
npx cap sync
3.2 웹용 스크립트 추가
웹 환경에서는 아임포트 JavaScript SDK를 사용한다. index.html에 추가:
<script src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
4. 인증 서비스 구현
본인인증을 처리하는 서비스를 구현한다. 웹과 네이티브 환경을 분기해서 처리한다.
4.1 CertificationService 기본 구조
import { Injectable, NgZone } from '@angular/core';
import { FunctionsService } from './functions.service';
import { CertificationData, CertificationOptions, IMP, Response } from 'iamport-capacitor';
import { Platform } from '@ionic/angular';
const USER_CODE: string = '[USER_CODE]'; // 가맹점 식별 코드
@Injectable({
providedIn: 'root',
})
export class CertificationService {
constructor(
private functions: FunctionsService,
private ngZone: NgZone,
private platform: Platform
) {}
/**
* 휴대폰 본인인증 처리
* @param phone 휴대폰 번호
* @param name 이름 (선택사항)
* @returns 인증 결과 데이터
*/
requestCertification(phone: string, name?: string): Promise<any> {
return new Promise((resolve, reject) => {
if (this.platform.is('cordova') || this.platform.is('capacitor')) {
this.capacitorCertification(phone, name).then(resolve).catch(reject);
} else {
this.webCertification(phone, name).then(resolve).catch(reject);
}
});
}
}
4.2 네이티브 앱 인증 구현
capacitorCertification(phone: string, name?: string): Promise<any> {
return new Promise((resolve, reject) => {
const imp = new IMP();
// 고유한 주문번호 생성
const today = new Date();
const hours = today.getHours();
const minutes = today.getMinutes();
const seconds = today.getSeconds();
const milliseconds = today.getMilliseconds();
const merchant_uid = 'mid_' + hours + minutes + seconds + milliseconds;
const certification: CertificationData = {
merchant_uid,
company: '[companyName]',
name: name,
phone: phone,
};
const params: CertificationOptions = {
userCode: USER_CODE,
data: certification,
callback: (rsp: Response) => {
this.ngZone.run(async () => {
if (rsp.success === 'true') {
// Firebase Functions를 통해 인증 정보 검증
const result: any = await this.functions.getCertificationData(
rsp.imp_uid
);
if (!result) {
reject(false);
return;
}
resolve(result.data);
} else {
resolve(false);
}
});
},
callbackOnBack: () => {
resolve(false);
},
};
imp
.certification(params)
.then(async (response) => {
if (response) {
// Android는 URL에서 imp_uid 추출
if (this.platform.is('android')) {
const urlParams = new URLSearchParams(
new URL(response['url']).search
);
const imp_uid = urlParams.get('imp_uid');
const result: any = await this.functions.getCertificationData(
imp_uid
);
if (!result) {
reject(false);
return;
}
resolve(result.data);
}
// iOS는 직접 응답 받음
if (this.platform.is('ios')) {
resolve(response);
}
} else {
resolve(false);
}
})
.catch((error) => {
console.error('인증 중 오류:', error);
reject(false);
});
});
}
4.3 웹 환경 인증 구현
private webCertification(phone: string, name?: string): Promise<any> {
return new Promise((resolve, reject) => {
const IMPWeb = (window as any).IMP;
// 고유한 주문번호 생성
const today = new Date();
const hours = today.getHours();
const minutes = today.getMinutes();
const seconds = today.getSeconds();
const milliseconds = today.getMilliseconds();
const merchant_uid = 'mid_' + hours + minutes + seconds + milliseconds;
if (typeof IMPWeb !== 'undefined') {
IMPWeb.init(USER_CODE);
const data: CertificationData = {
merchant_uid,
company: '[companyName]',
name: name,
phone: phone,
};
IMPWeb.certification(data, (rsp: any) => {
this.ngZone.run(async () => {
if (rsp.success) {
// Firebase Functions를 통해 인증 정보 검증
const result: any = await this.functions.getCertificationData(
rsp.imp_uid
);
if (!result) {
reject(false);
return;
}
resolve(result.data);
} else {
resolve(false);
}
});
});
} else {
reject('IMP is not defined');
}
});
}
5. Firebase Functions 검증
클라이언트에서 받은 imp_uid를 서버에서 검증해야 한다. 보안을 위해 Firebase Functions를 사용한다.
5.1 FunctionsService 구현
import { Injectable, inject } from '@angular/core';
import { httpsCallable, Functions } from '@angular/fire/functions';
@Injectable({
providedIn: 'root',
})
export class FunctionsService {
private functions: Functions = inject(Functions);
constructor() {
this.functions.region = 'asia-northeast3';
}
/**
* 인증 정보 검증
* @param imp_uid 아임포트 인증 고유번호
* @returns 인증 정보
*/
async getCertificationData(imp_uid: string): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
const result = await httpsCallable(
this.functions,
'certifications'
)({ imp_uid });
resolve(result.data);
} catch (error) {
resolve(false);
}
});
}
}
5.2 Firebase Functions 구현
functions/index.js에 인증 검증 함수를 추가:
const functions = require('firebase-functions');
const axios = require('axios');
exports.certifications = functions
.region('asia-northeast3')
.https.onCall(async (data, context) => {
try {
const { imp_uid } = data; // 클라이언트에서 전달받은 imp_uid
// 1. 아임포트 인증 토큰 발급
const getToken = await axios({
url: 'https://api.iamport.kr/users/getToken',
method: 'post',
headers: { 'Content-Type': 'application/json' },
data: {
imp_key: '[REST_API_KEY]', // REST API 키
imp_secret: '[REST_API_SECRET]', // REST API Secret
},
});
const { access_token } = getToken.data.response;
// 2. imp_uid로 인증 정보 조회
const getCertifications = await axios({
url: 'https://api.iamport.kr/certifications/' + imp_uid,
method: 'get',
headers: { Authorization: 'Bearer ' + access_token },
});
const certificationsInfo = getCertifications.data.response;
// 3. 인증 정보 반환
return { success: true, data: certificationsInfo };
} catch (error) {
throw new functions.https.HttpsError('internal', error.message);
}
});
6. 컴포넌트에서 사용하기
회원가입이나 프로필 수정 페이지에서 본인인증을 사용하는 방법을 설명한다.
6.1 휴대폰 번호 유효성 검사
인증 전에 휴대폰 번호 형식을 검증한다:
// ValidationService
checkPhoneFormat(phone: string): boolean {
const phoneReg = /^.*(?=^.{10,11}$)(?=.*[0-9]).*$/;
return phoneReg.test(phone);
}
6.2 본인인증 버튼 클릭 처리
import { Component } from '@angular/core';
import { CertificationService } from './core/services/certification.service';
import { ValidationService } from './core/services/validation.service';
import { LoadingService } from './core/services/loading.service';
import { AlertService } from './core/services/alert.service';
@Component({
selector: 'app-join',
templateUrl: './join.page.html',
})
export class JoinPage {
member = {
phone: '',
name: '',
gender: '',
residentRegistration: '',
};
checkCertification = false; // 본인인증 성공 여부
constructor(
private certificationService: CertificationService,
private validationService: ValidationService,
private loadingService: LoadingService,
private alertService: AlertService
) {}
/**
* 본인인증 버튼 클릭 시
*/
async goConfirm() {
// 휴대폰 번호 유효성 검사
if (!this.validationService.checkPhoneFormat(this.member.phone)) {
await this.alertService.okBtn('올바른 휴대폰 번호를 입력해주세요.');
return;
}
await this.loadingService.load();
try {
// 본인인증 실행
const result = await this.certificationService.requestCertification(
this.member.phone,
this.member.name
);
// 인증 성공 시
if (result) {
this.checkCertification = true;
this.member.name = result['name'];
this.member.gender = result['gender'] === 'female' ? 'female' : 'male';
// 주민번호 마스킹 처리
this.member.residentRegistration =
this.certificationService.formatBirthDay(
result['birthday'],
result['gender']
);
await this.alertService.okBtn('본인인증 성공하였습니다.');
} else {
// 인증 실패 시
this.checkCertification = false;
await this.alertService.okBtn('본인인증 실패하였습니다.');
}
} catch (error) {
console.error('인증 오류:', error);
await this.alertService.okBtn('인증 중 오류가 발생했습니다.');
} finally {
this.loadingService.hide();
}
}
}
6.3 템플릿 예제
<div class="contain">
<ion-label>
휴대폰번호
<div class="required"></div>
</ion-label>
<ion-item class="input-wrap">
<ion-input
[(ngModel)]="member.phone"
[disabled]="checkCertification"
type="tel"
maxlength="11"
placeholder="(-) 없이 휴대폰번호를 입력해주세요."
></ion-input>
<ion-button
(click)="goConfirm()"
[disabled]="!validationService.checkPhoneFormat(member.phone) || checkCertification"
fill="default"
class="default-btn-border"
>
{{ checkCertification ? '인증완료' : '본인인증' }}
</ion-button>
</ion-item>
<p
*ngIf="member.phone && !validationService.checkPhoneFormat(member.phone)"
class="validation"
>
휴대폰번호는 10~11글자 이내로 입력해주세요.
</p>
</div>
7. 개인정보 보호 처리
주민등록번호는 법적으로 저장할 수 없으므로, 생년월일과 성별 정보만 마스킹해서 저장한다.
7.1 주민번호 마스킹 함수
/**
* 생년월일과 성별을 주민번호 형식으로 변환 (마스킹)
* @param birthday 생년월일 (YYYY-MM-DD)
* @param gender 성별 (male/female)
* @returns 마스킹된 주민번호 (YYMMDDG******)
*/
formatBirthDay(birthday: string, gender: string): string {
// 생년월일에서 연도, 월, 일 추출
const [year, month, day] = birthday.split('-');
const genderCode = this.getGenderCode(Number(year), gender);
// 월과 일을 두 자리로 포맷팅
const formattedMonth = ('0' + month).slice(-2);
const formattedDay = ('0' + day).slice(-2);
// 연도 두 자리 + 월 두 자리 + 일 두 자리
const sliceBirthday = year.slice(-2) + formattedMonth + formattedDay;
// 주민번호 앞 6자리 + 성별 코드 + 마스킹 처리
return sliceBirthday + genderCode + '******';
}
/**
* 성별과 출생연도를 통한 주민번호 뒷자리 첫 번째 숫자 결정
* @param year 출생연도
* @param gender 성별
* @returns 성별 코드 (0-9)
*/
getGenderCode(year: number, gender: string): string {
if (year < 1900) {
// 1900년 이전
return gender === 'female' ? '9' : '0';
} else if (year < 2000) {
// 1900년대
return gender === 'female' ? '2' : '1';
} else {
// 2000년대 이후
return gender === 'female' ? '4' : '3';
}
}
- 주민등록번호 전체를 저장하는 것은 불법이다
- 생년월일과 성별만 마스킹해서 저장해야 한다
- 개인정보보호법을 준수해야 한다
8. 실전 팁과 주의사항
8.1 자주 하는 실수
- REST API 키 노출: 클라이언트 코드에 REST API 키를 넣으면 안 된다
- imp_uid 미검증: 클라이언트에서 받은 imp_uid를 서버에서 반드시 검증해야 한다
- 주민번호 저장: 주민등록번호 전체를 저장하면 불법이다
- 환경 분기 누락: 웹과 네이티브 환경을 제대로 분기하지 않으면 오류 발생
8.2 보안 권장사항
- 서버 검증 필수: 모든 인증 정보는 서버에서 검증한다
- HTTPS 사용: 모든 통신은 HTTPS로 암호화한다
- 토큰 만료 처리: 인증 토큰은 일정 시간 후 만료되도록 처리한다
- 로그 관리: 개인정보는 로그에 남기지 않는다
8.3 테스트 방법
- 1아임포트 테스트 모드 사용
- 2테스트용 휴대폰 번호로 인증 진행
- 3인증 성공/실패 케이스 모두 테스트
- 4웹과 네이티브 환경 모두 테스트
8.4 에러 처리
인증 실패 시 사용자에게 명확한 메시지를 표시한다:
try {
const result = await this.certificationService.requestCertification(
this.member.phone,
this.member.name
);
if (result) {
// 성공 처리
this.checkCertification = true;
} else {
// 실패 처리
await this.alertService.okBtn('본인인증에 실패했습니다. 다시 시도해주세요.');
}
} catch (error: any) {
// 에러 타입에 따른 처리
if (error.message?.includes('취소')) {
await this.alertService.okBtn('인증이 취소되었습니다.');
} else {
await this.alertService.okBtn('인증 중 오류가 발생했습니다.');
}
}
마무리
아임포트를 사용하면 휴대폰 본인인증을 쉽게 구현할 수 있다. 하지만 보안과 개인정보 보호를 위해 서버 검증과 마스킹 처리는 반드시 해야 한다.
웹과 네이티브 환경을 모두 지원하려면 환경 분기 처리가 중요하다. 또한 REST API 키는 절대 클라이언트에 노출하지 말고, Firebase Functions에서만 사용해야 한다.
- 아임포트 가입 후 가맹점 식별 코드 발급
- 웹은 JavaScript SDK, 네이티브는 Capacitor 플러그인 사용
- REST API 키는 Firebase Functions에서만 사용
- imp_uid를 서버에서 반드시 검증
- 주민번호는 마스킹해서 저장 (법적 요구사항)
- 웹과 네이티브 환경 분기 처리 필수
'개발 > FRONT' 카테고리의 다른 글
| npm cache 때문에 수정이 적용되지 않을 때 해결방법 npmcache 삭제 (0) | 2025.12.23 |
|---|---|
| Android Error : NullPointerException 완전 정리 (0) | 2025.12.23 |
| Firebase Cloud Messaging(FCM) 완벽 설정 가이드 - 푸시 알림의 모든 것 (0) | 2025.12.21 |
| async/await로 완성하는 비동기 조회 마스터 클래스 - Promise 지옥에서 벗어나기 (0) | 2025.12.21 |
| Firebase Storage 파일 업로드 완벽 마스터하기 - 실전 코드로 배우는 업로드 전략 (0) | 2025.12.21 |