QR 코드는 매일 본다. 카페 메뉴, 택시 결제, 지하철 광고. 그런데 카메라가 그 흑백 점 격자만 보고 어떻게 URL을 읽어내는지 생각해본 적은 별로 없는 것 같다.
처음 들었던 의문은 두 가지였다. 어떤 각도로 잡아도, 손이 떨려도, 왜 잘 인식될까. 한쪽 모서리가 가려져 있어도 어떻게 멀쩡히 읽힐까. 답을 정리해보니 단순한 격자처럼 보이는 사각형 안에 생각보다 많은 구조가 들어 있다.
QR 코드는 1994년 일본 덴소(Denso Wave)가 자동차 부품 추적용으로 만든 형식이다. 바코드보다 더 많은 정보를 담으면서도, 인쇄가 흐려지거나 일부가 가려져도 읽혀야 한다는 요구에서 출발했다. 그 요구가 지금 보는 QR의 모든 디테일을 만들었다.
1. 전체 구조 한눈에 보기
QR 코드를 처음 보면 검은색과 흰색 점이 무작위로 흩어진 격자처럼 보인다. 사실 각 영역마다 정해진 역할이 있다.
그림 1. QR 코드의 주요 섹션을 색으로 구분한 도식.
크게 일곱 개 영역이 있다. 모서리의 큰 사각형 세 개는 파인더 패턴(Finder Pattern) 으로 카메라가 “여기가 QR입니다”라고 판단하는 기준점이다. 우하 근처의 작은 사각형은 정렬 패턴(Alignment Pattern) 으로 곡면이나 기울임을 보정한다. 가로/세로 점선 십자는 타이밍 패턴(Timing Pattern) 으로 모듈 격자가 몇 행/몇 열인지 알려준다.
파인더 주변의 L자 영역은 포맷 정보(Format Information) 다. 어떤 오류 정정 레벨을 쓰는지, 어떤 마스크 패턴을 적용했는지가 여기에 인코딩되어 있고, 한쪽이 손상돼도 읽히도록 두 군데에 똑같이 적힌다.
가운데 회색 영역이 실제 데이터와 오류 정정 코드 가 들어가는 자리다. 외곽의 흰 영역은 여백(Quiet Zone) 으로 카메라가 격자의 경계를 잡는 데 쓰인다.
읽는 순서가 정해져 있어서 카메라는 이 순서대로 정보를 뽑는다. 파인더로 위치와 회전을 잡고, 타이밍으로 격자 간격을 잡고, 포맷 정보로 메타데이터를 읽고, 마지막에 데이터를 디코드한다. 그래서 회전이 어떻든 잘 읽힌다.
2. 파인더 패턴이 작동하는 비밀
세 모서리의 큰 사각형이 가장 눈에 띈다. 7×7 모듈 크기에 검정-흰-검정 3겹 구조. 단순해 보이는데 카메라 인식의 비밀이 거의 여기에 있다.
그림 2. 파인더 패턴 하나를 7×7 모듈 단위로 확대한 모습. 가로·세로 어느 방향으로 가운데를 가로질러도 검:흰:검:흰:검이 1:1:3:1:1 비율로 나타난다.
핵심은 비율이다. 파인더 패턴을 어느 방향으로 가로지르는 한 줄을 스캔해도, 검정과 흰색이 정확히 1:1:3:1:1 비율 로 나타난다. 가로로 스캔해도, 세로로 해도, 외곽-내부-중심-내부-외곽 순서로 1+1+3+1+1=7 모듈이 나온다.
이 비율이 영리한 이유는 두 가지다. 첫째, 어떤 각도로 카메라를 잡든 비율은 유지된다. 기울임이 있으면 절대 길이는 변해도 비율은 그대로 유지된다. 둘째, 자연 이미지에서 1:1:3:1:1 비율의 흑백 줄무늬는 거의 우연히 나오지 않는다. 카메라가 이미지를 한 번 훑어서 이 비율의 줄을 찾으면 “여기에 QR이 있다”고 빠르게 판단할 수 있다.
세 모서리에 같은 패턴이 있는 것도 이유가 있다. 두 개로는 직선만 그릴 수 있지만 세 개면 평면을 정의할 수 있다. 좌상·우상·좌하 세 위치로 QR이 어느 방향을 향하고 있는지까지 한 번에 파악된다. 그래서 거꾸로 들고 찍어도 잘 읽힌다.
3. 데이터는 어떻게 비트가 되어 격자에 들어가나
“HELLO”를 QR로 만든다고 해보자. 어떤 과정을 거쳐 검은 점 무늬가 만들어질까.
그림 3. 입력 텍스트가 비트로 변환되어 격자에 배치되기까지의 5단계.
1단계는 모드 선택 이다. QR은 입력 데이터 종류별로 다른 인코딩 모드를 쓴다. 숫자만(0~9)이면 Numeric, 대문자와 숫자 일부 기호이면 Alphanumeric, 임의 바이트면 Byte, 한자나 한글 같은 동아시아 문자는 Kanji 모드. 각 모드는 4비트 식별자로 구분된다. “HELLO”는 알파벳 대문자라 Alphanumeric(0010)이다.
2단계는 문자 수와 데이터 인코딩 이다. 모드 다음에 데이터 길이를 비트로 적고, 그 뒤에 실제 문자를 비트로 변환한다. Alphanumeric은 두 글자를 묶어 11비트로 인코딩하는 등 모드별로 룰이 다르다.
3단계는 종료 비트와 패딩 이다. 데이터가 끝났음을 알리는 0000을 붙이고, QR 용량까지 모자란 만큼 정해진 패딩 비트(11101100과 00010001의 반복)로 채운다.
4단계는 Reed-Solomon 오류 정정 코드 추가 다. 데이터 비트 뒤에 일정 비율로 EC 코드워드를 덧붙인다. 이 부분이 QR이 망가져도 읽히게 하는 비밀이다.
5단계는 격자 배치와 마스크 다. 비트열을 우하 모서리부터 지그재그 패턴으로 데이터 영역에 채워 넣고, 마지막으로 마스크를 XOR로 덮어씌운다. 마스크 이야기는 따로 한 챕터가 필요하다.
4. 마스크 패턴 — 인코더가 8개를 다 시도하는 이유
데이터를 격자에 그대로 배치하면 문제가 생길 수 있다. 데이터가 우연히 모두 1(검정)이면 큰 검정 블록이 생긴다. 또는 1:1:3:1:1 비율이 우연히 데이터 영역에 나타나면 카메라가 그 자리를 가짜 파인더로 오인할 수 있다.
이 문제를 해결하는 게 마스크다. 8가지 미리 정해진 흑백 패턴 중 하나를 데이터 영역에 XOR로 덮어씌워 검정과 흰의 분포를 균형 있게 만든다.
그림 4. 마스크 0~7을 8×8 모듈 단위로 시각화. 각 마스크는 (i, j) 좌표에 대한 간단한 수식으로 정의된다.
체스판, 가로줄, 세로줄, 대각선, 큰 격자, 격자형, 혼합, 사선. 여덟 가지 모두 단순한 수식으로 정의된다. 어떤 좌표(i, j)에서 마스크 값이 1이면 그 모듈의 색을 뒤집고, 0이면 그대로 둔다.
문제는 어떤 마스크가 가장 좋은지가 데이터마다 다르다는 점이다. 그래서 인코더는 8개를 모두 적용해보고 각각에 점수를 매긴 뒤, 가장 낮은 점수의 마스크를 최종 선택한다.
5. 마스크 점수 — 무엇을 평가하나
점수는 네 가지 패널티 룰로 매겨진다.
그림 5. 8개 마스크 후보 각각의 패널티 합산 점수 예시. 실제 점수는 데이터마다 다르며, 가장 낮은 총점의 마스크가 선택된다.
N1, 같은 색 연속: 한 행 또는 열에 같은 색이 5개 이상 연속되면 패널티. 길이가 길수록 점수가 누적된다.
N2, 2×2 같은 색 블록: 같은 색 모듈 4개가 정사각형으로 묶이면 패널티. 큰 블록이 생기지 않게 한다.
N3, 1:1:3:1:1 비율의 가짜 파인더: 데이터 영역에 1:1:3:1:1 비율의 흑백 줄이 우연히 나타나면 큰 패널티. 카메라가 가짜 파인더로 오인하는 사고를 막는다.
N4, 검정/흰 균형: 검정 모듈 비율이 50%에서 멀어질수록 패널티. 카메라 노출 보정에 유리하게 만든다.
네 룰을 합산한 총 점수가 가장 낮은 마스크가 선택된다. 같은 데이터라도 비트가 살짝 바뀌면 선택되는 마스크가 달라지는 이유다.
마스크 인덱스(어떤 마스크를 썼는지)는 포맷 정보에 적힌다. 디코더는 포맷 정보를 먼저 읽어 어느 마스크를 썼는지 알아낸 뒤, 똑같은 마스크를 다시 XOR로 덮어 원본 데이터를 복원한다. XOR는 두 번 적용하면 원래대로 돌아오는 성질이 있다.
6. 에러 정정 레벨 — 더러워져도 읽히는 이유
QR이 가려져도, 더러워져도, 한쪽 모서리가 찢어져도 읽히는 비밀은 Reed-Solomon 오류 정정 코드 다. 데이터 비트 뒤에 별도의 EC 코드워드를 덧붙여, 일부 비트가 손상돼도 원본을 복원할 수 있게 한다.
QR은 네 단계의 정정 레벨을 지원한다.
그림 6. 네 정정 레벨별 Data 영역과 EC 영역의 비율. 레벨이 올라갈수록 EC가 차지하는 비중이 늘어난다.
L(Low)은 약 7%까지 복구 가능, M(Medium)은 15%, Q(Quartile)는 25%, H(High)는 30%다. 레벨이 올라갈수록 가려져도 잘 읽힌다는 뜻이지만, 그만큼 EC 코드워드가 차지하는 비율이 늘어나기 때문에 같은 데이터를 담아도 QR 크기가 커진다.
로고가 박혀 있는 QR을 본 적 있을 것이다. 가운데가 회사 로고로 덮여 있는데도 정상적으로 읽힌다. 이건 보통 H 레벨로 인코딩해 30% 손상까지 복구되도록 만들어진 것. 로고가 가린 영역의 데이터를 EC 코드가 복원한다.
물론 트레이드오프가 있다. 같은 텍스트를 H로 만들면 L로 만든 것보다 격자가 훨씬 커진다. 인쇄 공간이 빠듯하면 L이나 M으로, 야외 광고처럼 손상 위험이 있으면 H로. 용도별로 골라 쓰면 된다.
마무리
QR 코드의 모든 디테일이 결국 한 가지 목표로 수렴한다. 어떤 환경에서든, 어떤 각도에서든, 일부가 망가져도 잘 읽히게. 1994년 자동차 부품 추적용으로 만들어진 단순한 격자가 30년 동안 결제와 인증, 메뉴와 광고에 살아남은 이유다.
다음에 QR을 볼 때 한 번 격자를 자세히 들여다보면 좋겠다. 모서리 세 사각형은 카메라가 위치를 잡는 기준점, 가로 세로 점선은 격자 동기, 우하 작은 사각형은 기울임 보정. 그리고 검정 점들의 분포는 마스크 알고리즘이 여덟 번 시도해서 고른 가장 균형 잡힌 모습이다.
겉으로 단순해 보이는 것이 안에 더 정교한 설계를 숨기고 있다는 게 — 좋은 엔지니어링의 한 단면 같기도 하다.