월요일 아침, 협력사 인보이스가 스캔 PDF로 받은편지함에 떨어졌고, OCR이 읽어낸 IBAN은 DE89 3704 0044 0532 O130 00이다. 끝에서 두 번째 그룹의 알파벳 O, 보이는가? 하마터면 못 보고 넘어갈 뻔했다. 지난번 미지급 정산 배치에서 12,500 EUR짜리 송금이 같은 함정으로 반송됐다 — 프린터와 OCR 사이 어디선가 0이 O로 둔갑했고 — 은행은 반송 수수료 25 EUR을 떼어 갔다.
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로 나눈 나머지는 항상 두 자리에 들어가고, 이는 표준이 체크 디지트에 할당한 길이와 정확히 일치한다.
- 흔한 오타에 대한 높은 커버리지. 모든 단일 문자 오류와 인접 두 자리의 자리바꿈을 압도적인 확률로 잡아낸다 — 형식 분석에 따르면 종합 커버리지는 99.5%를 넘는다.
가장 영리한 부분은 2단계의 회전이다. 회전이 없다면 BBAN 내부 오류만 잡을 수 있고, 국가 코드는 모듈로에 「무시」된다. 국가 코드와 체크 디지트를 끝으로 옮기는 순간 그것들도 나눗셈에 참여하는 정수의 일부가 되고, 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 | 4글자 은행 + 4자리 지점 + 15자리 계좌 + 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만 한다면 대부분의 오류는 잡히지만, BBAN 내부의 일부 단일 문자 오류는 IBAN 레벨을 통과해 은행에서 국가 체크를 돌릴 때 비로소 실패한다. 「100% 정확」을 표방하는 검증 라이브러리 대부분이 거짓말인 이유다 — 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의 검증기는 어떤 종류의 치환이 일어났을지 단서를 준다 — 실패 시 “Checksum failed, check digits or account body are wrong.” 메시지가 뜨므로, 거기서부터 시각적으로 유사한 문자 쌍을 훑어보면 된다.
공백과 너비 0 문자
PDF나 Outlook에서 복사·붙여넣기는 비분할 공백(U+00A0), 너비 0 결합자(U+200D), 가끔 BOM까지 끌어들인다. 자작 IBAN 체크 스크립트 대부분은 ASCII 공백만 제거하고 여기서 막힌다. ZeroTool의 정규화는 ISO 13616 그대로 — AZ와 09가 아닌 모든 코드 포인트를 검증 전에 제거한다. 표준이 「사람이 읽는 형식에서의 비영숫자는 모두 표시용」이라고 명시하기 때문이다.
선행 0 손실
스프레드시트는 IBAN의 적이다. Excel은 셀을 숫자로 해석해 0123을 123으로 「친절하게」 바꾼다. IBAN이 표 계산을 거쳐 결제 시스템에 돌아왔을 때 길이가 맞지 않고 mod-97이 실패한다. 본질적 해결은 구조적이다 — IBAN 열을 텍스트로 저장하고 절대 숫자로 파싱하지 말 것 — 하지만 검증기는 증상을 잡을 수 있다.
국가 코드 혼동
DE(독일)와 DK(덴마크)는 대부분의 키보드에서 옆자리이며 IBAN 길이는 4자 차이가 난다. 누군가 독일 IBAN 본체를 덴마크 접두사 밑에 붙여넣으면 길이 검사가 즉시 실패한다. 검증기는 구체적인 오류로 응답한다: 「Wrong length for Denmark: expected 18, got 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, 약 28KB) | < 1ms | mod-97 + 길이 | 무료 | 클라이언트 사이드 |
| ZeroTool IBAN Validator & Parser | < 1ms | mod-97 + 길이 + BBAN 분해 | 무료 | 클라이언트 사이드 |
| iban.com REST API | ~150ms | mod-97 + 은행 조회 + IBAN→BIC | 사용량 과금 | 서버 간 |
| openiban.com REST API | ~200ms | 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 반송 수수료를 한 번만 막아도 즉시 본전이 뽑히고, 협력사 관계에 돌아오는 무형의 이득은 그보다 훨씬 크다.