QR코드는 밝고 어두운 모듈이 늘어선 그림이고, 안에 들어 있는 것은 링크가 아니라 바이트열입니다. 읽는 쪽은 이미지에서 기호를 찾고, 기울기와 배율을 보정하고, 정해진 순서로 모듈을 읽고, 마스크를 벗기고, Reed–Solomon 오류 정정 코드워드를 검산한 뒤 바이트열을 돌려줍니다. 이 글에 나오는 실패는 대부분 이 단계 중 하나에서 멈춘 결과입니다.
이 사이트의 QR코드 스캔은 ZXing C++를 WebAssembly로 만든 zxing-wasm으로 이 과정을 페이지 안에서 실행합니다. 아래에서는 기호의 구성, 오류 정정별 용량, 문자 인코딩이 정해지는 방식, Wi-Fi와 vCard 내용을 읽는 방법, 실패하는 조건을 차례로 봅니다.
기호의 구성: 파인더, 타이밍, 얼라인먼트, 여백
기호 구조는 ISO/IEC 18004:2006이 정합니다. 데이터 외에도 고정된 도형이 있습니다.
- 파인더 패턴(위치 검출 패턴). 왼쪽 위, 오른쪽 위, 왼쪽 아래의 7×7 사각형입니다. 읽는 쪽은 이것으로 기호의 위치와 회전을 찾습니다. 오른쪽 아래에는 없고, 이 비대칭으로 방향을 판단합니다.
- 타이밍 패턴. 6번째 행과 6번째 열에 밝음과 어두움이 번갈아 놓인 줄입니다. 파인더 패턴 사이의 모듈 수를 세는 데 씁니다.
- 얼라인먼트 패턴(정렬 패턴). 버전 2부터 기호 안쪽에 놓이는 5×5 사각형입니다. 버전 1에는 없습니다. 비스듬히 찍은 사진의 왜곡을 보정합니다.
- 형식 정보. 오류 정정 레벨과 마스크 종류를 파인더 패턴 근처에 두 번 기록합니다. 한쪽이 손상되어도 읽을 수 있게 하기 위해서입니다.
- 데이터와 오류 정정 코드워드. 나머지 영역에 지그재그 순서로 채우고, 마스크 패턴과 XOR합니다.
바깥에는 **여백(콰이어트 존)**이 필요합니다. 네 변 모두 4모듈 너비의 밝은 여백입니다(ISO/IEC 18004:2006 §5.3.7). 여백이 없으면 옆의 어두운 글자나 사진 테두리를 기호의 일부로 잘못 볼 수 있습니다. QR코드를 글자나 사진 가장자리에 바짝 붙였을 때 읽히지 않고, 흰 여백을 두면 읽히는 이유입니다.
크기는 버전으로 셉니다. 버전 1은 21×21 모듈이고, 한 버전 올라갈 때마다 한 변이 4모듈씩 늘어 버전 40에서 177×177이 됩니다.
| 버전 | 모듈 수 | 이 사이트 생성기에서의 예 |
|---|---|---|
| 1 | 21×21 | 오류 정정 L의 짧은 대문자 URL |
| 2 | 25×25 | 오류 정정 M의 https://zerotool.dev |
| 5 | 37×37 | 같은 연락처의 MECARD 형식 |
| 8 | 49×49 | 같은 연락처의 vCard 3.0 형식 |
| 40 | 177×177 | 오류 정정 L의 최대 용량 |
네 가지 모드와 문자당 비용
QR코드는 글자를 그대로 넣지 않고, 내용을 여러 세그먼트로 나눈 뒤 세그먼트마다 모드를 고릅니다.
| 모드 | 쓸 수 있는 문자 | 비용 |
|---|---|---|
| 숫자 | 0–9 | 숫자 3개당 10비트 |
| 영숫자 | 0–9, A–Z, 공백과 $ % * + - . / : | 문자 2개당 11비트 |
| 바이트 | 임의의 바이트 | 바이트당 8비트 |
| 한자 | JIS X 0208 문자 | 문자당 13비트 |
한 기호 안에서 세그먼트 경계마다 모드를 바꿀 수 있습니다(ISO/IEC 18004:2006 §6.3.6). 호스트는 대문자, 경로는 소문자인 URL이 영숫자와 바이트 세그먼트를 함께 쓰는 이유입니다. 생성기는 전체 비트 수가 가장 적은 분할을 고르고 상태 줄에 표시합니다.
QR코드 만들기의 실제 결과 세 가지를 보면 차이가 드러납니다.
https://zerotool.dev를 오류 정정 M으로 만들면 **버전 2, 25×25 모듈, 데이터 224비트 중 172비트(77%)**입니다. 바이트 모드로 20바이트입니다.HTTPS://ZEROTOOL.DEV는 오류 정정 L에서 버전 1, 21×21입니다. 대문자는 영숫자 모드에 들어가기 때문입니다. 소문자로 되돌리면 버전 2가 필요합니다. 스킴과 호스트는 대소문자를 구분하지 않으므로(RFC 3986 §3.1, §3.2.2) 경로의 첫/앞까지는 이 치환이 안전합니다. 경로와 쿼리 문자열은 대소문자를 구분할 수 있습니다.- 2,400자 평문을 오류 정정 M으로 넣으면 19,220 데이터 비트가 필요하고, 버전 40의 M은 18,672비트만 담아 69바이트가 넘칩니다. 같은 내용은 L이면 들어갑니다. 생성기는 맞추려고 내용을 자르지 않습니다.
한국어, 중국어, 일본어는 보통 UTF-8에서 한 글자에 3바이트, 기본 다국어 평면 밖의 문자는 4바이트입니다. 바이트 모드의 2,953은 글자 수가 아니라 바이트 수입니다.
버전, 오류 정정, 용량
오류 정정은 네 단계이고, 잃어도 복원할 수 있는 코드워드 비율로 설명합니다.
| 레벨 | 복원 가능한 코드워드, 약 | 맞는 용도 |
|---|---|---|
| L | 7% | 깨끗한 화면, 짧은 링크 |
| M | 15% | 일반적인 기본값 |
| Q | 25% | 손에 닿거나 긁히는 표면 |
| H | 30% | 작은 인쇄, 로고 포함, 산업용 라벨 |
코드워드는 GF(256) 위의 Reed–Solomon 부호로 계산하고, 블록으로 나누어 교차 배치합니다. 긁힌 자국 하나가 블록 전체를 망가뜨리지 않게 하기 위해서입니다. 오류 정정은 손상된 코드워드를 되돌리는 장치이고 암호화가 아닙니다. 레벨을 넘는 손상은 복원할 수 없습니다.
레벨을 올리면 오류 정정 코드워드가 늘어나 같은 내용이라도 더 큰 버전이 필요합니다. 기호가 촘촘해지면 인쇄하거나 촬영할 때 모듈당 픽셀 수도 늘려야 하므로 이점이 상쇄될 수 있습니다. 먼저 표면에 맞춰 레벨을 정하고, 그다음 실제 인쇄 크기를 확인합니다.
버전 40, 레벨 L의 상한은 다음과 같습니다.
| 모드 | 용량 |
|---|---|
| 숫자 | 7,089자리 |
| 영숫자 | 4,296자 |
| 바이트 | 2,953바이트 |
| 한자 | 1,817자 |
한 모드만 쓸 때의 최댓값입니다. 한글과 영문을 섞으면 위의 세그먼트 비용으로 계산하므로 실제로 들어가는 양은 줄어듭니다.
ECI와 문자 인코딩
QR코드가 저장하는 것은 바이트열입니다. 문자 인코딩은 ECI(Extended Channel Interpretation) 헤더로 선언할 수 있습니다(ISO/IEC 18004). 대표적인 할당은 ECI 20이 Shift_JIS, ECI 26이 UTF-8입니다. 읽는 쪽은 Big5, GB2312, GBK, GB18030, EUC-KR, ISO-8859 계열도 다룹니다.
대부분의 생성기는 ECI를 쓰지 않습니다. 2006년판 표준은 바이트 모드의 기본을 ISO/IEC 8859-1로 정하지만, 실제로는 UTF-8 바이트열을 선언 없이 넣는 도구가 많습니다. 읽는 쪽은 다음 순서로 판단합니다.
- ECI가 있으면 선언된 인코딩으로 읽고
UTF-8(ECI 26)처럼 표시합니다. - ECI가 없고 UTF-8로 유효하면 UTF-8로 읽습니다.
- 아니면 페이지 언어의 예전 인코딩을 먼저 시도합니다. 한국어 페이지는 EUC-KR, 중국어 페이지는 GB18030, 일본어 페이지는 Shift_JIS입니다.
- 그래도 안 되면 ZXing의 추정 규칙을 따릅니다. BOM이 있으면 UTF-16, 다음 Shift_JIS, 마지막 ISO-8859-1입니다.
추정한 결과에는 '추정'이 붙습니다. '문자 인코딩' 메뉴에서 같은 바이트열을 UTF-8, Shift_JIS, GB18030/GBK, Big5, EUC-KR, ISO-8859-1, UTF-16BE, UTF-16LE로 다시 읽을 수 있습니다. 어느 인코딩으로도 텍스트가 되지 않는 바이트열은 '바이너리'로 표시하고 16진수로 펼칩니다.
이 사이트에서 확인한 예입니다.
- EUC-KR 바이트로 만든 「회의실 3층 예약 확인」은 이 페이지에서 바로 읽혔고 'EUC-KR(추정)'으로 표시됐습니다.
- GBK 바이트의
你好,世界。二维码测试는 영어 페이지에서 ISO-8859-1로 추정되었고(ZXing과 같은 동작), 메뉴에서 GB18030을 고르자 정상으로 돌아왔습니다.
한국어 실무에서는 오래된 업무 시스템이 EUC-KR로 QR코드를 만들고 ECI를 쓰지 않는 경우가 있습니다. 휴대폰 앱이 UTF-8로 읽으면 글자가 깨집니다. 먼저 이 페이지에서 인코딩을 골라 원문을 확인하고, 필요하면 UTF-8로 다시 만드세요.
브라우저가 이미지를 읽는 과정
페이지는 zxing-wasm 3.1.4를 씁니다. ZXing C++를 WebAssembly로 만든 것이고, 스크립트와 모듈은 이 사이트의 /vendor에서 읽으며 CDN을 쓰지 않습니다. 처음 한 번만 내려받고 gzip 기준 약 420KB, 압축 전 합계 약 967KiB입니다. 전송량은 압축과 캐시에 따라 다릅니다.
디코딩은 페이지 안에서 끝납니다. 파일, 붙여 넣은 캡처, 카메라 프레임의 이미지 바이트를 이 사이트로 업로드하지 않습니다. 다만 이것은 디코딩 경로의 이야기입니다. 페이지는 분석 스크립트와 광고도 불러오고, URL 모드에서는 입력한 주소로 이미지를 가져옵니다. 페이지 밖으로 나가는 요청은 아래 '개인정보' 절에 나눠 적었습니다.
읽는 쪽의 동작은 다음과 같습니다.
- 한 이미지에서 최대 16개의 코드를 읽고 왼쪽 위부터 행 순서로 번호를 붙입니다. 미리보기에 그리는 번호는 결과 카드와 일치합니다.
- QR, Micro QR, rMQR을 읽습니다.
- 흑백 반전, 회전, 여백이 거의 없는 코드도 시도합니다.
- 원래 해상도로 최대 16,777,216픽셀까지 처리합니다. 더 큰 이미지는 줄이고 상태 줄에 알립니다. 브라우저 자체의 캔버스 제한은 기기와 버전에 따라 다릅니다.
- 이미지는 페이지 안에 둡니다. 카메라는
getUserMedia를 쓰고 후면 카메라를 우선하며 프레임을 기기에서 디코딩합니다.
2026-09-30 실측에서 33모듈 코드는 45×45픽셀에서 읽히지 않았고 60×60에서 읽혔습니다. 모듈당 약 1.5 이미지 픽셀이 기준입니다. 4000×3000 휴대폰 사진에 작은 코드가 있으면 데스크톱 Chromium에서 약 1.2초 걸렸습니다.
Wi-Fi, 연락처, 메일, SMS, 위치 정보의 해석
디코딩한 바이트열은 텍스트이고, 앞 몇 글자가 앱의 처리 방식을 정합니다. 읽는 쪽도 같은 접두사로 항목을 나누고 한 줄 문자열로 두지 않습니다.
| 접두사 | 해석 | 근거 |
|---|---|---|
http:// 또는 https:// | URL. 실제 호스트와 경고 표시 | — |
WIFI: | 네트워크 이름, 비밀번호, 암호화 방식, 숨김 여부 | ZXing: Barcode Contents |
BEGIN:VCARD | vCard 연락처 | RFC 2426(1998년 9월) |
MECARD: | NTT 도코모의 간단한 연락처 형식 | 도코모 항목표 |
mailto: | 받는 사람, 참조, 제목, 본문 | RFC 6068(2010년 10월) |
tel: | 전화번호 | RFC 3966(2004년 12월) |
sms: 또는 SMSTO: | 번호와 본문 | RFC 5724(2010년 1월). SMSTO:는 ZXing 형식 |
geo: | 위도, 경도, 고도, 지도 링크 | RFC 5870(2010년 6월) |
그 밖의 scheme: | 텍스트만. 앱이 필요하다고 표시 | — |
Wi-Fi 내용은 한 줄 텍스트입니다. WIFI:T:WPA;S:카페 손님용;P:비밀번호;;처럼 씁니다. ZXing은 \, ;, ,, ", :를 백슬래시로 이스케이프하고, Android 해석 코드가 그 백슬래시를 다시 제거합니다. Wi-Fi Alliance는 같은 URI를 WPA3 Specification v3.5 7절에 넣었고, 비밀번호가 있는 네트워크는 모두 T:WPA, R:1은 WPA3-Personal 전용을 뜻합니다. 와이파이 QR코드 만들기는 이 형식으로 만들고 그리기 전에 항목 길이를 검사합니다. 읽는 쪽은 항목을 나눌 뿐 대신 연결하지 않습니다.
연락처는 텍스트 덩어리입니다. RFC 2426의 이스케이프 규칙에 따라 읽는 쪽은 이름, 회사, 전화, 메일, 주소, URL, 메모를 항목으로 보여 줍니다. FN:김민준, ORG:예시 주식회사, TEL;TYPE=CELL:010-1234-5678이 든 vCard는 이름·회사·전화 세 줄이 됩니다. MECARD는 짧은 대신 회사명과 직책 항목이 없습니다.
mailto:, tel:, sms:, geo:는 각자의 RFC에 따라 항목을 나눕니다. 읽는 쪽은 메일 앱을 열지 않고, 전화를 걸지 않고, 문자를 보내지 않고, 내비게이션을 켜지 않습니다. 링크가 되는 것은 http와 https뿐이고 그것도 직접 클릭할 때만입니다. javascript:, data:, vbscript:, file:은 텍스트로 표시하고 경고를 붙입니다.
QR코드를 링크로 여는 습관은 큐싱에도 그대로 적용됩니다. 금융위원회 등 관계 부처는 공유 킥보드나 행사 안내의 QR코드를 악용해 악성 앱 설치를 유도하는 '큐싱'을 주의하라고 안내했습니다(2025-01-20 보도자료). 링크를 열기 전에 확인할 점은 두 가지이고, 읽는 쪽이 둘 다 표시합니다.
https://kick.example.kr@ride-pay.example.com/start는 공유 킥보드 도메인처럼 보입니다.@앞은 사용자 이름이고, 호스트 줄에는ride-pay.example.com이 나옵니다.https://exаmple.com/login의а는 키릴 문자입니다. 호스트는xn--exmple-4nf.com으로 표시되고 비슷한 글자 경고가 붙습니다.
읽히지 않을 때
모든 이미지를 읽는 스캐너는 없습니다. 실패하면 원본 이미지에서 원인이 보이는 경우가 많습니다.
- 모듈당 픽셀이 부족합니다. 33모듈 코드는 45×45픽셀에서 실패하고 60×60에서 읽혔습니다. 모듈당 약 1.5픽셀이 기준입니다. 큰 사진에서 작은 코드를 잘라낼 때는 잘라낸 뒤에도 해상도가 남아 있어야 합니다.
- 흔들림과 반사. 손떨림, 렌즈의 지문, 코드 위로 비치는 반사는 밝고 어두운 차이를 지웁니다. 각도를 바꿔 다시 찍으면 읽히는 경우가 많습니다.
- 대비가 낮습니다. 회색 바탕의 회색 코드는 읽혀도 색이 바랜 인쇄물은 안 될 수 있습니다. 생성기는 휘도 차이가 40% 아래로 내려가면 경고합니다. ISO/IEC 15415 기호 대비 C 등급의 하한입니다.
- 흑백 반전. 검은 바탕에 흰 코드는 표준이 허용하고(반사 반전, §5.2) zxing-cpp도 읽지만, 휴대폰 앱 중에는 반전 이미지를 시도하지 않는 것이 있습니다. 이 페이지에서 읽히고 휴대폰에서 안 읽히면 반전을 의심해 볼 수 있습니다.
- 한 장에 여러 코드. 최대 16개까지 처리하지만, 코드가 겹쳐 있거나 이미지 가장자리에서 잘렸거나 다른 코드 위에 인쇄되어 있으면 하나도 읽히지 않을 수 있습니다.
- 오류 정정 한도를 넘는 손상. H 등급도 코드워드의 약 30%까지입니다. 큰 로고나 이가 나간 모서리가 있는 코드는 읽힐 수도, 읽히지 않을 수도 있습니다.
- 문자 인코딩 추정 실패. 이것은 글자 깨짐이고 디코딩 실패가 아닙니다. 바이트는 읽혔고 인코딩이 다른 것입니다.
- 브라우저가 표시하지 못하는 형식. PDF는 먼저 캡처해야 합니다. HEIC는 표시할 수 있는 브라우저(보통 Safari)에서만 읽힙니다.
- URL 모드의 CORS. 이미지 주소를 넣으면 그 서버가 교차 출처 요청을 허용해야 합니다. 허용하지 않으면 이미지를 저장한 뒤 끌어다 놓으세요.
여기서 읽히지 않으면 더 선명한 원본, 더 좁은 자르기, 다른 스캐너를 시도한 뒤 코드 자체가 잘못되었다고 판단하세요. 어떤 스캐너로도 읽히지 않는 코드는 있습니다. 스캐너 하나만 읽지 못하는 코드는 흔합니다.
개인정보: 페이지 밖으로 나가는 요청
로컬 파일, 붙여 넣은 캡처, 카메라 프레임은 페이지 안에서 디코딩합니다. 이미지와 해독한 텍스트를 이 사이트로 업로드하지 않고, 브라우저 저장소에도 쓰지 않습니다. 디코더 스크립트와 WebAssembly 모듈은 처음에 이 사이트의 /vendor에서 읽으므로 그 요청에는 네트워크가 필요합니다. '지우기' 또는 Ctrl/⌘+L은 결과를 지우고 카메라를 멈춥니다.
디코딩과 별개로 다음 요청이 있습니다.
- URL 모드는 입력한 주소로 이미지를 가져옵니다. 쿠키와 리퍼러를 붙이지 않습니다. 이미지 서버에는 요청이 보이고, 그 서버의 로그 정책이 적용됩니다.
- 해독한 링크, 지도, 메일 주소, 전화번호, SMS를 열면 해당 목적지에 접속하거나 설정된 앱이 열립니다.
- 페이지는 Google Analytics와 AdSense를 불러옵니다. 분석 도구는 일반적인 페이지·기기 정보를 모으고 도구 이벤트(카메라 요청, 이미지 출처, 결과 유형, 인코딩 변경, 링크 열기)를 기록합니다. 이벤트에 이미지나 해독한 텍스트는 들어가지 않습니다. 복사 이벤트는 현재 결과를 성공적으로 복사했을 때만 기록합니다.
해독한 내용은 탭이 열려 있는 동안 페이지에 남지만, 이 페이지는 오프라인 전용 리더가 아닙니다. 디코더, 분석 스크립트, 광고를 불러옵니다. Network 패널에서 실제 요청을 확인할 수 있고, URL 모드와 링크 경고가 브라우저 밖으로 나가는 요청을 설명합니다.
직접 만들고, 인쇄하고, 확인하기
읽기와 만들기는 같은 표준을 쓰므로 생성 쪽에서 보면 제한이 더 분명합니다. QR코드 만들기는 정적 코드를 만듭니다. 내용이 그림 안에 있고, 이 사이트를 거치는 리디렉션도 만료도 없습니다.
- 텍스트/URL, 연락처, 메일, 전화, SMS, 위치 템플릿. Wi-Fi는 와이파이 QR코드 만들기로 안내합니다.
- 오류 정정 L, M, Q, H, PNG 256–4096px, SVG. PNG는 모듈당 정수 픽셀입니다. 버전 2에 여백을 더하면 33모듈 폭이라 1024px를 고르면 1023px, 모듈당 31px가 됩니다.
- 기본 여백은 4모듈입니다. 줄일 수 있지만 경고가 나옵니다.
- 용량이 부족하면 필요한 비트 수, 들어가는 비트 수, 오류 정정을 한 단계 낮추면 들어가는지를 표시합니다.
- 한자 모드(JIS X 0208). 기본은 꺼져 있습니다. 모든 스캐너 앱에서 확인한 것이 아니기 때문입니다.
같은 연락처는 vCard 3.0이면 오류 정정 M에서 버전 8(49×49), MECARD면 버전 5(37×37)에 들어갑니다. 로고 합성, 점 무늬, 그라데이션, 테두리는 제공하지 않습니다. 모듈을 가리는 만큼 오류 정정에 의존하게 되기 때문입니다.
인쇄하기 전에 읽는 사람이 쓸 휴대폰으로 내보낸 파일을 스캔하고, 같은 파일을 QR코드 스캔으로도 읽어 보세요. 템플릿을 잘못 골랐는지, 여백이 부족한지, 인쇄 크기 때문에 모듈당 픽셀이 모자라는지 확인할 수 있습니다. QR코드가 아니라 바코드가 필요하면 바코드 생성기를 쓰세요. EAN-13, EAN-8, UPC-A, Code 128, Code 39, ITF-14, Codabar를 만들며, 이 페이지의 QR 스캐너는 이것들을 읽지 못합니다.