더 알아보기

Vercel 연동

Vercel 에서 결제가 안 뜨는 이유는 네 가지예요. 아래를 그대로 복사하세요.

로컬에서는 되는데 배포하면 결제창이 안 뜨는 경우, 아래 네 개를 순서대로 그대로 넣으면 돼요.

증상 여기로
결제 버튼을 눌러도 아무 반응이 없어요 1. 보안 헤더
결제창은 뜨는데 결제수단·카드를 고르면 멈춰요 1. 보안 헤더
콘솔에 Refused to frame 이 찍혀요 1. 보안 헤더
금액이 클 때만 인증창이 안 떠요 2. 결제창 여는 방식
모바일·인앱 브라우저에서만 안 떠요 2. 결제창 여는 방식
client_key 가 undefined 예요 3. 환경 변수

1보안 헤더(CSP) — 이거 복붙하세요

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "frame-src 'self' https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.nicepay.co.kr https://*.settlebank.co.kr https://settlebank.co.kr https://*.inicis.com https://*.kcp.co.kr https://*.tosspayments.com https://*.smartropay.co.kr https://*.smartro.co.kr https://*.teledit.com https://teledit.com https://*.danalpay.com https://danalpay.com https://*.danal.co.kr https://danal.co.kr https://*.payletter.com https://*.payapp.kr https://*.easypay.co.kr https://*.welcomepayments.co.kr https://welcomepayments.co.kr https://*.payco.com https://payco.com https://*.kakao.com https://*.kakaopay.com https://kakaopay.com https://*.naver.com https://*.mobilians.co.kr https://*.ksnet.co.kr https://*.kspay.co.kr https://*.jtnet.co.kr https://jtnet.co.kr https://*.paypal.com https://*.stripe.com https://*.stripe.network https://*.kbcard.com https://*.bccard.com https://*.lottecard.co.kr https://*.shinhancard.com https://*.hyundaicard.com https://*.hanacard.co.kr https://*.samsungcard.co.kr https://*.nonghyup.com https://*.wooricard.com https://dacs.wooricard.com:8886 https://*.citibank.co.kr https://*.vpay.co.kr https://vpay.co.kr https://*.daum.net https://*.daumcdn.net https://*.channel.io"
        }
      ]
    }
  ]
}json

PG · 카드 인증 · 주소 검색 · 상담 위젯에 필요한 값이 전부 들어 있어요. 어떤 PG를 쓰는지 몰라도 그대로 넣으면 돼요.

CSP를 설정한 적이 없다면 1번은 건너뛰어도 돼요

CSP는 직접 켜야 동작해요. 선언한 적이 없다면 결제창은 그냥 떠요. 그래도 결제창이 안 뜬다면 2번을 보세요.

이미 CSP 를 쓰고 있다면 `frame-src` 값만 합치세요

통째로 덮어쓰면 원래 동작하던 분석 도구·채팅 위젯이 멈춰요. 설정 파일의 다른 항목(rewrites 등)도 그대로 두세요.

그리고 원래 없던 지시어를 새로 만들지 마세요 — 특히 script-src 를 새로 선언하는 순간 목록에 적지 않은 스크립트가 전부 차단돼서 분석 도구·채팅 위젯·광고 태그가 한꺼번에 멈춰요. 원래 쓰고 있던 지시어라면 아래 이미 쓰고 있는 지시어만 병합의 값을 합치세요.

고쳤는데 안 바뀌면 `middleware.ts` 를 보세요

CSP 를 넣을 수 있는 곳이 vercel.json · next.config.js · middleware.ts 세 곳인데, middleware.ts 가 나머지를 덮어써요. 실제로 나가는 값을 먼저 확인하세요.

curl -sI https://내도메인 | grep -i content-security-policybash

내 문제인지 먼저 확인해요

브라우저 개발자도구(F12) → Console 탭에 아래와 비슷한 줄이 있는지 봐요.

Refused to frame 'https://...' because it violates the following
Content Security Policy directive: "frame-src ..."

이 메시지가 보이면 부트페이 장애도, 연동키(Client Key) 문제도 아니에요. 내 웹서버 설정 문제예요.

결제창을 막는 지시어는 frame-src 하나예요(iframe-src 라는 지시어는 없어요). frame-src 를 아예 선언하지 않으면 default-src 로 폴백되므로, default-src 'self' 만 있어도 똑같이 막혀요.

무엇이 들어 있나 — 그룹별로 복사하세요

위 완본을 그대로 쓰면 아래가 전부 포함돼요. 일부만 넣고 싶을 때 각 블록을 그대로 복사하세요.

PG 결제창 (21개)

라이트페이 · 나이스페이먼츠 · 세틀뱅크(헥토파이낸셜) · KG이니시스 · NHN KCP · 토스페이먼츠 · 스마트로 · 다날 · 페이레터 · 페이앱 · KICC 이지페이 · 웰컴페이먼츠 · 페이코 · 카카오페이 · 네이버페이 · KG모빌리언스 · KSNET · JTNet · 티페이 · PayPal · Stripe

https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.nicepay.co.kr https://*.settlebank.co.kr https://settlebank.co.kr https://*.inicis.com https://*.kcp.co.kr https://*.tosspayments.com https://*.smartropay.co.kr https://*.smartro.co.kr https://*.teledit.com https://teledit.com https://*.danalpay.com https://danalpay.com https://*.danal.co.kr https://danal.co.kr https://*.payletter.com https://*.payapp.kr https://*.easypay.co.kr https://*.welcomepayments.co.kr https://welcomepayments.co.kr https://*.payco.com https://payco.com https://*.kakao.com https://*.kakaopay.com https://kakaopay.com https://*.naver.com https://*.mobilians.co.kr https://*.ksnet.co.kr https://*.kspay.co.kr https://*.jtnet.co.kr https://jtnet.co.kr https://*.paypal.com https://*.stripe.com https://*.stripe.network

https://*.bootpay.co.kr 와 라이트페이 두 줄은 어떤 PG를 쓰든 항상 필요해요.

카드 인증(3-D Secure) (11개)

KB국민 · BC · 롯데 · 신한 · 현대 · 하나 · 삼성 · NH농협 · 우리 · 씨티 · VPay(카드사 공동)

https://*.kbcard.com https://*.bccard.com https://*.lottecard.co.kr https://*.shinhancard.com https://*.hyundaicard.com https://*.hanacard.co.kr https://*.samsungcard.co.kr https://*.nonghyup.com https://*.wooricard.com https://dacs.wooricard.com:8886 https://*.citibank.co.kr https://*.vpay.co.kr https://vpay.co.kr

PG 결제창 안에서 카드 인증 단계로 넘어가면 카드사 자체 서버(ACS) 가 열려요. PG 도메인만 허용하면 "결제창은 떴는데 카드를 고르니 멈춘다" 가 돼요.

우리카드는 포트(:8886)까지 적어야 해요

CSP는 포트를 생략하면 기본 포트(443)만 허용해요. 우리카드 인증 서버는 8886 포트에서만 응답하므로 https://*.wooricard.com 만으로는 덮이지 않아요.

놓치면 다른 카드는 되는데 우리카드 사용자만 결제가 실패해요.

신협 · 전북은행 · 광주은행 · 수협은 실제 인증 서버 여부를 확인하지 못해 목록에서 뺐어요. 해당 카드에서 막히면 콘솔에 찍힌 도메인을 추가하세요.

주소 검색 · 상담 위젯

https://*.daum.net https://*.daumcdn.net https://*.channel.io

본인인증

본인인증(method: 'auth')을 쓴다면 완본에 없으니 따로 추가​​해요.

https://nice.checkplus.co.kr https://*.mobile-ok.com https://*.kmcert.com https://*.vno.co.kr https://*.teledit.com

이미 쓰고 있는 지시어만 병합

아래 지시어를 원래 선언해 두었다면 결제 도메인을 함께 넣어야 해요. 선언한 적이 없다면 건드리지 마세요.

form-action · connect-src · script-src · img-src

form-action — KG이니시스·NHN KCP 모바일은 폼 전송으로 결제창을 전환해요

form-action 'self' https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.nicepay.co.kr https://*.settlebank.co.kr https://settlebank.co.kr https://*.inicis.com https://*.kcp.co.kr https://*.tosspayments.com https://*.smartropay.co.kr https://*.smartro.co.kr https://*.teledit.com https://teledit.com https://*.danalpay.com https://danalpay.com https://*.danal.co.kr https://danal.co.kr https://*.payletter.com https://*.payapp.kr https://*.easypay.co.kr https://*.welcomepayments.co.kr https://welcomepayments.co.kr https://*.payco.com https://payco.com https://*.kakao.com https://*.kakaopay.com https://kakaopay.com https://*.naver.com https://*.mobilians.co.kr https://*.ksnet.co.kr https://*.kspay.co.kr https://*.jtnet.co.kr https://jtnet.co.kr https://*.paypal.com https://*.stripe.com https://*.stripe.network https://*.kbcard.com https://*.bccard.com https://*.lottecard.co.kr https://*.shinhancard.com https://*.hyundaicard.com https://*.hanacard.co.kr https://*.samsungcard.co.kr https://*.nonghyup.com https://*.wooricard.com https://dacs.wooricard.com:8886 https://*.citibank.co.kr https://*.vpay.co.kr https://vpay.co.kr

connect-src — 결제 승인·상태 조회 통신

connect-src 'self' https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.nicepay.co.kr https://*.settlebank.co.kr https://settlebank.co.kr https://*.inicis.com https://*.kcp.co.kr https://*.tosspayments.com https://*.smartropay.co.kr https://*.smartro.co.kr https://*.teledit.com https://teledit.com https://*.danalpay.com https://danalpay.com https://*.danal.co.kr https://danal.co.kr https://*.payletter.com https://*.payapp.kr https://*.easypay.co.kr https://*.welcomepayments.co.kr https://welcomepayments.co.kr https://*.payco.com https://payco.com https://*.kakao.com https://*.kakaopay.com https://kakaopay.com https://*.naver.com https://*.mobilians.co.kr https://*.ksnet.co.kr https://*.kspay.co.kr https://*.jtnet.co.kr https://jtnet.co.kr https://*.paypal.com https://*.stripe.com https://*.stripe.network

script-src — 부트페이 SDK·주소 검색 스크립트

script-src 'self' 'unsafe-inline' https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.daum.net https://*.daumcdn.net https://*.channel.io

img-src — 카드사 로고·QR (data: blob:)

img-src 'self' data: blob: https://*.bootpay.co.kr https://*.lightpay.kr https://lightpay.kr https://*.nicepay.co.kr https://*.settlebank.co.kr https://settlebank.co.kr https://*.inicis.com https://*.kcp.co.kr https://*.tosspayments.com https://*.smartropay.co.kr https://*.smartro.co.kr https://*.teledit.com https://teledit.com https://*.danalpay.com https://danalpay.com https://*.danal.co.kr https://danal.co.kr https://*.payletter.com https://*.payapp.kr https://*.easypay.co.kr https://*.welcomepayments.co.kr https://welcomepayments.co.kr https://*.payco.com https://payco.com https://*.kakao.com https://*.kakaopay.com https://kakaopay.com https://*.naver.com https://*.mobilians.co.kr https://*.ksnet.co.kr https://*.kspay.co.kr https://*.jtnet.co.kr https://jtnet.co.kr https://*.paypal.com https://*.stripe.com https://*.stripe.network

Vercel · Next.js 가 아니라면

같은 값을 아래 위치에 넣으면 돼요.

환경 위치
Nuxt nuxt.config.ts 의 routeRules
nginx add_header Content-Security-Policy "..." always;
Express helmet.contentSecurityPolicy({ directives })
헤더를 못 만질 때 HTML <head> 의 <meta http-equiv="Content-Security-Policy">

운영 반영 전에는 Content-Security-Policy-Report-Only 로 먼저 돌려 위반만 수집해 보는 방법도 있어요.


2결제창은 이렇게 여세요

PC 는 팝업, 모바일은 리다이렉트​​예요. iFrame 은 브라우저 정책 변화의 영향을 가장 많이 받아요.

const isMobile = /Mobile|Android|iP(hone|od|ad)/i.test(navigator.userAgent)

await Bootpay.requestPayment({
    client_key: process.env.NEXT_PUBLIC_BOOTPAY_CLIENT_KEY,
    price: 50000,
    order_name: '주문명',
    order_id: orderId,
    extra: {
        open_type: isMobile ? 'redirect' : 'popup',
        redirect_url: 'https://내도메인/order/result'   // redirect일 때 필수
    }
})javascript

금액이 클 때만 인증창이 안 뜨는 증상​(100만원 초과 · KB국민·BC·카카오뱅크)이 바로 이 설정으로 해결돼요. 그 증상은 보안 헤더를 아무리 고쳐도 안 고쳐져요. 자세한 내용은 로컬 네트워크 권한 문서를 보세요.

`redirect` 는 결과를 서버가 받아요

프론트엔드 콜백이 호출되지 않고 redirect_url 서버 라우트​​가 결과를 받아요. Next.js 라면 Route Handler(app/order/result/route.ts)로 만들어야 해요. 옵션만 바꾸고 결과 처리를 그대로 두면 결제는 되는데 주문이 확정되지 않아요.

그대로 복사할 수 있는 서버 라우트 예시는 로컬 네트워크 권한 문서에 있어요.

창 모드를 바꿔도 1번 헤더는 필요해요

redirect 로 열어도 SDK 는 부트페이 게이트웨이를 iFrame 으로 먼저 열어요. frame-src 에 부트페이 도메인이 없으면 세 방식 모두 막혀요.

CSP 를 아예 건드릴 수 없다면

보안 정책이나 권한 문제로 응답 헤더를 못 만지는 경우에는 HTML <head> 의 <meta http-equiv="Content-Security-Policy"> 로 같은 값을 넣으세요. 창 모드를 바꾸는 것으로는 대신할 수 없어요. 방식별 결과 수신 차이는 창 모드 문서를 참고하세요.


3환경 변수

# Vercel → Settings → Environment Variables
NEXT_PUBLIC_BOOTPAY_CLIENT_KEY=...   # 브라우저에 노출돼요 (정상)
BOOTPAY_SECRET_KEY=...               # 노출되면 안 돼요bash
Secret Key 에 `NEXT_PUBLIC_` 을 붙이지 마세요

NEXT_PUBLIC_ 이 붙은 값은 빌드 결과물에 그대로 포함돼 누구나 볼 수 있어요. 승인·조회·취소는 app/api/ 의 서버 코드에서만 처리하세요.

`client_key` 가 `undefined` 라면

① 접두사가 NEXT_PUBLIC_ 인지 ② Vercel 에 등록했는지 ③ 등록 후 재배포했는지 — 이 순서로 확인하세요. 값을 바꾸면 재배포해야 반영돼요.


4결제 버튼 컴포넌트

@bootpay/client-js 는 window 를 쓰므로 서버 렌더링 단계에서 부르면 에러가 나요. App Router 라면 'use client' 를 선언하세요.

'use client'

import { Bootpay } from '@bootpay/client-js'

export default function PayButton({ orderId, price }: { orderId: string; price: number }) {
    const onClick = () => Bootpay.requestPayment({ /* 위 2번 코드 */ })
    return <button onClick={onClick}>결제하기</button>
}tsx
팝업은 클릭 핸들러 안에서 바로 호출해요

버튼 클릭과 requestPayment() 사이에 await(주문 생성 API 등)를 두면 브라우저가 팝업을 차단해요. 주문 생성은 결제 버튼을 누르기 전에 끝내두세요.


그래도 안 되면

  1. 콘솔(F12)에 Refused to frame 이 있으면 → 거기 찍힌 도메인을 frame-src 에 그대로 추가해요. 이 목록보다 콘솔이 정확해요 — PG와 카드사는 도메인을 바꿔요
  2. curl -sI https://내도메인 으로 실제로 나가는 헤더를 확인해요. 설정이 다른 곳에서 덮이고 있을 수 있어요
  3. 위를 다 했는데 모바일만 안 된다면 open_type: 'redirect' 로 전환해요
  4. 금액이 클 때만 인증창이 안 뜬다면 CSP가 아니에요 → 로컬 네트워크 권한
  5. Preview 배포에서 결과를 못 받으면 → Vercel Deployment Protection 이 막은 거예요. Production 도메인​​에서 테스트하세요
  6. PC · 모바일 양쪽 경로​​로 테스트 결제를 해서 주문이 확정되는지 확인해요
  7. 웹훅을 함께 켜두면 결과를 놓치지 않아요