로컬에서는 되는데 배포하면 결제창이 안 뜨는 경우, 아래 네 개를 순서대로 그대로 넣으면 돼요.
| 증상 | 여기로 |
|---|---|
| 결제 버튼을 눌러도 아무 반응이 없어요 | 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"
}
]
}
]
}jsonconst CSP = "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"
module.exports = {
async headers() {
return [{
source: '/:path*',
headers: [{ key: 'Content-Security-Policy', value: CSP }],
}]
},
}javascriptPG · 카드 인증 · 주소 검색 · 상담 위젯에 필요한 값이 전부 들어 있어요. 어떤 PG를 쓰는지 몰라도 그대로 넣으면 돼요.
CSP는 직접 켜야 동작해요. 선언한 적이 없다면 결제창은 그냥 떠요. 그래도 결제창이 안 뜬다면 2번을 보세요.
통째로 덮어쓰면 원래 동작하던 분석 도구·채팅 위젯이 멈춰요. 설정 파일의 다른 항목(rewrites 등)도 그대로 두세요.
그리고 원래 없던 지시어를 새로 만들지 마세요 — 특히 script-src 를 새로 선언하는 순간 목록에 적지 않은 스크립트가 전부 차단돼서 분석 도구·채팅 위젯·광고 태그가 한꺼번에 멈춰요. 원래 쓰고 있던 지시어라면 아래 이미 쓰고 있는 지시어만 병합의 값을 합치세요.
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.networkhttps://*.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.krPG 결제창 안에서 카드 인증 단계로 넘어가면 카드사 자체 서버(ACS) 가 열려요. PG 도메인만 허용하면 "결제창은 떴는데 카드를 고르니 멈춘다" 가 돼요.
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.krconnect-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.networkscript-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.ioimg-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.networkVercel · 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_url 서버 라우트가 결과를 받아요. Next.js 라면 Route Handler(app/order/result/route.ts)로 만들어야 해요. 옵션만 바꾸고 결과 처리를 그대로 두면 결제는 되는데 주문이 확정되지 않아요.
그대로 복사할 수 있는 서버 라우트 예시는 로컬 네트워크 권한 문서에 있어요.
redirect 로 열어도 SDK 는 부트페이 게이트웨이를 iFrame 으로 먼저 열어요. frame-src 에 부트페이 도메인이 없으면 세 방식 모두 막혀요.
보안 정책이나 권한 문제로 응답 헤더를 못 만지는 경우에는 HTML <head> 의 <meta http-equiv="Content-Security-Policy"> 로 같은 값을 넣으세요. 창 모드를 바꾸는 것으로는 대신할 수 없어요. 방식별 결과 수신 차이는 창 모드 문서를 참고하세요.
3환경 변수
# Vercel → Settings → Environment Variables
NEXT_PUBLIC_BOOTPAY_CLIENT_KEY=... # 브라우저에 노출돼요 (정상)
BOOTPAY_SECRET_KEY=... # 노출되면 안 돼요bashNEXT_PUBLIC_ 이 붙은 값은 빌드 결과물에 그대로 포함돼 누구나 볼 수 있어요. 승인·조회·취소는 app/api/ 의 서버 코드에서만 처리하세요.
① 접두사가 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 등)를 두면 브라우저가 팝업을 차단해요. 주문 생성은 결제 버튼을 누르기 전에 끝내두세요.
그래도 안 되면
- 콘솔(F12)에
Refused to frame이 있으면 → 거기 찍힌 도메인을frame-src에 그대로 추가해요. 이 목록보다 콘솔이 정확해요 — PG와 카드사는 도메인을 바꿔요 curl -sI https://내도메인으로 실제로 나가는 헤더를 확인해요. 설정이 다른 곳에서 덮이고 있을 수 있어요- 위를 다 했는데 모바일만 안 된다면
open_type: 'redirect'로 전환해요 - 금액이 클 때만 인증창이 안 뜬다면 CSP가 아니에요 → 로컬 네트워크 권한
- Preview 배포에서 결과를 못 받으면 → Vercel Deployment Protection 이 막은 거예요. Production 도메인에서 테스트하세요
- PC · 모바일 양쪽 경로로 테스트 결제를 해서 주문이 확정되는지 확인해요
- 웹훅을 함께 켜두면 결과를 놓치지 않아요