매입 채무 처리에서 흔한 실수: 스캔한 인보이스에서 OCR이 읽은 IBAN이 DE89 3704 0044 0532 O130 00으로 나온다. 끝에서 두 번째 묶음의 영문자 O는 놓치기 쉽고, 그대로 송금하면 은행에서 거부되거나 반환된다.
mod-97 한 번이면 잡힌다. IBAN 표준은 바로 이 시나리오를 위해 설계되었다.
IBAN은 정확히 무엇인가
IBAN(International Bank Account Number)은 ISO 13616으로 정의되고, SWIFT가 IBAN Registry를 통해 운영·유지한다. 각 IBAN은 하나의 문자열에 네 가지를 압축한다:
| 부분 | 길이 | 출처 |
|---|---|---|
| 국가 코드 | 2자 | ISO 3166-1 alpha-2 |
| 체크 디지트 | 2자리 숫자 | 나머지 부분의 mod-97 |
| BBAN(Basic Bank Account Number) | 11~30자 | 국가 표준 |
| 전체 길이 | 15~34자 | 국가별 고정 |
가장 짧은 건 노르웨이 15자, 세인트루시아와 몰타가 32·31자. 체크 디지트는 국가 코드 바로 뒤에 자리한다 — 그래서 영국 IBAN은 GB82 WEST… 모양이다. GB가 국가, 82가 체크섬, WEST부터가 BBAN이다.
전 세계적으로 개인에게 IBAN을 발급하는 단일 기관은 없다. 각 국가가 표준을 채택한 뒤, 자국의 BBAN 구조(은행 코드 위치, 계좌 번호 길이, 각 자리의 문자 종류)를 정의하고 SWIFT IBAN Registry에 게시한다. 이 구조가 모든 검증기가 구현해야 할 계약서다.
mod-97이 어떻게 작동하고, 왜 영리한가
체크 디지트는 무작위가 아니다. 문자를 숫자로 매핑한 IBAN 전체를 97로 나누면 나머지가 1이 되도록 설계되었다. 알고리즘은 5단계이고 브라우저에서 마이크로초 단위로 끝난다:
- 정규화. 공백을 제거하고 전부 대문자로.
- 회전. 처음 4자(국가 코드 + 체크 디지트)를 끝으로 보낸다.
- 문자 치환. 각 문자를 2자리 숫자로:
A → 10,B → 11, …,Z → 35. - 나머지. 결과 숫자 문자열을 하나의 큰 정수로 보고
n mod 97계산. - 비교. 나머지가
1이면 체크섬 통과.
function isValidIban(raw) {
const s = raw.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(s)) return false;
const rearranged = s.slice(4) + s.slice(0, 4);
const numeric = [...rearranged]
.map(c => (c >= 'A' ? (c.charCodeAt(0) - 55).toString() : c))
.join('');
// BigInt는 긴 IBAN(러시아는 33자)에서 53비트 부동소수 한계를 넘는 것을 방지한다.
return BigInt(numeric) % 97n === 1n;
}
왜 97인가? 이유는 셋:
- 소수. 소수 모듈로를 쓰면 「임의의 한 자리 오류가 나머지를 바꾸는」 확률이 극대화된다 — 더 작은 인수가 오류를 흡수할 수 없기 때문.
- 두 자리. 97로 나눈 나머지는 항상 두 자리에 들어가고, 이는 표준이 체크 디지트에 할당한 길이와 정확히 일치한다.
- 흔한 오타를 폭넓게 검출. IBAN이 쓰는 ISO/IEC 7064 MOD 97-10 방식은 한 글자 치환과 인접한 두 글자 자리바꿈을 모두 검출하도록 설계되었다.
2단계의 회전은 체크 디지트를 ISO/IEC 7064가 기대하는 끝으로 옮긴다. 국가 코드도 나누는 수에 포함되므로 DE를 FR로 바꾸거나 체크 디지트를 잘못 쳐도 계산이 맞지 않게 된다.
BBAN은 국가별 창의력이 발휘되는 자리
처음 4자 뒤의 BBAN 포맷은 각 국가가 정한다. SWIFT IBAN Registry가 모든 국가의 레이아웃을 코드화한다. 얼마나 다양한지 맛만 본다면:
| 국가 | 길이 | BBAN 구조 |
|---|---|---|
| 노르웨이(NO) | 15 | 4자리 은행 + 6자리 계좌 + 1자리 국가 체크 디지트 |
| 벨기에(BE) | 16 | 3자리 은행 + 7자리 계좌 + 2자리 국가 체크 디지트 |
| 네덜란드(NL) | 18 | 4글자 은행(ABNA, RABO, INGB…) + 10자리 계좌 |
| 독일(DE) | 22 | 8자리 은행 + 10자리 계좌(지점 없음) |
| 영국(GB) | 22 | 4글자 은행 + 6자리 sort code + 8자리 계좌 |
| 프랑스(FR) | 27 | 5자리 은행 + 5자리 지점 + 11자 계좌 + 2자리 국가 체크 디지트 |
| 이탈리아(IT) | 27 | 1글자 국가 체크 + 5자리 은행 + 5자리 지점 + 12자 계좌 |
| 사우디아라비아(SA) | 24 | 2자리 은행 + 18자 계좌 |
| 브라질(BR) | 29 | 8자리 은행 + 5자리 지점 + 10자리 계좌 + 1글자 계좌 유형 + 1자 보유자 |
| 모리셔스(MU) | 30 | 6자리 은행 코드(영문 4 + 숫자 2) + 2자리 지점 + 12자리 계좌 + 000 + 영문 3자 통화 |
두 패턴이 눈에 띈다:
라틴어권 그룹(FR, IT, BE, MC, MR, PT, SM, ST, TN) 은 모두 BBAN 안에 IBAN 수준의 mod-97과 별개인 국내 체크 디지트를 넣는다. 프랑스의 RIB key는 은행 + 지점 + 계좌에 대한 별도의 mod-97이고, 이탈리아 BBAN의 첫 글자는 위치 가중 표로 계산하는 CIN 문자(A–Z)다. IBAN mod-97은 한 글자 치환을 모두 잡으므로 국내 체크가 쓸모 있는 곳은 따로 있다. 국내 계좌 번호를 입력하고 시스템이 IBAN을 만들어 주는 경우, 한 자리를 잘못 쳐도 체크 디지트가 맞는 IBAN이 생기며 이를 막는 것은 국내 체크뿐이다.
영어권 그룹(GB, IE, MT) 은 문자로 은행 코드를 표기한다. 이 문자들은 은행 약칭의 머리글자(WEST, BARC, LOYD, HSBC)여서, GB IBAN을 파싱하면 독일보다 가독성 높은 정보가 나온다 — 표 조회 없이도 BARC가 Barclays임을 추측할 수 있다.
실제 IBAN 오류는 어디서 오는가
IBAN이 틀렸을 때의 비용은 금액 자체보다 — 은행이 반송하기까지 몇 주가 걸리기도 한다 — 반송 수수료(건당 5~30 EUR), 운영 측 후속 조치 시간, 협력사 관계의 마찰에 더 크다. 방어할 만한 유형들:
OCR 오인식
스캔된 인보이스의 실패 패턴은 예측 가능하다. 가장 흔한 문자 치환:
| 오 | 정 | 이유 |
|---|---|---|
O | 0 | 문자 O와 숫자 0이 저품질 폰트에서 거의 같은 형태 |
I / l / 1 | 1 | 산세리프 폰트에서 글자 모양이 겹친다 |
S | 5 | 이탤릭이나 장식체에서 흔한 오인식 |
B | 8 | 압축 출력된 은행 명세서에서 흔하다 |
Z | 2 | 유럽 대륙 수기 관습 |
모두 한 글자 치환이므로 mod-97이 전부 잡는다. ZeroTool 검증기는 “체크섬 실패 (mod 97 ≠ 1). 체크 디지트 또는 계좌 본문이 잘못되었을 수 있습니다.”라고 알려 주지만 어느 글자가 틀렸는지는 말하지 않는다. 위 표의 비슷하게 생긴 글자를 찾아보자.
공백과 너비 0 문자
PDF나 Outlook에서 복사·붙여넣기는 비분할 공백(U+00A0), 너비 0 결합자(U+200D), 가끔 BOM까지 끌어들인다. ASCII 공백만 지우는 스크립트는 여기서 실패한다. ZeroTool의 정규화는 입력을 대문자로 바꾼 뒤 A–Z, 0–9 외의 모든 문자를 지우고 검증한다. ISO 13616은 구분자 없는 전자 형식과 네 자리마다 공백으로 나누는 인쇄 형식을 정하므로 공백 제거는 예상된 처리다. 그 밖의 문자 제거는 편의상의 처리라 섞여 들어온 기호도 함께 가려진다.
선행 0 손실
전체 IBAN은 영문자로 시작하므로 스프레드시트는 이를 텍스트로 유지한다. 문제는 나눠 둔 부분에서 생긴다. 별도 열에 넣은 국내 계좌 번호와 은행 코드는 Excel이 숫자로 바꾸면서 앞자리 0을 잃고(0123이 123으로), 그 열로 다시 조립한 IBAN은 길이가 틀어진다. 모든 부분을 텍스트로 저장하자.
국가 코드 혼동
DE(독일)와 DK(덴마크)는 한 글자 차이이고 IBAN 길이는 4자리 차이다. 독일 IBAN 본문을 덴마크 접두사 뒤에 붙이면 길이 검사에서 바로 실패한다. 검증기는 “Denmark IBAN 길이 오류: 기대 18, 실제 22.”라는 구체적인 오류를 낸다.
소문자
규격은 대문자 전용이다. 일부 은행은 가독성을 위해 대소문자 혼용으로 인쇄하고, 이메일은 IBAN을 하이퍼링크로 감싸면서 텍스트를 소문자화하기도 한다. 정규화가 처리하지만, 「이건 통과해야 하는데」 류의 제보를 좇을 때 머릿속에 두면 좋다.
IBAN 검증기가 알려주지 않는 것
mod-97이 통과하면 검증기가 「정답」이라고 부르고 싶은 유혹이 든다. 그렇지 않다. 세 가지는 항상 사정거리 밖이다:
- 계좌가 존재하는지. 은행이 지난주에 해지했을 수 있다. mod-97은 알 길이 없다.
- 계좌가 정상 상태인지. 동결·휴면·차단 — 어느 쪽이든 송금 지시는 받아내고 결제 단계에서 반송한다.
- IBAN과 명의가 일치하는지. EU의 PSD2와 SEPA 스킴은 이 검증을 수취 은행에 떠넘긴다. 송금자인 당신은 검사하지 않는다. 영국의 Confirmation of Payee와 유럽에서 2024~2025년 단계적으로 시행되는 Verification of Payee가 바로 이를 겨냥하지만, 그것들은 은행 계층의 일이지 당신의 폼 검증의 일이 아니다.
올바른 마음가짐: IBAN 검증은 필수적인 1차 필터이지 충분조건이 아니다. 클라이언트 측에서 저렴하게 오타를 잡고, 나머지는 은행 API(혹은 실제 송금 시도)에 맡긴다.
체크아웃 폼에 검증 통합하기
전형적인 패턴: 입력하는 동안 검증하고, 성공 시 초록 체크, 실패 시 인라인 오류를 띄우고, mod-97을 통과하지 못하면 제출을 막는다. 바닐라 JS 골격:
<label for="iban">IBAN</label>
<input
id="iban"
type="text"
inputmode="text"
autocapitalize="characters"
spellcheck="false"
autocomplete="off"
aria-describedby="iban-msg"
/>
<p id="iban-msg" role="status" aria-live="polite"></p>
<script>
const input = document.getElementById('iban');
const msg = document.getElementById('iban-msg');
input.addEventListener('input', () => {
const result = isValidIban(input.value); // 위의 함수
msg.textContent = result ? 'Valid IBAN.' : 'IBAN checksum invalid.';
msg.className = result ? 'ok' : 'err';
});
</script>
운영에서 효과를 보는 세 가지 디테일:
autocapitalize="characters"는 iOS 자동 수정이gB82…를 보내는 것을 막는다 — 그러면 format regex에서 바로 떨어진다.aria-live="polite"는 스크린 리더 사용자에게 포커스를 빼앗지 않으면서 검증 결과를 들려준다.- 제출 시점이 아니라 입력 중에 검증한다. 300ms 제출 라운드트립을 기다리지 않고
n번째 키 입력에서 오류를 잡는 것 — 그것이 전체 요점이다.
성공 시 유료 IBAN API(국가 체크 디지트 + 계좌 존재 조회 포함)도 호출한다면 디바운스를 더하자: mod-97은 키 입력마다 돌려도 충분히 빠르지만 API는 300~500ms 디바운스가 무난하다.
무료 옵션 비교
IBAN 검증을 둘 곳은 세 군데다: 클라이언트 라이브러리, SaaS API, 브라우저 도구.
| 옵션 | 커버리지 | 비용 | 프라이버시 |
|---|---|---|---|
iban (npm) | mod-97 + 길이 | 무료 | 클라이언트 사이드 |
| ZeroTool IBAN Validator & Parser | mod-97 + 길이 + BBAN 분해 | 무료 | 클라이언트 사이드 |
| iban.com REST API | mod-97 + 은행 조회 + IBAN→BIC | 사용량 과금 | 서버 간 |
| openiban.com REST API | mod-97 + 길이 | 무료, 레이트 제한 | 서버 간 |
정답은 「당신이 풀고 싶은 문제」에 달려 있다. 개인 프로젝트의 무료 폼이라면 — npm 라이브러리든 ZeroTool 페이지든 충분하다. 실제 돈을 움직이는 결제 처리라면 — API 단계의 국가 체크 디지트 커버리지와 IBAN→BIC 매핑은, 로컬 검증이 놓친 오류를 한 번 잡아내는 순간 본전을 뽑는다.
ZeroTool의 도구는 「검수」 사용 사례에 최적화되어 있다 — IBAN을 들고 있는데 어떻게 파싱되는지 보고 싶고, 누구에게도 보내고 싶지 않은 상황. 클라이언트 측에 임베드하는 것과 동일한 mod-97 엔진에, 표준 npm 라이브러리가 늘 노출하지는 않는 국가별 BBAN 분해를 더한 것이다.
범위 밖(과 그 이유)
ZeroTool에는 다음이 없다:
- IBAN 생성기. 국가와 은행 코드만 있으면 수학적으로 체크 디지트가 올바른 「형식상 유효한 IBAN」을 만들어낼 수 있다. 이 기능을 공개하지 않는 이유는 계좌 사칭 사기의 진입 장벽을 낮추기 때문이다. 진짜 IBAN은 은행에서 발급된다. 그 외 경로로 IBAN을 생성해야 할 개발자의 정당한 필요는 거의 없다.
- BIC / SWIFT 조회. 은행 코드를 은행 이름으로 매핑하려면 월 단위로 갱신되는 라이선스 데이터베이스를 들여와 재배포해야 하고 운영 부담이 크다. BIC 조회는 SWIFT 공식 디렉터리나 국가별 중앙은행 등기부가 권위 있는 출처다.
- SEPA 입금 QR 코드. 유럽결제이사회의 EPC069-12 포맷은 은행 앱의 송금 화면을 사전 입력해 주는 QR을 만든다. ZeroTool의 QR 코드 생성기로 QR은 만들 수 있지만 EPC069-12 페이로드는 직접 조립해야 한다 — 검증기의 일은 IBAN이지 결제 지시가 아니다.
이것들은 의도된 공백이다. 모든 관련 기능을 하나의 도구에 욱여넣으면 도구의 목적과 신뢰 모델이 둘 다 옅어진다.
더 읽을거리
- ISO 13616-1:2020 — 공식 표준, 유료 다운로드지만 요약은 무료로 볼 수 있다.
- SWIFT IBAN Registry — 모든 국가의 BBAN 구조가 담긴 공개 PDF, 정기 갱신.
- European Payments Council — SEPA Credit Transfer rulebook — IBAN을 제출한 뒤 은행이 실제로 무엇을 하는지.
- Wikipedia: International Bank Account Number — 표준의 읽기 좋은 역사와 국가별 채택 타임라인.
ZeroTool 안에서는 IBAN 검증기가 URL 파서(결제 리다이렉트 URL의 토큰 해석), Cookie 파서(결제 포털이 발급하는 세션 쿠키 디버깅), QR 코드 생성기(검증된 IBAN으로 SEPA EPC069-12 QR 생성)와 자연스럽게 짝을 이룬다.
다음에 OCR이 막 읽어낸 IBAN이 적힌 인보이스가 오면, 결제 화면에 붙이기 전에 먼저 검증기에 붙여 보라. mod-97이 마이크로초 안에 「이 송금이 반송될지」를 알려준다. 2분의 실사로 25 EUR 반송 수수료를 한 번만 막아도 즉시 본전이 뽑히고, 협력사 관계에 돌아오는 무형의 이득은 그보다 훨씬 크다.