웹사이트에 배지 검증 임베드하기 (iframe도 SDK도 필요 없어요)

작은 JavaScript 스니펫(또는 서버 측 fetch)만 추가하면 공개 JSON-LD 엔드포인트로 모든 Open Badge를 검증할 수 있어요. iframe도, 서드파티 트래커도 필요 없어요.

Nacho Coll 작성자 업데이트됨 19 분 읽기
작은 JavaScript 스니펫(또는 서버 측 fetch)만 추가하면 공개 JSON-LD 엔드포인트로 모든 Open Badge를 검증할 수 있어요. iframe도, 서드파티 트래커도 필요 없어요.

자격 증명을 발급하다 보면 언젠가 이런 질문을 받게 돼요. “이 배지가 진짜인지 어떻게 알 수 있죠?” 이때 “PNG를 믿으세요”라고 답해서는 안 돼요. Open Badge v2.0에는 공개되어 있고 기계가 읽을 수 있는 검증 엔드포인트가 있어요. 내 사이트에 “Verified” 위젯을 넣는 데는 코드 약 10줄이면 충분해요. iframe도, 서드파티 임베드 스크립트도, SDK도 필요 없어요.

이 글에서는 엔드포인트를 호출하고 결과를 렌더링하는 방법을 바닐라 JavaScript와 서버 측 Node, Python으로 보여 드려요. 또한 “검증”이 실제로 무엇을 확인하는지, badges.ninja가 이미 수령자에게 제공하는 사람용 페이지와 이 엔드포인트가 어떻게 다른지, 그리고 검증 위젯이 완성된 것처럼 보이지만 사실은 아무것도 증명하지 못하게 만드는 흔한 실수도 다뤄요.

”검증됨”이 실제로 의미하는 것

Open Badge v2.0 Assertion은 정해진 구조를 가진 JSON-LD 문서예요. Assertion 자체(누가 언제 받았는지), BadgeClass에 대한 참조(배지가 무엇이고 기준을 누가 정의하는지), 발급자 프로필에 대한 참조(누가 보증하는지)로 이루어져 있어요. 배지를 “검증”한다는 것은 사실 서로 다른 세 가지를 확인한다는 뜻이에요.

  1. Assertion이 존재하고 확인되는가. 수여 ID가 실제로 존재하며, 추측되었거나 변조된 값이 아니어야 해요.
  2. 취소되었는가. 발급자는 잘못 발급했거나, 정책을 위반했거나, 인증이 만료된 경우처럼 나중에 배지를 취소할 수 있어요. 올바른 검증기는 기록이 존재한다는 사실만 확인하지 말고 취소 여부도 반드시 보여 줘야 해요.
  3. 연결 고리가 맞물리는가. Assertion이 참조하는 배지와 발급자가 실제로 존재하고 서로 일치하는 레코드로 확인되어야 해요. 고아가 되었거나 일치하지 않는 ID여서는 안 돼요.

URL에 요청을 보내 200 상태만 확인하는 위젯은 두 번째 항목을 완전히 놓쳐요. 취소된 Assertion도 문제없이 조회돼요. 페이로드에 취소 플래그가 붙어 있을 뿐이에요. 위젯이 “요청이 성공했는가”만 본다면 취소된 자격 증명도 검증된 것으로 표시돼요. 이는 위젯이 아예 없는 것보다 더 나빠요.

공개 사람용 페이지와 API 엔드포인트

https://badges.ninja에서 발급된 모든 수여에는 수령자가 바로 공유할 수 있는 공개 URL이 있고, 보는 데 가입이 필요 없어요.

Public credential page

대부분의 수령자는 이 페이지를 LinkedIn, 이력서, 이메일 서명에 링크해요. 사람이 보도록 만들어졌고, 배지 이미지와 발급자, 발급일이 표시되며, 여러분이 따로 할 일은 없어요. 아래 API 엔드포인트는 같은 Assertion을 구조화된 JSON-LD로 돌려줘요. 우리 사이트로 링크하는 대신 직접 UI를 만들 때 필요한 것이 바로 이쪽이에요.

엔드포인트

GET https://api.badges.ninja/certify-badge/award/{guid}

인증 헤더는 필요 없어요. 의도적으로 공개되어 있어요. Open Badges의 핵심은 고용주, 채용 담당자, 자격 심사 기관 등 누구나 계정 없이 자격 증명을 검증할 수 있어야 한다는 점이니까요. 반환되는 Assertion의 정확한 필드 이름을 포함한 전체 명세는 검증 문서에서 확인할 수 있어요.

수여 GUID를 전달하세요. GUID는 직접 관리하는 발급 기록에서 가져오거나, 수령자가 공유한 badges.ninja/awards/<guid> 링크에서 추출할 수 있어요.

바닐라 JS: 가져와서 렌더링하기

이것이 위젯의 전부예요. 빌드 단계도 npm 패키지도 없이 어떤 페이지에든 그대로 붙여 넣을 수 있어요.

<div id="badge-verify"></div>
<script>
  async function renderBadgeVerification(awardId, mountId) {
    const el = document.getElementById(mountId);
    el.textContent = "Checking badge status";
    try {
      const res = await fetch(
        `https://api.badges.ninja/certify-badge/award/${awardId}`
      );
      if (!res.ok) {
        el.textContent = "Credential not found.";
        return;
      }
      const assertion = await res.json();
      // —— revocation check first, a resolvable assertion isn't enough on its own
      if (assertion.revoked) {
        el.textContent = `Revoked: ${assertion.revocationReason || "no reason given"}`;
        return;
      }
      el.textContent = `Verified, issued ${new Date(assertion.issuedOn).toLocaleDateString()}`;
    } catch (err) {
      el.textContent = "Verification check failed.";
    }
  }
  renderBadgeVerification("REPLACE_WITH_AWARD_ID", "badge-verify");
</script>

REPLACE_WITH_AWARD_ID를 실제 수여 GUID로 바꾸세요. 클라이언트 측 통합은 이게 전부예요. CSS와 씨름해야 하는 iframe 경계도 없고, 검증 확인과 함께 자체 분석이나 추적 픽셀을 끌고 오는 서드파티 스크립트도 없어요.

서버 측: Node

백엔드에서 검증하고 싶을 수도 있어요. 예를 들어 직접 관리하는 프로필 페이지에 “Verified Partner” 배지를 표시하기 전이나, 온보딩 과정에서 자격 증명을 증빙으로 받아들이기 전이 그래요. 그럴 때는 DOM 부분만 빼면 fetch가 똑같아요.

async function verifyAward(awardId) {
  const res = await fetch(
    `https://api.badges.ninja/certify-badge/award/${awardId}`
  );
  if (!res.ok) return { verified: false, reason: "not_found" };
  const assertion = await res.json();
  if (assertion.revoked) {
    return { verified: false, reason: assertion.revocationReason || "revoked" };
  }
  return { verified: true, issuedOn: assertion.issuedOn, badge: assertion.badge };
}

서버 측: Python

import requests
# —— resolve the assertion, then check revocation before trusting it
def verify_award(award_id):
    res = requests.get(f"https://api.badges.ninja/certify-badge/award/{award_id}")
    if res.status_code != 200:
        return {"verified": False, "reason": "not_found"}
    assertion = res.json()
    if assertion.get("revoked"):
        return {"verified": False, "reason": assertion.get("revocationReason", "revoked")}
    return {"verified": True, "issued_on": assertion.get("issuedOn"), "badge": assertion.get("badge")}

두 예제 모두 200이 아닌 응답을 예외로 던지지 않고 “찾을 수 없음”으로 처리해요. 공개 검증 엔드포인트에서 잘못되었거나 추측한 수여 ID는 예외적인 상황이 아니라 충분히 예상되는 입력이에요. 잡아낸 오류가 아니라 일반적인 분기로 처리하세요.

다른 사람의 UI를 임베드하는 대신 결과를 직접 스타일링하기

위 위젯은 내 DOM에 fetch 결과를 넣는 것뿐이므로 모든 픽셀을 직접 제어할 수 있어요. 흔한 패턴은 응답에서 곧바로 세 가지 시각 상태로 매핑하는 거예요.

  • Verified — 초록색 체크 표시와 “Verified”, 발급일. 사이트의 배지/태그 컴포넌트에 맞춰 스타일링해요.
  • Revoked — 차분하거나 붉은 상태로 취소 사유를 숨기지 않고 보여 줘요. 수령자와 제3자 모두 조용한 실패보다 솔직한 “이유”가 도움이 돼요.
  • Not found — 시각적으로는 “취소됨”과 같게 다루되(가짜 성공 상태는 절대 렌더링하지 마세요) 문구는 달리해요. 잘못된 ID와 의도적인 취소는 페이지를 읽는 사람에게 서로 다른 상황이니까요.

컴포넌트 라이브러리는 필요 없어요. verified / revoked / not_found에 대한 switch 하나와 CSS 클래스 세 개면 충분해요.

캐싱으로 엔드포인트에 무리를 주지 않기

검증 엔드포인트는 공개되어 있고 인증이 필요 없으므로 요청당 크레딧 비용을 걱정할 필요가 없어요. 하지만 클라이언트에서 캐싱 없이 호출하면 페이지를 불러올 때마다 네트워크 왕복이 생겨요. 모든 방문자에게 같은 배지를 보여 주는 페이지(예: 푸터의 “Verified Partner” 배지)라면 요청마다 다시 가져오지 말고 짧은 TTL로 서버 측에 결과를 캐싱하세요. 취소는 빠르게 변하는 신호가 아니므로 5분에서 15분이면 충분해요.

const cache = new Map();
async function verifyAwardCached(awardId, ttlMs = 10 * 60 * 1000) {
  const hit = cache.get(awardId);
  if (hit && Date.now() - hit.at < ttlMs) return hit.result;
  const result = await verifyAward(awardId);
  cache.set(awardId, { result, at: Date.now() });
  return result;
}

방문자마다 다른 위젯(수령자가 개인 사이트에 자기 자격 증명을 임베드하는 경우)이라면 캐시를 건너뛰고 페이지를 불러올 때마다 새로 호출하세요. 그 규모에서는 추가 요청이 무시할 만한 수준이에요.

badges.ninja가 수령자에게 이미 제공하는 것

완전히 맞춤형인 임베드를 만들기 전에, 이미 무엇이 제공되는지 알아 두면 좋아요. 모든 수여의 공개 페이지에는 수령자가 코드 없이 바로 복사할 수 있는 공유용·임베드용 스니펫이 준비되어 있어요.

Public share page with social buttons + embed snippets

목적이 “수령자가 자기 배지가 진짜임을 보여 주게 하는 것”이라면, badges.ninja/awards/<guid> 페이지로 링크하는 편이 맞춤 위젯을 만드는 것보다 간단한 경우가 많아요. 앞에서 본 사람용 페이지이고, 검증됨과 취소됨의 구분도 이미 처리해 줘요. 이 글의 맞춤 위젯은 검증 상태를 내 제품 UI 안에 렌더링해야 할 때 만드세요. 파트너 디렉터리, 내부 감사 대시보드, 지원자가 주장한 인증을 확인하는 채용 도구처럼 다른 사이트로 링크하는 것이 알맞은 UX가 아닌 경우예요.

왜 iframe이나 벤더 위젯이 아닐까요?

일부 플랫폼은 검증 상태를 임베드하라며 <iframe src="https://vendor.example.com/verify-widget"> 같은 스니펫을 줘요. 동작은 하지만 실제 비용이 따라와요. 내용을 사이트에 맞게 다시 스타일링할 수 없고(벤더가 만든 CSS를 그대로 써야 해요), Content-Security-Policy에서 명시적으로 허용해야 하는 교차 출처 문서가 생기며, 방문자가 검증 상태에 관심이 있든 없든 페이지를 불러올 때마다 서드파티 iframe 호스트로 요청이 나가요. 호출 두 번짜리 fetch 위젯은 이 세 가지를 모두 피해요. DOM도 스타일링도 내 것이고, 내가 호출하기로 했을 때만 실행돼요.

검증 체인 전체 따라가기

상태 위젯보다 더 철저한 것을 만든다면, 예를 들어 자격 증명을 일괄 확인하는 감사 도구나 컴플라이언스 보고서라면 Assertion에서 멈추지 말고 체인 전체를 따라가세요. Assertion은 badge URL(BadgeClass: 이름, 설명, 기준)을 참조하고, BadgeClass는 다시 issuer URL(조직 프로필)을 참조해요. 엄격한 검증기는 세 가지를 모두 확인해 내부적으로 일관된지 점검해요. 배지가 주장하는 발급자에게 실제로 속해 있는지, Assertion이 주장하는 배지를 실제로 참조하는지를 보는 거예요. 최상위 Assertion이 200을 반환하는지만 확인하는 것으로는 부족해요. 단일 자격 증명의 상태 위젯에는 이 정도 깊이까지는 필요 없지만, 파트너나 감사자가 “검증을 실제로 얼마나 깊이 하나요?”라고 물을 때를 대비해 이런 구조가 있다는 것은 알아 둘 만해요.

피해야 할 흔한 실수

  • HTTP 상태만 확인하기. 앞서 설명했듯이 취소된 Assertion도 조회돼요. 상태 코드만으로 검증되었다고 판단하지 말고 항상 assertion.revoked를 읽으세요.
  • 공유 컴포넌트에 수여 ID를 하드코딩하기. 위젯을 여러 수령자의 페이지에서 재사용할 예정이라면, 수여 ID를 스크립트에 박아 넣지 말고 매개변수(data 속성, 쿼리 문자열, prop)로 받으세요.
  • 네트워크 오류를 조용히 삼키기. 아무것도 기록하지 않는 try/catch가 실제로 없는 배지와 똑같은 “찾을 수 없음” 문구를 보여 주면 나중에 디버깅이 정말 힘들어져요. 적어도 로그에서는 “네트워크 호출이 실패했다”와 “API가 아니라고 답했다”를 구분하세요.
  • 클라이언트 측 앱에서 렌더링할 때마다 다시 검증하기. 위젯이 자주 다시 렌더링되는 React나 Vue 컴포넌트 안에 있다면, 렌더링마다가 아니라 마운트당 한 번(또는 위 캐싱 섹션의 TTL 구간당 한 번)만 검증하세요.

크레딧이 드나요? 무료 플랜에서도 되나요?

아니요, 크레딧은 들지 않아요. 검증 엔드포인트는 발급을 관리하는 크레딧 체계 밖에 있어요. 배지가 유효한지 확인하는 데는 발급자에게 비용이 들지 않고, 호출하는 방문자에게 계정, API 키, 어떤 플랜도 필요 없어요. 배지가 Free, Starter, Pro 중 어느 플랜에서 발급되었든 마찬가지예요. 검증은 자격 증명 자체의 속성이지 발급자의 구독에 따라 제한되는 기능이 아니에요. 그래서 트래픽을 예측하기 어려운 공개 페이지(채용 페이지, 파트너 디렉터리)에도 계정에 묶인 속도 제한을 걱정하지 않고 이 위젯을 안심하고 둘 수 있어요. 인증이 필요한 API 경로가 아니라 읽기 전용 공개 엔드포인트를 호출하는 것이니까요.

한 가지 분명히 해 둘 점이 있어요. 엔드포인트가 반환하는 Assertion JSON에는 수령자의 원본 이메일 주소가 들어 있지 않아요. 수령자 이메일은 백엔드에 해시되어 저장되기 때문에 공개 검증 응답에서 개인 연락처를 긁어 갈 수 없어요. 돌려받는 정보는 자격 증명(배지, 발급자, 날짜, 취소 상태)을 확인하기에 충분하면서도, 수여 ID를 순회하는 누군가를 위한 리드 수집 도구가 되지는 않아요.

첫 번째 검증 가능한 자격 증명을 발급해 볼까요?

badges.ninja에서 무료로 시작하기 — 비주얼 디자이너, 공개 검증 페이지, PDF 인증서, Open Badge v2.0 출력. 신용카드는 필요 없어요.

Assertion의 모든 필드에 대한 주석이 달린 전체 구조는 JSON-LD 필드 레퍼런스를 참고하세요. 그리고 자격 증명을 검증만 하는 것이 아니라 프로그래밍 방식으로 발급한다면, API 퀵스타트에서 이 엔드포인트의 출력이 만들어지는 인증과 수여 생성 호출을 다뤄요.

Nacho Coll

작성자 소개

Founder & Engineer at Badges Ninja

Nacho founded Badges Ninja to make issuing verifiable digital credentials as simple as a single API call — Open Badge v2.0 badges and certificates, minted, hosted, and verifiable without standing up your own issuer infrastructure. Writes about the Open Badges spec, credential verification, and running a credentialing platform serverless on AWS, from the operator side of the wire.

블로그로 돌아가기

관련 글