TIL-03: QR Code

회사에서 신규 프로젝트로 QR Code를 사용해야 할 필요성이 생겨서 이왕 이 기술에 대해 알아보기로 결정하여 TIL을 진행하기로 했습니다.
회사의 Time&Attendance 서비스에서는 고객사 직원들이 출근(Clock in), 퇴근(Clock out)을 하여 기록할 수 있도록 하고 있는데, 최근 들어 고객사로부터 어뷰징 방지를 위해 QR Code를 찍어야 CI/CO를 할 수 있도록 하는 신규 기능을 추가해달라는 요청이 많이 들어왔습니다.
저 개인적으로는 QR Code를 평소에 사용만 해봤지 이것을 구현하기 위한 기반 기술에 대해서는 아무것도 알지 못한 상태입니다. 이번 TIL을 통해 기초적인 부분이라도 파악해보려고 합니다.
QR Code란?

한 문장 정의: QR Code(Quick Response Code)는 2차원 매트릭스 형태의 바코드로, 텍스트/URL/데이터 등을 흑백 패턴으로 인코딩하여 카메라로 빠르게 읽을 수 있게 만든 것입니다.
왜 필요한가?
기존 바코드(1D)는 수십 자의 숫자 정도만 담을 수 있습니다. 하지만 웹 서비스를 만들다보면 좀 더 많은 정보를 담아야 할 필요성이 생깁니다
- 앱을 실행시킬 URL(Deep Link 또는 Universal Link)
- Unique Identifier
- 사용자가 어떤 액션을 수행하는지
- 위변조 방지를 위한 토큰이나 타임스탬프
이 정도 데이터를 담으려면 1D 바코드로는 불가능하고 2D인 QR Code가 필요합니다. QR Code는 최대 약 4,296자의 영숫자를 담을 수 있어서 URL + 파라미터를 충분히 인코딩할 수 있습니다.
핵심 원리 5가지
1. 인코딩 구조 - "데이터를 흑백 모듈로 변환"
QR Code의 최소 단위는 모듈(module) - 흑색 또는 백색의 정사각형 점입니다. 입력 데이터를 비트열로 변환한 뒤, 이 비트열을 흑(1)/백(0) 모듈 패턴으로 매트릭스에 배치합니다.
인코딩 모드는 4가지가 있고, 데이터 종류에 따라 자동 선택됩니다.
- Numeric: 숫자만 (효율 최고, 최대 7,089자)
- Alphanumeric: 영대문자 + 숫자 + 일부기호 (최대 4,296자)
- Byte: ISO-8859-1 등 바이트 데이터 (최대 2,953 바이트)
- kanji: 일본어 한자 (최대 1,817자)
2. 위치 탐지 패턴 (Finder Pattern) - "어디서 찍혀도 읽힌다"
QR Code의 3개 모서리에 있는 큰 정사각형 패턴을 본 적이 있을 겁니다. 이것이 Finder Pattern입니다. 카메라가 QR Code를 인식할 때 가장 먼저 이 3개 패턴의 위치를 찾아서 코드의 방향, 크기, 기울기를 결정합니다.
■■■■■■■ □ ■■■■■■■
■□□□□□■ □ ■□□□□□■
■□■■■□■ □ ■□■■■□■
■□■■■□■ □ ■□■■■□■
■□■■■□■ □ ■□■■■□■
■□□□□□■ □ ■□□□□□■
■■■■■■■ □ ■■■■■■■
□□□□□□□ □ □□□□□□□
■■■■■■■
■□□□□□■
■□■■■□■ ← 4번째 모서리에는 없음!
■□■■■□■ (이걸로 방향 판별)
■□■■■□■
■□□□□□■
■■■■■■■
3개만 있는 이유가 중요한데, 만약 4개 모서리 모두에 있으면 상하좌우 대칭이 되어 방향을 판별할 수 없기 때문입니다. 3개만 배치해서 "빈 모서리 = 우하단"이라는 규칙으로 방향을 확정합니다.
3. 오류 정정 (Error Correction) - "일부가 손상돼도 읽힌다"
QR Code는 Reed-Solomon 오류 정정 코드를 사용합니다. 데이터 외에 오류 정정용 코드워드를 추가로 인코딩하여, QR Code 일부가 가려지거나 손상되어도 복원할 수 있습니다.
4가지 레벨이 있습니다.
| 레벨 | 복원 가능 비율 | 용도 |
|---|---|---|
| L(Low) | -7% | 깨끗한 환경, 데이터 용량 최대화 |
| M(Medium) | -15% | 일반적 용도 |
| Q(Quartile) | -25% | 약간 거친 환경 |
| H(High) | -30% | 로고 삽입, 거친 환경 |
신규 QR Code 기반 CI/CO 기능에 기반해서 생각해보자면, 매장의 태블릿을 사용해 화면에 직접 QR Code를 출력할 것이기 때문에 물리적 손상 가능성은 매우 낮습니다. 그러니 L 또는 M 레벨이면 충분할 것 같습니다.
4. 버전 (Version) - "데이터 양에 따라 크기가 달라진다"
QR Code에는 Version 1(21x21 모듈)부터 Version 40(177x177)까지 40단계가 있습니다. 버전이 올라갈수록 모듈 수가 늘어나 더 많은 데이터를 담을 수 있습니다.
공식: 모듈 수 = (버전 x 4) + 17
예를 들어 https://app.example.com/clockin?store=ABC123&token=xyz 정도의 URL(약 60바이트)이라면 Version 4(33x33) 정도면 충분합니다.
5. 마스킹 (Masking) - "스캐너가 잘 읽도록 패턴 최적화"
인코딩된 데이터가 우연히 Finder Pattern과 비슷한 패턴을 만들거나, 흑/백 모듈이 한쪽에 몰리면 스캐너가 오인식할 수 있습니다. 그래서 QR Code 생성 시 8가지 마스크 패턴을 모두 적용해보고, 패널티 점수가 가장 낮은 마스크를 자동 선택합니다. 이 과정은 라이브러리가 자동으로 처리하므로 개발자가 직접 다룰 일은 거의 없습니다.
QR Code vs 다른 코드 비교
| 비교 항목 | 1D 바코드 | QR Code(2D) | Data Matrix |
|---|---|---|---|
| 데이터 용량 | ~20 숫자 | ~4,296자 영숫자 | ~2,335자 영숫자 |
| 방향 인식 | 수평만 | 360도 어느 방향 | 360도 어느 방향 |
| 오류 정정 | 없음 | 최대 30% | 최대 30% |
| 인식 속도 | 보통 | 빠름 | 빠름 |
| 생태계 | POS 시스템 | 범용 (모바일 카메라 지원) | 산업용 중심 |
제 사용 사례에서는 QR Code가 최적이라고 판단됩니다. 모바일 카메라 기본 앱이 QR Code를 네이티브로 인식하기 때문입니다.
QR Code 기반 T&A 시스템 아키텍처
구현 흐름을 아주 간단하게 살펴보자면:
[매장 태블릿] [직원 모바일] [T&A 서버]
│ │ │
│ 1. QR 코드 표시 │ │
│ (URL 인코딩) │ │
│ ─────────────────→ │ │
│ 카메라로 스캔 │ │
│ │ 2. URL → 앱 실행 │
│ │ (Deep Link) │
│ │ │
│ │ 3. CI/CO 요청 ──────→ │
│ │ │ 4. 검증 & 기록
│ │ ←────── 응답 ──────── │
│ │ 5. 결과 표시 │
여기서 QR Code가 담는 내용의 예시:
https://example.tna.com/clock
?store_id=STORE_001
&action=clock_in
&ts=1709900000
&sig=a3f2b1c8...
sig는 서버 비밀키로 생성한 HMAC 서명으로, QR Code가 위조되지 않았음을 검증합니다. ts는 QR Code의 유효 시간을 제한하여 스크린샷 재사용 등을 방지합니다.
장단점 분석
장점:
- 모바일 카메라 기본 지원: 별도 앱 설치 없이 URL 인식 가능
- 오류 정정 내장: 약간의 화면 반사/흐림도 인식
- 빠른 인식 속도: 직원이 빠르게 CI/CO 가능
- 데이터 용량 충분: URL + 파라미터를 충분히 담을 수 있음
단점:
- 스크린샷 복제 가능: 시간 기반 토큰이나 일회용 QR로 대응 필요
- 조명 환경 영향: 태블릿 밝기, 반사 등에 따라 인식률 변동
- 카메라 성능 의존: 저사양 기기에서 인식 지연 가능
QR Code와 연관된 다른 기술들
앞서 T&A 시스템 아키텍처에서 QR Code에 ts(타임스탬프)와 sig(HMAC 서명)를 포함시켜 보안을 강화하는 방법을 살펴봤습니다. 이 "시간 기반 유효성" 아이디어는 사실 TOTP라는 기존 기술에서 빌려온 개념입니다. QR Code가 TOTP의 비밀키 전달 수단으로도 쓰이기 때문에, 두 기술이 어떻게 연결되는지 함께 알아보겠습니다.
1. TOTP
TOTP란?
한 문장 정의: TOTP(Time-based One-Time Password)는 현재 시각과 비밀키를 조합하여 일정 시간(보통 30초)마다 새로운 일회용 비밀번호를 생성하는 알고리즘입니다.
Google Authenticator, Microsoft Authenticator 같은 앱에서 30초마다 바뀌는 6자리 숫자 - 그게 바로 TOTP 입니다.
왜 필요한가?
문제: 비밀번호가 유출되면 누구나 로그인할 수 있습니다. 비밀번호는 "아는 것(knowledge)"이라서, 한 번 알려지면 끝입니다.
해결: 비밀번호 + "가지고 있는 것(possession)"을 조합합니다. TOTP는 사용자의 기기에 저장된 비밀키로 시간 기반 코드를 생성하므로, 비밀번호가 유출되어도 기기가 없으면 로그인할 수 없습니다. 이것이 2FA(Two-Factor Authentication)의 핵심입니다.
TOTP이 핵심 원리
1. 비밀키 공유 - "서버와 클라이언트가 같은 seed를 가진다"
TOTP의 출발점은 서버와 클라이언트(인증 앱)가 동일한 비밀키(Secret Key)를 공유하는 것입니다.
[서버] [클라이언트 앱]
│ │
│ 비밀키: JBSWY3DPEHPK3PXP │
│ ════════════════════════════ │
│ (둘 다 같은 키를 가짐) │
│ │
이 비밀키는 최초 등록 시 단 한 번만 전달되고, 이후에는 네트워크를 통해 전송되지 않습니다. 여기서 QR Code가 등장합니다 (뒤에서 설명하겠습니다)
2. 시간을 입력으로 - "현재 시각이 곧 카운터"
TOTP는 HOTP(HMAC-based OTP)의 확장인데, HOTP가 "카운터 값"을 입력으로 쓰는 반면, TOTP는 현재 시각을 카운터로 사용합니다.
타임스텝 계산:
T = floor(현재_Unix_시간 / 시간_간격)
예시 (시간 간격 = 30초):
- Unix 시간 1709899980 → T = 56996666
- Unix 시간 1709900009 → T = 56996666 (같은 30초 구간)
- Unix 시간 1709900010 → T = 56996667 (다음 30초 구간 → 새 코드!)
3. HMAC-SHA1으로 코드 생성 - "비밀키 + 시간 → 해시 → 6자리"
생성 과정을 단계별로 보면 이렇습니다.
Step 1: 타임스텝 계산
T = floor(1709900000 / 30) = 56996666
Step 2: T를 8바이트 빅엔디안으로 변환
T_bytes = 0x000000000365B33A
Step 3: HMAC-SHA1 계산
hash = HMAC-SHA1(secret_key, T_bytes)
→ 20바이트 해시값 생성
Step 4: Dynamic Truncation (동적 절단)
offset = hash[19] & 0x0F ← 마지막 바이트의 하위 4비트
code = (hash[offset..offset+3]) & 0x7FFFFFFF ← 4바이트 추출, 최상위 비트 제거
Step 5: 자릿수 제한
OTP = code mod 10^6 ← 6자리로 제한
→ 예: 482917
왜 Dynamic Truncation인가? HMAC-SHA1의 출력은 20바이트(160비트)인데 여기서 6자리 숫자를 뽑아야 합니다. 단순히 앞 6자리를 쓰면 해시의 특정 부분만 노출되어 예측 가능성이 생깁니다. 해시값 자체를 이용해 "어디서 자를지"를 결정함으로써 예측을 어렵게 만듭니다.
4. 시간 허용 오차 - "시계가 약간 어긋나도 괜찮다"
현실에서 서버와 클라이언트 시계가 정확히 일치하기는 어렵습니다. 그래서 서버는 보통 현재 타임스텝 ±1~2 구간의 코드도 함께 검증합니다.
서버가 검증하는 범위 (skew = 1인 경우):
T-1 구간의 OTP ← 30초 전 코드도 허용
T 구간의 OTP ← 현재 코드
T+1 구간의 OTP ← 30초 후 코드도 허용
이로써 시계가 최대 30초 정도 어긋나도 인증이 성공합니다.
5. 표준: RFC 6238
TOTP는 RFC 6238에 정의되어 있고, 기반이 되는 HOTP는 RFC 4226에 정의되어 있습니다. 이 표준 덕분에 Google Authenticator, Authy, 1Password 등 서로 다른 앱이 동일한 비밀키로 동일한 코드를 생성할 수 있습니다.
2. otpauth
otpauth란?
한 문장 정의: otpauth는 OTP 설정 정보(비밀키, 계정, 알고리즘 등)를 URI 형식으로 표현하는 스킴(scheme)입니다.
웹 브라우저에서 https://로 시작하는 URL을 클릭하면 브라우저가 열리듯, otpauth://로 시작하는 URI를 인식하면 인증 앱이 열립니다.
URI 구조
otpauth://TOTP/Example:alice@example.com?
secret=JBSWY3DPEHPK3PXP
&issuer=Example
&algorithm=SHA1
&digits=6
&period=30
각 구성요소를 분해하면 이렇습니다.
otpauth:// ← 스킴: 인증 앱이 처리할 URI임을 선언
totp/ ← 타입: TOTP (또는 hotp)
Example: ← 발급자 (서비스 이름)
alice@example.com ← 계정 식별자
?secret=JBSWY3DPEHPK3PXP ← ⭐ 핵심! Base32로 인코딩된 비밀키
&issuer=Example ← 발급자 (중복이지만 호환성을 위해)
&algorithm=SHA1 ← 해시 알고리즘 (기본값: SHA1)
&digits=6 ← OTP 자릿수 (기본값: 6)
&period=30 ← 시간 간격 초 (기본값: 30)
QR Code와 연결 - "왜 QR Code를 쓰는가?"
여기서 핵심 질문입니다. 비밀키를 어떻게 안전하게 사용자 기기로 전달할 것인가?
비밀키 JBSWY3DPEHPK3PXP를 사용자에게 직접 입력하라고 하면 오타 위험이 크고, 네트워크로 전송하면 중간에 탈취될 수 있습니다.
해결: otpauth URI 전체를 QR Code로 인코딩 합니다.
[서버] [사용자 기기]
│ │
│ 1. 비밀키 생성 │
│ 2. otpauth URI 구성 │
│ 3. URI → QR 코드 생성 │
│ 4. 화면에 QR 코드 표시 ───→ 카메라 스캔 │
│ │
│ 5. URI 파싱 │
│ 6. 비밀키 저장 │
│ 7. TOTP 생성! │
│ │
│ 8. 사용자가 입력한 OTP 검증 ←── OTP 입력 │
이 흐름에서 비밀키는 네트워크를 통하지 않고 화면 → 카메라라는 물리적 채널로 전달됩니다. 같은 공간에 있어야만 QR Code를 스캔할 수 있으므로, 원격 탈취가 매우 어렵습니다.
QR Code + T&A CI/CO
여기서 중요한 구분을 해야 합니다.
직접 사용이 아니다.
T&A system에서 QR Code는 otpauth URI를 담는 것이 아니라 Deep Link URL을 담아야 합니다. 목적이 다르기 때문입니다.
otpauth QR: 비밀키를 기기에 전달하여 OTP 생성기 등록
T&A QR: 앱 실행 URL을 전달하여 CI/CO 액션 트리거
하지만 빌려올 수 있는 아이디어가 있습니다
TOTP의 시간 기반 유효성 개념은 T&A QR Code 보안에 직접 적용할 수 있습니다.
정적 QR (위험):
https://tna.app/clock?store=STORE_001
→ 한번 스크린샷 찍으면 영원히 재사용 가능!
시간 기반 QR (안전):
https://tna.app/clock?store=STORE_001&ts=1709900000&sig=a3f2...
→ ts가 현재 시각, sig가 서버 비밀키로 생성한 HMAC
→ 서버에서 ts가 30초~1분 이내인지 검증
→ 만료된 QR은 거부!
이 방식에서 태블릿은 주기적으로(예: 30초마다) 새 QR Code를 생성하여 화면에 표시하고, 서버는 QR에 포함된 타임스탬프와 서명을 검증합니다. TOTP와 원리가 같습니다.
QR Code + TOTP를 T&A에 적용한 Flow에 대해 좀 더 설명하자면:
- 매장 태블릿이 TOTP 코드 생성 (30초마다)
- Deep Link URL 구성 (store_id + otp + ts)
- URL → QR Code 인코딩 (매트릭스 생성)
- 직원이 모바일 카메라로 스캔
- Deep Link 실행 → T&A 앱 열림 (앱의 CI/CO 화면으로 이동)
- 직원이 CI or CO 중에 하나를 선택하여 Submit 버튼 터치
- 앱이 서버에 CI/CO 요청 (otp, ts 포함)
- 서버가 otp + ts 유효성 검증 ← 여기서 판정
- 유효 → CI/CO 기록 완료
- 무효 → 거부 (만료 or 이미 사용된 코드)
마무리
이번 TIL을 통해 QR Code의 인코딩 구조, Finder Pattern, 오류 정정, 버전, 마스킹이라는 핵심 원리를 파악했고, TOTP의 시간 기반 유효성 개념을 QR Code 보안에 어떻게 적용할 수 있는지까지 정리할 수 있었습니다.
다음에는 실제 구현 단계에서 Deep Link 처리 방식(iOS Universal Link vs Android App Link), QR Code 갱신 주기와 사용자 경험 사이의 트레이드오프, 그리고 서버 측 HMAC 키 관리 전략을 더 알아볼 예정입니다.
Comments (0)
Checking login status...
No comments yet. Be the first to comment!
