오랜만에 블로그를 쓴다...
회사에 일을 시작하면서 하루하루가 바쁘게 돌아가다 보니 블로그를 쓸 여력이 없었다.. (핑계야 핑계 이 자식아)
일하면서 블로그 쓰시는 수많은 개발자 분들 존경합니다!
그래도 노션에 이것저것 정리해 두었으니 판도리의 상자를 열어보자

다니던 회사에서 외주를 하나 맡아 진행하였는데, 외주사에서 NICE 본인인증 개발을 요청하셨다.
오! 한 번도 해보지 않은 것이잖아! 재밌겠는 걸(?)
이때까지는 몰랐다.. 험난한 일이 겪을 줄은...
데스크탑에서는 되는데, 다른 곳에서는 안되네..?
처음에 구현하였을 때는 데스크탑과 모바일 브라우저(크롬 웹 뷰와 갤럭시 인터넷)에서는 인증이 정상적으로 되었다.
하지만, iOS 웹(사파리)와 카카오톡 인앱 브라우저에서는 본인인증 후 오류 화면이 나타났다.

에러 화면에는 500 | Internet Server Error. 라고 나타났고, 서버 로그를 살펴보았을 때는 관련하여 별 다른 에러 사항은 없었다 ..
Next.js 에서 500 에러를 반환하다니,, 일단 머리를 부여잡고 방법을 찾기 시작했다.
NICE는 어떤 인증 과정을 거칠까?

허허 만들고 나니 뿌듯하다
대략적으로 위 이미지의 과정을 통해 본인인증이 진행된다.
우리는 프론트엔드 관점에서 인증과정을 해결할 것이기 때문에, 6번 (NICE 인증 창 호출)부터 살펴보자
NICE 인증 창(팝업)을 열어보자!
우선, 백엔드(서버)에서 아래 3가지 값을 받고, NICE 측에 보내야 인증 창을 열 수 있다.
- token_version_id: 암·복호화/검증에 쓰이는 키(또는 프로토콜)의 버전 식별자. NICE 가입 시 받을 수 있다고 한다.
- enc_data: 실제 인증 결과(예: 이름, 생년월일, CI/DI 등)가 들어있는 암호화된 페이로드. 그대로 읽을 수는 없고, 서버에서 NICE 검증/복호화 API로 보내 해제해야 한다.
- integrity_value: 전달 중 변조 여부를 확인하는 무결성 체크값(MAC/HMAC 계열). 서버가 enc_data가 바뀌지 않았는지 검증할 때 사용된다고 한다.
값의 이름은 프로젝트마다 다르게 쓰일 수 있을 것 같다.
자, 이제 인증 창을 열어보자
// 1. 폼 생성
const form = document.createElement("form");
// 2. method와 action(엔드포인트 지정)
form.method = "POST";
form.action = "https://nice.checkplus.co.kr/CheckPlusSafeModel/service.cb"; // 폼을 제출할 목적지(엔드포인트)
// 3. input에 넣을 name과 value를 지정
const inputs = [
{ name: "m", value: "service" },
{ name: "token_version_id", value: token_version_id },
{ name: "enc_data", value: enc_data },
{ name: "integrity_value", value: integrity_value },
];
// 4. input을 만들고 forEach를 통해 값을 할당
inputs.forEach(({ name, value }) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = name;
input.value = value;
form.appendChild(input);
});
document.body.appendChild(form);
// 5. window.open을 통해 창(팝업) 오픈
const popup = window.open("", "nicePopup", "width=500,height=600");
form.target = "nicePopup" // 해당 폼 제출 결과가 팝업 창에서 열리도록 설정
// 6. 폼 전송 - (token_version_id, enc_data, integrity_value를 담아 POST 전송)
form.submit();
document.body.removeChild(form);
위 코드의 과정을 거치면 NICE 인증 창을 열 수 있다.

폼을 NICE 측에 정상적으로 보냈다면 열어둔 창에서 NICE에서 받은 인증 화면이 로드된다.
(순식간에 일어나는 일이라 창이 열리면 거의 바로 인증 화면이 나타난다.)
특이한 점은, form.submit()를 통해 폼을 전송한 후, NICE로부터 응답 값을 JSON 같은 형식이 아닌 HTML/CSS/JS로 받는다는 점이다.
요즘은 JSON을 많이들 사용하지만 옛날에는 저렇게 HTML/CSS/JS로 반환받는 일이 지금보다 더 많지 않았을까 싶다.
본인인증 후 처리 (window.closed)
본인인증이 끝나면 NICE 측에서 우리가 설정한 returnUrl로 리다이렉트 시킨다.
이 부분이 아주 NICE 인증에서 중요한 점 중 하나라고 생각한다.
이 returnUrl을 어떻게 처리하냐에 따라 백/프론트에서 처리할 방법이 다양하다.
하지만, 처음 NICE 본인인증 개발 당시 returnUrl을 지정한다는 사실을 몰랐다..
스스로 NICE 인증에 대한 이해도 부족했고, 백엔드 개발자 분이 급하게 용역으로 오셔서 비대면으로 소통이 부족했던 당시 ...
일단은! 본인인증을 정상적으로 진행하면 인증 창이 알아서 닫힌다. (아마 NICE 측에서 창이 닫히도록 처리한 것 같다.)
이후, 프론트에서 인증 창이 닫힌 것을 감지하고, 백엔드 서버 측에 token_version_id, enc_data, integrity_value 값을 보내면 인증이 완료되었다는 것을 확인하도록 구현하였다.
인증 창이 닫힌 것을 감지하는 방법은?
프론트에서 인증 창이 닫혔다는 것을 감지하기 위해서 window 객체의 closed를 활용하였다.
그리고 React의 상태 관리처럼 실시간으로 값 변화를 감지하지 못하기 때문에, setInterval를 통해 1초에 한 번씩 window.closed 값을 확인하도록 코드를 만들었다.
const popup = window.open("", "nicePopup", "width=500,height=600"); // 이전에 선언
const checkPopupClosed = setInterval(async () => {
if (popup.closed) {
try {
// 서버로 token_version_id, enc_data, integrity_value 보내기
// 이외 인증 완료 처리
} catch (e) {
console.error("인증 실패");
}
}
}, 1000);
여기까지 구현하였을 때 크롬(데스크탑과 개발자 모드 모바일까지)과 갤럭시 인터넷 브라우저에서는 본인인증이 정상적으로 되어서 할 만하네! 라고 생각하였다..
하지만, 우리 프론트엔드 개발자는 다양한 브라우저를 고려해야 했기에 iOS의 사파리로 시도해 보았으나... 결과는 Next.js의 505.
뿐만 아니라, 카카오톡 인앱 브라우저에서도 Next.js의 505 화면이 떴다..
이때부터 회사와 집, 주말을 가리지 않고 오랜 시간에 걸쳐 해결 방법을 탐구하였다. . . .... ... ..
시도한 다른 방법 (userAgent)
혹시나 모바일에서 window.open을 통해 새 창(모바일에서는 새 탭으로 열리는)으로 열리지 않도록 수정하면 해결할 수 있지 않을까 생각했다.
그렇게 다음 2가지 방법을 시도하였다.
- 새 창으로 열리지 않고 현재 창(탭)에서 열리게 해야 한다.
- 모바일에서 접속했다는 사실을 알 수 있어야 한다.
먼저, 현재 창(탭)에서 열리게 하려면 form의 target을 "_self"으로 지정하면 된다.
const form = document.createElement("form");
form.target = "_self";
다음으로, 접속한 기기가 브라우저인지 알 수 있는 방법은 userAgent를 활용하는 것이다.
userAgent란, 브라우저가 자기 자신(유저 에이전트)에 대한 정보를 문자열로 알려주는 값이다. 즉, 브라우저가 스스로를 소개하는 문자열을 의미한다.
보통 JS에서 navigator.userAgent를 통해 이 정보를 조회할 수 있으며, 예를 들어 아래와 같은 정보가 담겨있다.
console.log(navigator.userAgent);
// ▼ navigator.userAgent를 통해 조회된 정보 예시 ▼
// Mozilla/5.0 (Windows NT 10.0; Win64; x64)
// AppleWebKit/537.36 (KHTML, like Gecko)
// Chrome/121.0.0.0 Safari/537.36
여기서 브라우저의 환경이 모바일이라면 userAgent에 Mobile, Android, iPhone과 같은 키워드가 담겨있다.
target = "_self"과 navigator.userAgent을 사용하여 모바일 환경이라면 현재 창(탭)에서 로직을 작동시키기 위한 코드는 아래와 같다.
const isMobile = /Mobi|Android|iPhone|iPad|iPod|KAKAO/i.test(
navigator.userAgent
);
if (isMobile) {
form.target = "_self"; // 모바일 - 현재 창(탭)
form.submit();
}
코드를 보면, 정규표현식과 test() 함수를 사용하여 모바일 환경인지 판단할 수 있었고, 카카오톡 인앱 브라우저에서는 "KAKAO"라는 키워드가 들어갈 수 있다고 하여서 이 부분도 추가하였다.
그리고 결과는!
안타깝게 똑같은 문제가 계속 발생 .. .. . .. . . .
NICE는 인증 후 리다이렉트 시킬 때 어떻게 처리를 할까
원인의 공통적인 점은 모바일에서 이 현상이 일어난다는 점이다.
하지만, 모바일 환경에서 개발자 도구의 콘솔을 확인하기가 쉽지 않았다.
데스크탑 환경에서 크롬 개발자 도구의 모바일 뷰에서는 이상하게도 인증 과정이 잘 흘러갔다.
회사에 있는 데스크탑의 운영체제가 모두 윈도우이고, 나 또한 맥북이 없었기에 맥 OS에서 사파리를 통해 모바일 뷰를 확인해 볼 수 없었다.
머리를 굴리다가 '띵'하고 떠오른 방법은 MicroSoft의 Edge를 사용해 보는 것이었다. (지금 생각하면 네이버의 웨일도 있긴 하다)

손에 땀을 쥐며 Edge의 개발자 도구 - 모바일 뷰에서 NICE 인증을 하였더니! 다른 모바일 브라우저 환경과 똑같이 500 에러가 발생하는 것을 확인할 수 있었다.

그리고 Payload를 확인하니, token_version_id, enc_data, integrity_value까지 3개의 값이 있었다!
그렇다는 건.. 응답 값으로 request body에 form data 값을 넣어서 보냈었군!
보통 body에는 백엔드에서 Json 형태의 가공한 데이터를 받았었는데, Form Data로 넘어온다는 것이 참 신기하였다.
- request Method가 POST라는 점
- Form Data가 body로 넘어온다는 점
이 2가지를 알았지만, 클라이언트에서 일반적인 방법으로는 처리할 수는 없었다.
하지만, Next.js는 서버 사이드 렌더링처럼 서버와 관련해서 해결할 수 있는 방법이 있지 않을까?
다음에 이어지는 페이지에서 Next.js의 Route Handlers를 통해 해결 방법을 살펴보자.
https://mini-frontend.tistory.com/12
Next.js 에서 NICE 본인인증 구현하기2 (Route Handlers)
이전 포스트:https://mini-frontend.tistory.com/11 Next.js 에서 NICE 본인인증 구현하기 (iOS, 카카오톡 인앱 브라우저 환경에서는 왜 안될까)오랜만에 블로그를 쓴다...회사에 일을 시작하면서 하루하루가 바
mini-frontend.tistory.com
'문제 해결 (개발)' 카테고리의 다른 글
| Next.js 에서 NICE 본인인증 구현하기2 (Route Handlers) (0) | 2025.11.18 |
|---|---|
| AWS 서버 비용 줄여 보자! (EC2) (1) | 2024.04.25 |