Lovable에 토스페이먼츠 붙이기 — 시크릿 키를 프론트에 두지 않는 법
1.왜 시키는 대로 하면 막히나
러버블은 프런트엔드를 아주 잘 만듭니다. 문제는 카드 승인이 프런트엔드 일이 아니라는 데 있습니다.
토스페이먼츠가 주는 키는 두 종류입니다. 하나는 브라우저에서 결제창을 띄울 때 쓰는 클라이언트 키, 다른 하나는 API를 호출할 때 쓰는 시크릿 키입니다. 개발자센터 문서는 두 번째 키에 대해 이렇게 못 박습니다.
“시크릿 키는 외부에 노출되면 안 돼요. GitHub, 클라이언트 코드 등 외부에 보이는 곳에 추가하지 마세요.”
브라우저에서 도는 코드는 전부 ‘클라이언트 코드’입니다. 번들에 넣어도, 환경변수처럼 생긴 이름에 담아도, 개발자도구에서 그대로 읽힙니다. 러버블이 만들어준 파일 안에 test_sk_ 나 live_sk_ 로 시작하는 문자열이 보인다면 그 지점에서 멈춰야 합니다. 러버블 잘못이라기보다, 요청을 "결제 붙여줘" 한 줄로 던졌을 때 도구가 가장 짧은 길을 고른 결과에 가깝습니다.
2.흐름을 셋으로 쪼개면 경계가 보인다
토스페이먼츠 문서는 한 건의 거래를 요청 → 인증 → 승인 세 단계로 설명합니다. 앞의 둘은 브라우저, 마지막 하나는 서버 몫입니다.
구매자가 버튼을 누르면 SDK가 창을 띄웁니다(요청). 카드사 화면에서 본인 확인이 끝나면 정해둔 성공 주소로 돌아옵니다(인증). 여기까지는 아직 돈이 움직이지 않았습니다. 마지막 단계에서 서버가 승인 API를 불러야 비로소 “구매자의 카드 한도 또는 은행 계좌에서 차감”됩니다. 이 호출을 빠뜨리면 화면상으로는 성공처럼 보이는데 정산에는 아무것도 잡히지 않습니다.
그러니까 러버블에게 맡길 것과 옮겨야 할 것의 선은 여기입니다. 창을 띄우는 코드는 브라우저에 두고, 마지막 한 번의 호출만 밖으로 빼면 됩니다.
3.키를 둘 자리 — Edge Function 시크릿
러버블은 이미 이 문제의 해법을 갖고 있습니다. Supabase Edge Function입니다.
러버블 공식 문서는 민감한 자격증명을 이렇게 다룬다고 적고 있습니다. “Secrets are stored in your Supabase project, where your edge functions can read them; they never appear in your app’s code or repository.” 프로젝트에 보관되고 함수만 읽어가며, 앱 코드나 저장소에는 나타나지 않는다는 뜻입니다. 문서가 예로 드는 용도 자체가 “프런트엔드 코드에 노출할 수 없는 자격증명을 쓰는 모든 작업”입니다.
실무 순서는 단순합니다. 개발자센터에서 발급받은 시크릿 키를 Supabase 프로젝트의 시크릿으로 등록하고, 러버블 채팅에는 이렇게 요청하십시오.
confirm-payment 이라는 Edge Function을 만들어줘.
- paymentKey, orderId, amount 를 받는다
- 시크릿은 TOSS_SECRET_KEY 로 읽는다 (프런트엔드에 넣지 마)
- 승인 결과를 payments 테이블에 저장한다
프런트엔드에는 클라이언트 키만 남기고, 성공 페이지에서 이 함수를 호출해줘.
‘프런트엔드에 넣지 마’를 문장에 직접 넣는 것이 생각보다 크게 작동합니다. 이 말이 없으면 도구는 다시 짧은 길로 갑니다.
4.승인 호출 — 콜론 하나로 막히는 자리
엔드포인트는 POST /v1/payments/confirm 이고, 필수 파라미터는 paymentKey · orderId · amount 세 개입니다.
인증에서 대부분 한 번은 걸립니다. 시크릿 키를 그냥 base64로 바꾸면 실패합니다. 키 뒤에 콜론(:)을 붙인 다음 인코딩해야 합니다. 문서에도 “잘못된 요청입니다. :를 포함해 인코딩해주세요”라는 안내가 그대로 실려 있습니다. 함수 안쪽은 대략 이런 모양이 됩니다.
const secret = Deno.env.get("TOSS_SECRET_KEY");
const auth = btoa(secret + ":"); // ← 콜론을 빼면 인증 실패
const res = await fetch(
"https://api.tosspayments.com/v1/payments/confirm",
{
method: "POST",
headers: {
"Authorization": "Basic " + auth,
"Content-Type": "application/json",
},
body: JSON.stringify({ paymentKey, orderId, amount }),
}
);
const data = await res.json();
if (!res.ok) {
// 승인 실패. data.code / data.message 를 그대로 남겨둘 것
return new Response(JSON.stringify(data), { status: res.status });
}
실패 응답을 삼키지 마십시오. 나중에 원인을 찾을 단서가 code 와 message 뿐인 경우가 많습니다.
5.금액 대조 — 빠뜨리기 쉬운 한 줄
성공 주소로 돌아온 금액을 그대로 믿고 승인하면 안 됩니다.
주소창에 실려 오는 값은 브라우저를 거쳐 온 값입니다. 토스페이먼츠 문서가 검증을 권하는 이유도 여기 있습니다. “클라이언트에서 결제 금액을 조작해 승인하는 행위를 방지할 수 있어요.” 요청을 만들 때 orderId 와 금액을 데이터베이스에 먼저 적어두고, 승인 직전에 대조하십시오.
const { data: order } = await supabase
.from("orders").select("amount").eq("id", orderId).single();
if (!order || order.amount !== amount) {
return new Response("amount mismatch", { status: 400 });
}
// 여기를 통과한 뒤에만 승인 API 를 부른다
쿠폰이나 적립금을 쓰는 화면이라면 이 대조가 특히 중요합니다. 할인 계산을 브라우저에서만 하고 있으면 값이 얼마든지 바뀔 수 있기 때문입니다.
6.자주 나는 오류 두 가지
키를 섞어 쓴 경우와, 함수 주소를 잘못 부른 경우가 대부분입니다.
INVALID_API_KEY — 테스트 키는 test 로 시작합니다. 문서는 “세트가 아닌 키를 사용하거나 테스트 또는 라이브 키를 섞어 사용하면 INVALID_API_KEY 오류가 발생”한다고 밝힙니다. 브라우저에는 테스트 클라이언트 키를 넣고 함수에는 라이브 시크릿 키를 넣어둔 상태, 실서비스 전환 중에 흔히 생깁니다. 두 자리를 같은 세트로 맞추십시오.
승인이 아예 안 도는 경우 — 성공 페이지가 함수를 호출하지 않고 그냥 ‘주문이 완료되었습니다’만 띄우는 화면일 때입니다. 러버블이 만든 성공 페이지에 승인 호출이 실제로 들어 있는지 확인하고, 테스트 환경에서 한 건 통과시킨 뒤 상점 관리자 화면에 그 건이 잡히는지 눈으로 보십시오. 화면의 문구가 아니라 관리자 화면의 목록이 기준입니다.
7.러버블 Cloud를 쓸 때와 자체 Supabase를 쓸 때
지금 당장은 차이가 없지만, 넘겨줄 사이트라면 자체 프로젝트를 권합니다.
러버블 문서는 둘을 이렇게 구분합니다. Cloud는 “Automatic, no extra account”로 별도 계정 없이 바로 쓰고 비용은 러버블 크레딧으로 나갑니다. 자체 Supabase는 계정과 프로젝트가 필요하고 요금도 Supabase에서 따로 청구됩니다. 결제 코드 자체는 어느 쪽이든 같습니다.
다만 실제 카드 거래가 오가는 사이트라면, 관리 화면과 로그를 직접 여는 쪽이 편합니다. 승인 실패가 났을 때 함수 로그를 바로 볼 수 있어야 하고, 나중에 사이트를 다른 사람에게 넘길 때도 계정 소유가 명확해야 합니다. 이 인수인계 문제는 따로 다룰 만한 주제라, 블로그 목록에 이어지는 글을 올려두겠습니다.
§이 글의 근거
승인 엔드포인트와 필수 파라미터 세 개, 시크릿 키 노출 금지 문구, 콜론을 포함한 base64 인코딩, 테스트 키 접두어와 INVALID_API_KEY 조건, 요청·인증·승인 3단계와 금액 위변조 방지 안내는 모두 토스페이먼츠 개발자센터 원문에서 확인했습니다 — 코어 API, API 키, 결제 흐름 이해하기. 시크릿 보관 방식과 Cloud·자체 프로젝트 비교는 Lovable 공식 문서에서 가져왔습니다.
본문에 넣지 않은 것도 밝혀둡니다. v2 SDK의 npm 패키지 이름, 테스트용 클라이언트 키 값, 멱등키 헤더의 정확한 이름, 인증 후 승인까지의 제한 시간은 이번 확인 과정에서 원문으로 못 박지 못해 문장째 뺐습니다. 그 네 가지는 연동 전에 개발자센터에서 직접 확인하시기 바랍니다. 코드는 설명을 위한 최소 형태이며, 실제 적용 시에는 오류 처리와 중복 호출 방지를 더해야 합니다. 문의는 SLOHERO 홈에서 받습니다.