링크 : https://d2.naver.com/helloworld/2351859
네이버페이는 구매자가 네이버 ID로 네이버페이 가맹점 어디에서나 안심하고 간편하게 구매할 수 있게 하는 서비스입니다
네이버페이 서비스를 이용하기 위해서 가맹점은 서버 간 통신을 통해 응답값을 받아 프런트엔드에 전달하고 네이버페이 결제 화면을 호출해야 했습니다. 복잡한 과정 때문에 가맹점은 많은 사전 지식이 필요하고 구현에도 많은 고민이 필요했습니다. 이는 가맹점이 네이버페이 서비스를 이용하는 데 많은 비용이 든다는 것을 의미합니다.
이 때문에 가맹점의 작업을 최소화할 수 있도록 네이버페이 JavaScript SDK를 개발했습니다. 이 글에서는 네이버페이 JavaScript SDK 개발 과정 및 효과를 이야기하고자 합니다.
네이버페이 JavaScript SDK란
네이버페이 SDK가 필요했던 이유
SDK 개발 전 가맹점은 네이버페이를 연동하기 위해서 크게 다음 3단계의 과정이 필요했다.
- 결제 예약 API 호출: 가맹점은 서버 간에 결제 예약 API를 호출한다.
- 결제 화면 노출: 결제 예약 API 호출 후 받은 응답값을 활용하여 프런트엔드에서 페이지 전환 또는 팝업으로 결제 화면을 노출한다. 구매자는 결제 화면에서 결제 수단을 선택한다.
- 결제 승인 API 호출: 가맹점은 다시 서버 간에 결제 승인 API를 호출하여 결제를 마무리한다.
결제 승인 API 호출은 최종 승인 단계로, 서버 간 작업이 반드시 필요하다. 따라서 결제 승인 API 호출 단계는 SDK 개발 후에도 변경되지 않는다.
SDK 개발 전 결제 화면 노출까지의 과정을 좀 더 자세히 알아보겠다.
- 구매자는 결제 수단으로 네이버페이를 선택한다.
- 가맹점은 프런트엔드에서 Ajax API를 구현해 가맹점 서버로 요청을 보낸다.
- 가맹점은 server-to-server 통신을 통해 네이버페이 결제 예약 API를 호출한다.
- 가맹점은 응답값을 프런트엔드에 전달한다.
- 가맹점은 프런트엔드에서 네이버페이 결제 화면 URL을 호출한다.
가맹점 입장에서는 결제 예약 API를 호출 후 결제 화면을 호출해야 하므로, SSR(server-side rendering) 후 결제 화면 페이지로 리디렉션하거나 Ajax API를 따로 만들어야 한다. 이처럼 가맹점이 직접 구현해야 하는 부분이 많다.
그래서 가맹점의 개발 비용을 최소화하기 위해 SDK를 개발했다. 가맹점이 프런트엔드에서 네이버페이 JavaScript를 include하기만 하면 네이버페이를 쉽게 연동할 수 있게 하고자 했다.
이제부터 JavaScript SDK를 사용한 연동 과정에 대해 알아보도록 하겠다.
- 가맹점은 프런트엔드에 NaverPay.js 파일을 include하고 결제 정보를 설정한다.
- 구매자는 결제 수단으로 네이버페이를 선택한다.
- 가맹점은 Naver.Pay.open()을 호출하여 결제 화면을 노출한다.
가맹점은 네이버페이 JavaScript SDK를 사용하면 서버 간 작업 없이 결제 화면을 바로 노출할 수 있게 되었다. 즉, 가맹점은 이제 다음과 같은 코드를 사용하여 네이버페이 결제 화면을 노출할 수 있다.
<!DOCTYPE html>
<html>
<head></head>
<body>
<input type="button" id="naverPayBtn" value="네이버페이 결제 버튼">
<script src="https://nsp.pay.naver.com/sdk/js/naverpay.min.js"></script>
<script>
var oPay = Naver.Pay.create({
"mode" : "production", // development or production
"clientId": "u86j4ripEt8LRfPGzQ8" // clientId
});
//직접 만든 네이버페이 결제 버튼에 click 이벤트를 할당하세요.
var elNaverPayBtn = document.getElementById("naverPayBtn");
elNaverPayBtn.addEventListener("click", function() {
oPay.open({
"merchantUserKey": "가맹점 사용자 식별키",
"merchantPayKey": "가맹점 주문 번호",
"productName": "상품명을 입력하세요",
"totalPayAmount": "1000",
"taxScopeAmount": "1000",
"taxExScopeAmount": "0",
"returnUrl": "사용자 결제 완료 후 결제 결과를 받을 URL"
});
});
</script>
</body>
</html>
가맹점에게 JavaScript SDK를 제공함으로써 네이버페이 연동 개발 기간이 단축되었고 연동 포인트가 단순화되어 연동 검수 기간의 단축까지 이루어졌다.
동작 원리
이제 네이버페이 JavaScript SDK의 동작 원리를 알아보겠다.
가맹점이 JavaScript SDK를 include하면 window 객체에 Naver.Pay 객체가 생성된다.
Naver.Pay 객체에는 create(), open() 함수 등이 있다.
각 함수의 역할에 대해 알아보겠다.
create()
네이버페이 JavaScript SDK는 create() 호출 후 open()을 호출하도록 강제하고 있다. create()가 호출되면 include한 페이지에 static resource를 append한다.
CSS는 head 영역에 append된다.
레이어 결제 화면을 지원하는 태그는 body에 append된다.
레이어 결제 화면을 지원하기 위해 append된 태그에 이벤트(onClick)를 등록한다. 현재 브라우저의 cross origin 제한 정책에 의해 Ajax를 사용할 수 없다. 예를 들어 interpark.com에서 pay.naver.com으로 Ajax 요청을 할 수 없다. 이를 해결하는 방법은 다양한데, 네이버페이는 window.postMessage를 이용하여 cross origin 제한을 해결했다. 자세한 설명은 글 아래 cross origin 부분에서 설명하겠다.
다시 create()으로 돌아와서 window 객체에 "message" 이벤트를 listen하도록 이벤트를 등록한다. this._eventHandler는 polyfill을 지원하기 위해 만든 클래스이다.
this._eventHandler.attachEvent(window, "message", (event) => {
this.listen(event);
});
open()
가맹점이 open()을 호출하면 결제 예약(reserve)이 진행되고 결제 화면이 열린다.
- iframe을 생성하여 서버의 reserve proxy 페이지를 호출한다. 서버의 reserve proxy 페이지에서는 iframe의 window 객체에 "message" 이벤트를 listen하도록 이벤트를 등록한다.
- iframe이 proxy 페이지에 로딩된 시점을 파악하여 iframe 내의 contentWindow에 postMessage를 이용하여 결제 예약 파라미터를 전송한다.
- iframe 내에서 Ajax에 결제 예약 파라미터를 설정한 후 결제 예약 API를 호출한다.
- 마지막으로 응답받은 결제 예약키를 사용하여 결제 화면을 연다. 결제 화면은 레이어, 새 창, 팝업 형태로 열 수 있다.
지금까지는 네이버페이에서 JavaScript SDK가 필요했던 이유와 동작 원리를 설명했다. 다음으로는 JavaScript SDK 개발 과정과 몇 가지 고민해야 할 점을 알아보고 이를 해결한 방법을 이야기하겠다.
네이버페이 JavaScript SDK 개발 과정
대상 플랫폼 선정
먼저 SDK를 제공할 플랫폼을 선정하는 과정이 필요하다. 고려한 플랫폼은 Android, iOS, Web이며 상세 내용은 아래와 같다.
- Android
- iOS
- Web
- JavaScript
- React
- Vue.js
- AngularJS
이 중에서 가맹점이 가장 많이 사용하고 범용성이 가장 높은 Web 환경을 대상으로 JavaScript SDK를 먼저 제공하기로 했다. 따로 프레임워크 또는 라이브러리에 모듈로 제공하는 방법은 고려하지 않았다.
도구 설치 및 개발
개발 생산성과 유지보수를 용이하게 하기 위해서 모듈화가 필요했다. 이를 위해 네이버페이 JavaScript SDK는 ES6, npm 및 webpack 등을 이용하여 개발했다.
- webpack 사용 방법은 JavaScript 모듈화 도구, webpack을 참고한다.
네이버페이 JavaScript SDK 컴포넌트 구조는 아래와 같으며 webpack을 이용하여 최종 배포본를 생성한다.
npm 패키지 생성 후 라이브러리 배포까지 과정을 간략한 예시를 들어 설명하겠다. 여기에서는 IntelliJ와 CLI를 이용하여 작업했다. IntelliJ 사용 시 File > Settings > Languages & Frameworks > JavaScript > language version을 ECMAScript6으로 변경한다. 아래 과정은 사용하는 도구와 환경에 따라 다를 수 있다.
1. Node.js, npm 설치
패키지 매니저로 Node.js 설치하기를 참고하여 Node.js를 설치한다. npm은 Node.js와 함께 설치된다. 이 글에서는 Node.js v6.11.2, npm v3.10.10을 사용했다.
2. 프로젝트 생성 및 package.json 생성
- 빈 프로젝트를 하나 생성한다(File > New > Project > Empty Project).
- IntelliJ 사용 시 모듈이 추가되어 있지 않으면 프로젝트 Hierarchy가 보이지 않는다. 모듈을 추가하는 방법은 다음과 같다.
- File > ProjectStructure > Modules에서 + 버튼을 클릭하고 Import Module에서 빈 프로젝트 위치를 지정한다.
- Create module from existing sources를 선택한다.
- 프로젝트 루트를 선택하고 Finish를 클릭한다.
- 이 방법으로 모듈이 추가되지 않으면 Java 모듈을 추가한다.
- CLI 환경에서 해당 프로젝트 위치로 이동한 후 npm init을 실행하여 package.json을 생성한다.
3. index.js, pay.js 파일 생성
index.js 파일은 webpack의 entry 지점으로, Pay 객체를 생성하고 export한다.
import Pay from "./pay";
const pay = new Pay();
export {pay as Pay};
pay.js 파일에는 외부 가맹점에 제공할 함수가 있다.
class Pay {
constuctor() {
console.log("constuctor");
}
create() {
console.log("create");
}
open() {
console.log("open");
}
getVersion() {
console.log("v1.0.0");
}
}
export default Pay;
4. webpack 설치 및 webpack.config.js 파일 생성
npm install을 사용하여 webpack을 설치한다. 옵션은 -g, --save-dev 등이 있다.
- 옵션을 사용하지 않으면 webpack이 package.json의 dependency에 추가되며 프로젝트 내부에 node_modules 디렉터리가 생성된다.
- --save-dev 옵션을 사용하면 webpack이 package.json의 devDependencies에 추가된다.
- -g 또는 –-global 옵션은 전역 모드 옵션으로, 로컬 PC에 webpack 모듈을 설치해서 사용하며 package.json에는 포함되지 않는다. 즉, dependency나 devDependencies에 포함되지 않는다.
package.json의 dependency와 devDependencies의 차이는 외부 프로덕션 환경에 모듈로 제공할 때 devDependencies에 있는 모듈은 추가되지 않는다는 점이다.
webpack은 개발 환경에서만 필요하므로 --save-dev 옵션을 사용하여 설치한다. 제거할 때에는 npm uninstall을 사용한다. 옵션도 동일하게 사용한다.
npm install webpack webpack-cli --save-dev
npm init과 npm install webpack webpack-cli를 실행하고 나면 다음과 같은 package.json 파일을 확인할 수 있다. scripts 안에 build 명령어를 추가했다.
{
"name": "javascript_sdk_test",
"version": "1.0.0",
"description": "sdk_test",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"author": "shinhyun.kang",
"license": "ISC",
"devDependencies": {
"webpack": "^4.12.0",
"webpack-cli": "^3.0.8"
},
"dependencies": {}
}
webpack.config.js 파일을 작성해 보자. 아래 config는 index.js를 entry로 하여 번들링을 진행하며 결과물은 output 디렉터리 안에 output.js 파일로 저장된다.
const path = require('path');
const config = {
entry: {
main : ['./src/index.js']
},
output: {
path: path.resolve(__dirname, 'output'),
filename: 'output.js',
library: 'Naver',
libraryTarget: 'umd',
umdNamedDefine: true
}
};
module.exports = config;
5. webpack config 중 라이브러리에 관한 내용
config에서 library, libraryTarget이 라이브러리와 관련된 주요 설정이다. umdNamedDefine은 선택적으로 사용한다.
library 값 문자열은 변수명으로, libraryTarget 값에 따라 타입이 변경된다. 여기서는 libarary 값을 Naver로 하였다.
librayTarget 값은 var, this, window, commonjs, umd 등을 설정할 수 있다. 각 값을 설정했을 때 output.js 파일의 일부를 살펴보면 다음과 같다.
- librayTarget: var (기본값)
- librayTarget: this
- librayTarget: window
- librayTarget: commonjs
libaryTarget: umd(Universal Module Definition)
CommonJS, AMD를 각각 처리할 수 있으며, 이 두 개에 해당하지 않으면 window 객체에 할당한다.
!function(e,t)
{
"object"==typeof exports&&"object"==typeof module ? module.exports=t()
:"function"==typeof define&&define.amd ? define([],t)
:"object"==typeof exports ? exports.Naver=t()
:e.Naver=t()
}(window, function()
{
return function(e){}()
}
);
umdNamedDefine 값은 libraryTarget 값을 umd로 설정했을 때만 영향을 준다. umdNamedDefine 값을 true로 설정하면 AMD 모듈 이름을 libary에 값으로 지정한다. true로 설정하지 않으면 익명 define을 사용한다.
- umdNamedDefine: false -> define([],t)
- umdNamedDefine: true -> define("Naver",[],t)
JavaScript 라이브러리를 제공하는 방법에 따라 libaryTarget 값을 설정한다. 네이버페이는 umd를 적용했다. 옵션에 대한 더 자세한 설명은 webpack libary 가이드 문서를 참고한다.
6. webpack을 수행하여 output.js 파일 생성
앞에서 package.json 파일의 scripts에, build 명령어에 대해 webpack을 수행하도록 설정했다. npm run build를 수행하면 webpack.config.js 파일에 명시된 대로 webpack 번들링을 시작하고 그 결과인 output.js 파일을 생성한다.
output.js 파일을 HTML에서 include하여 함수를 수행해 보자. 개발자 도구를 이용하면 아래와 같이 window에 Naver 객체로 정의된 모습을 볼 수 있다. 미리 정의한 getVersion(), create() 함수도 사용해 보았다.
SDK 개발 시 고려해야 할 점
쉬운 사용
개발자가 쉽게 사용할 수 있는 SDK를 만들어야 한다.
기존 결제 연동에서는 서비스 개발자가 백엔드에서 네이버페이로 결제 예약 API를 호출하고 응답으로 받은 예약키로 프런트엔드에서 결제 화면을 노출해야 했다. 네이버페이 JavaScript SDK에서는 이 부분을 자바스크립트 함수 호출 하나만으로 구현할 수 있게 했다.
위에서 설명한 방법 외에 더 간단한 버전도 제공한다. 태그에 값을 설정하고 SDK를 include하기만 하면 네이버페이 결제 버튼이 생성된다.
<!DOCTYPE html>
<html>
<head></head>
<body>
<!--// mode : development or production-->
<script src=https://nsp.pay.naver.com/sdk/js/naverpay.min.js
data-client-id="u86j4ripEt8LRfPGzQ8"
data-mode="production"
data-merchant-user-key="가맹점 사용자 식별키"
data-merchant-pay-key="가맹점 주문 번호"
data-product-name="상품명을 입력하세요"
data-total-pay-amount="1000"
data-tax-scope-amount="1000"
data-tax-ex-scope-amount="0"
data-return-url="사용자 결제 완료 후 결제 결과를 받을 URL">
</script>
</body>
</html>
이러한 내용은 개발자센터에서 값을 변경해가며 테스트해 볼 수 있다.
코드 품질 관리
엄격한 품질 관리를 위해서 네이버페이 JavaScript SDK 개발에 다음과 같은 도구를 활용하고 있다.
- 단위 테스트: Mocha
- UI 테스트: Karma
- 코드 커버리지 테스트: nyc
- 정적 분석: ESLint
- 커밋 빌드, 일별(daily) 빌드, PR 빌드: travis-CI
Global namespace 사용 피하기
JavaScript에서 다양한 라이브러리 간 이름 중복 문제가 발생하지 않도록 주의해야 한다. Global namespace에 객체가 위치하면 변수명이나 함수명이 중복되어 문제가 생길 수 있으므로 별도의 독립된 namespace에서 객체를 생성한다. 네이버페이는 Naver.Pay라는 namespace를 기준으로 함수가 제공된다.
DOM, CSS 이름 중복 피하기
JavaScript SDK를 통해 HTML, CSS를 DOM에 append하는 경우, 가맹점이 기존에 사용하는 태그의 id나 class와 이름이 중복되는 경우를 주의해야 한다. 네이버페이는 class나 id가 중복되지 않도록 하기 위해 Npay_라는 prefix를 사용한다.
버전 관리 정책
시멘틱 버전 관리(Semantic Versioning)와 개발자센터 변경 히스토리를 통해 가맹점에게 SDK의 변경 내역을 참고할 수 있게 한다. 자세한 내용은 Semantic Versioning을 참고한다.
캐시 정책
SDK 역시 다른 애플리케이션과 마찬가지로 기능 개선 및 버그 수정을 위해 변경될 수 있다. 클라이언트에서 항상 최신 버전이 사용되도록 해야 하며, 이를 위해 네이버페이 SDK 배포 서버로부터 include하여 사용할 것을 가이드한다.
이때 캐시 정책이 매우 중요하다. SDK 로직에서 사용하는 서버 API 로직이 상호 밀접한 관련이 있을 수 있으므로, 변경 사항이 클라이언트에 적절히 적용이 되지 않으면 정상적인 서비스를 제공하기 어렵다.
이 문제를 해결하는 방법으로는 가맹점이 timestamp를 붙이거나 네이버페이 측에서 wrapping한 환경을 만들고 내부에서 timestamp를 붙이는 방법도 있으나 웹 표준인 cache-control을 활용하는 것이 좋다.
response 헤더에 cache-control을 "no-cache"로 설정하면 캐시를 허용하나 서버로 재검사 요청 후 304 응답을 받은 경우에만 캐시된 데이터를 사용한다. cache-control을 "no-store"로 설정하면 캐시를 전혀 허용하지 않고 모든 요청을 새로 한다. HTTP 캐싱에 대한 자세한 내용은 Google Developers의 HTTP 캐싱을 참고한다.
cache-control을 "no-cache", "no-store"로 설정하고 파일이 변경되었을 때 각 브라우저의 동작 여부를 테스트해 보았고 그 결과는 다음 표와 같다.
Windows - Chrome | Windows - IE11 | Windows - IE9 | Windows - Firefox | ||
---|---|---|---|---|---|
no-cache | O | O | O | O | |
no-store | O | O | O | O | |
macOS - Chrome | macOS - Safari | ||||
no-cache | O | O | |||
no-store | O | O | |||
iOS - Chrome | iOS - Safari | iOS – 네이버앱 | iOS - UIWebView | iOS - WKWebView | |
no-cache | O | O | O | O | O |
no-store | O | O | O | O | O |
Android - Chrome | Android - 네이버앱 | Android - 삼성브라우저 | |||
no-cache | O | O | O | ||
no-store | O | O | O |
각 브라우저의 cache-control 지원 여부는 MDN web docs의 Cache-Control을 참고한다.
cross origin
SDK를 사용하는 가맹점의 도메인은 모두 다르다. 다음은 도메인이 다른 환경에서 흔히 발생하는 오류로, Ajax 요청 시 same origin policy에 의해 발생한다.
이 오류를 해결하는 방법으로는 CORS, JSONP, postMessage, xDomain 등이 있다. 여기서는 CORS, postMessage에 대해 설명한다.
먼저 CORS는 Cross-Origin Resource Sharing의 약자로, HTTP 응답 헤더에 Access-Control-Allow-Origin : target origin을 설정하는 방법이다. target origin에는 별표(*)를 사용할 수 있다. 하지만 이 방법은 Internet Explorer 9 이하 버전에서는 XDomainRequest 객체를 통해서만 사용할 수 있다.
그래서 Internet Explorer 8 이상 버전을 지원하는 postMessage를 사용했다. postMessage는 cross origin 통신이 가능하게 하는 window 객체의 함수이다.
targetWindow .postMessage ( message , targetOrigin , [ transfer ]);
targetWindow는 팝업(새 창)이나 iframe일 수 있다. 따라서 open, opener 객체를 통해 postMessage를 전송하거나 iframe.contentWindow, parent를 통해 전송할 수 있다. 단, Internet Explorer 8을 지원하려면 iframe 형태로만 사용할 수 있다.
targetOrigin은 보안상 중요한 역할을 한다. targetOrigin에는 targetWindow의 origin을 입력해야 한다. 다른 값을 입력하면 다음과 같은 오류가 발생한다.
다음은 postMessage를 받는 쪽의 코드이다. "message" 이벤트를 받을 수 있게 이벤트 리스너를 연결한다. parent와 postMessage를 주고받아야 하는 경우에는 postMessage의 message에 가맹점의 origin 정보를 보내 활용했다(parent와 통신을 하려면 parent에도 "message" 이벤트를 받을 수 있게 이벤트 리스너를 연결해야 한다).
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
var receiveData = JSON.parse(event.data);
if (event.origin === receiveData.merchantOriginUrl) {
parent.postMessage("{'code':'Success'}", receiveData.merchantOriginUrl);
}
}
cross origin 문제를 해결하는 방법은 다양하므로 자신이 지원하고자 하는 환경에 가장 적합한 방식을 선택한다.
마치며
지금까지 네이버페이 서비스를 위한 SDK를 개발한 이유, 네이버페이 JavaScript SDK 동작 원리, webpack을 사용하여 라이브러리를 패키징하는 방법, SDK 개발 시 고려해야 할 점을 이야기했다.
네이버페이 JavaScript SDK는 네이버페이 개발자센터에서 사용해 볼 수 있다. 네이버페이 JavaScript SDK를 사용하기에 앞서 이 글이 네이버페이 JavaScript SDK를 이해하는 데 도움이 되었으면 좋겠다.
또한 SDK를 처음 개발하려는 분에게도 이 글이 도움이 되길 바란다.
'WEB > 기타' 카테고리의 다른 글
2018년과 이후 JavaScript의 동향 - JavaScript(ECMAScript) (0) | 2019.01.22 |
---|---|
MS, 엣지브라우저 크로미엄으로 재개발 공식화 (0) | 2019.01.22 |
네이버 메인 페이지의 트래픽 처리 (0) | 2019.01.22 |
GIF대신 Video로 변환해보자 (0) | 2019.01.22 |
Kakao Hangul Analyzer III (0) | 2019.01.22 |