Profile image
Jinyoung
Dev

TIL-03: QR Code

90% Human
10% AI
TIL-03: QR Code
0 views
14 min read

회사에서 신규 프로젝트로 QR Code를 사용해야 할 필요성이 생겨서 이왕 이 기술에 대해 알아보기로 결정하여 TIL을 진행하기로 했습니다.

회사의 Time&Attendance 서비스에서는 고객사 직원들이 출근(Clock in), 퇴근(Clock out)을 하여 기록할 수 있도록 하고 있는데, 최근 들어 고객사로부터 어뷰징 방지를 위해 QR Code를 찍어야 CI/CO를 할 수 있도록 하는 신규 기능을 추가해달라는 요청이 많이 들어왔습니다.

저 개인적으로는 QR Code를 평소에 사용만 해봤지 이것을 구현하기 위한 기반 기술에 대해서는 아무것도 알지 못한 상태입니다. 이번 TIL을 통해 기초적인 부분이라도 파악해보려고 합니다.


QR Code란?

QR Code - Example

한 문장 정의: 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가지가 있고, 데이터 종류에 따라 자동 선택됩니다.

  1. Numeric: 숫자만 (효율 최고, 최대 7,089자)
  2. Alphanumeric: 영대문자 + 숫자 + 일부기호 (최대 4,296자)
  3. Byte: ISO-8859-1 등 바이트 데이터 (최대 2,953 바이트)
  4. 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에 대해 좀 더 설명하자면:

  1. 매장 태블릿이 TOTP 코드 생성 (30초마다)
  2. Deep Link URL 구성 (store_id + otp + ts)
  3. URL → QR Code 인코딩 (매트릭스 생성)
  4. 직원이 모바일 카메라로 스캔
  5. Deep Link 실행 → T&A 앱 열림 (앱의 CI/CO 화면으로 이동)
  6. 직원이 CI or CO 중에 하나를 선택하여 Submit 버튼 터치
  7. 앱이 서버에 CI/CO 요청 (otp, ts 포함)
  8. 서버가 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!