KKflow

MAKING · 제작기

QRforge 제작기: 하트 모양 QR을 다섯 번 다시 그린 이야기

라이브러리를 버리고 QR 렌더러를 직접 만든 첫날부터, 모양을 내다 스캔이 안 되는 문제와 씨름한 31시간, 유료 요금제를 붙였다 하루 만에 걷어낸 결정까지 커밋 기록으로 돌아봤습니다.

· KKflow

QRforge의 첫 커밋은 2026년 5월 18일 밤 11시, 제목은 "client-side QR code generator"입니다. 처음부터 목표는 브라우저 안에서 QR 코드를 만드는 도구였습니다. 그 뒤 기록은 두 덩어리로 나뉩니다. 5월 18~19일 이틀 동안의 커밋 7개, 그리고 5주 넘게 비어 있다가 6월 22일 밤부터 24일까지 약 31시간 동안 이어진 30개의 PR입니다. 사이의 공백에 대해서는 기록에 아무 설명이 없습니다. 이 글은 그 두 구간을 따라간 제작기입니다. 코드는 거의 전부 Claude Code와 함께 작성했고, 저는 작업 단위마다 PR을 열고 합치는 방식으로 진행했습니다.

첫날: 라이브러리를 버리다

처음에는 QR 꾸미기용으로 널리 쓰이는 qr-code-styling 라이브러리로 시작했습니다. 7가지 콘텐츠 타입(URL, 텍스트, WiFi, 연락처, 이메일, SMS, 전화번호)과 PNG·SVG 다운로드가 첫 커밋에 들어갔습니다.

첫 버그는 레이아웃이었습니다. 크기 슬라이더가 미리보기 영역의 최소 크기를 직접 바꾸고 있어서, 800px을 고르면 오른쪽 열이 넓어지며 페이지 전체가 옆으로 밀렸습니다. 미리보기는 최대 320px로 고정하고, 슬라이더는 PNG로 저장할 때의 해상도를 정하는 용도로 바꿨습니다.

그리고 같은 날 큰 결정을 했습니다. 커밋 메시지는 이렇습니다. "Replace qr-code-styling with a custom SVG renderer built on top of the qrcode package's bit matrix. This unlocks shapes the previous library didn't support." QR 데이터를 칸(비트) 격자로 만드는 일만 qrcode 패키지에 맡기고, 그 격자를 어떤 모양으로 그릴지는 직접 SVG로 그리기로 한 것입니다. 그래야 기존 라이브러리가 지원하지 않던 하트·별·다이아몬드 모양의 점을 그릴 수 있었습니다.

모양을 내면 스캔이 안 된다

모양을 자유롭게 그릴 수 있게 되자 바로 다음 문제가 나왔습니다. QR 코드 전체를 하트나 원 모양으로 오려 내면, 세 모서리에 있는 큰 사각형(위치 찾기 패턴)이 잘려 나갑니다. 5월 19일 커밋에는 이렇게 적혀 있습니다. "Heart's narrow bottom inevitably trims off the bottom-left finder, which kills scannability regardless of error correction level." 하트의 좁은 아래쪽이 왼쪽 아래 위치 패턴을 잘라 버려서, 오류 정정 레벨을 아무리 올려도 스캔이 되지 않았습니다. 그래서 위치 패턴은 오려 내는 범위 밖에 따로 그리게 바꿨습니다.

6월에 다시 작업을 시작했을 때도 이 문제는 계속 모양을 바꿔 가며 돌아왔습니다. 틀(프레임) 기능은 기록상 여덟 단계를 거쳤습니다.

  1. QR 뒤에 깔리는 장식 배경
  2. QR을 실루엣 모양으로 표시
  3. 모양대로 오려 내고 오류 정정 레벨을 H로 강제
  4. QR은 네모 그대로 두고 모양 배지로 감싸기("wrap") — "기존엔 사각 QR을 모양대로 잘라 위치패턴이 삐져나와 지저분했음"
  5. 감싸기와 모양 변형("morph") 두 모드
  6. 변형은 다이아몬드(45° 회전)에만 적용 — 곡선형은 "QR이 잘려 삐져나와 별로였음"
  7. 오려 내기를 "스캔 안 될 수 있음" 경고와 함께 장식용으로 다시 추가
  8. 육각형·팔각형·방패, 3~12각형을 직접 만드는 다각형·별 빌더 추가

어느 쪽이 실제로 스캔되는지는 디코더로 확인했습니다. 커밋 기록에는 "오려내기는 디코더 테스트에서 실제로 인식 실패(장식 전용), wrap/회전은 인식 성공"이라고 남아 있습니다. 그래서 오려 내기는 경고를 붙인 장식 모드로만 남겼습니다.

하트만 다섯 번

그중에서도 하트 모양은 기록에 남은 것만 다섯 번 다시 그렸습니다. 둥근 아치 두 개로 만든 하트를 거쳐, 마지막 커밋은 "이전 아치형 하트가 어색해서 … 정통 밸런타인 하트로 교체"입니다. QR에서 모양을 낸다는 건 예쁘게 보이는 것과 카메라가 읽을 수 있는 것 사이에서 계속 줄타기를 하는 일이었습니다.

스캔되는지 화면에서 바로 확인

6월 23일 오후에는 "차별화"라는 이름으로 네 가지 기능을 붙였습니다.

  • 실시간 스캔 검증 — 꾸미는 동안 브라우저 안에서 QR을 직접 디코딩해서, 지금 디자인이 실제로 읽히는지 바로 알려 줍니다.
  • 디자인 공유 링크 — 디자인 설정을 주소의 # 뒤에 담아, 서버를 거치지 않고 링크로 공유합니다. 로고는 용량 때문에 공유 링크에서 뺐습니다.
  • 한국어·영어 전환
  • 대량 생성 — CSV 파일 한 줄마다 QR을 만들어 ZIP으로 묶어 내려받습니다.

그런데 스캔 검증이 거꾸로 문제를 일으켰습니다. 실제로는 잘 읽히는 QR에 "스캔이 안 될 수 있다"는 경고가 떴습니다. 원인은 커밋에 적혀 있습니다. "verifyScan이 512px 단일 배율로만 디코드해, 모듈이 떨어진 원형/둥근 점은 그 배율에서 jsQR 그리드 추출에 실패 → 실제로는 스캔되는 코드에 거짓 경고." 점 사이가 떨어진 원형 점은 특정 크기 하나에서만 디코딩에 실패했던 것입니다. 512·380·760px 세 가지 크기로 각각 시도하도록 바꿔 해결했습니다.

작은 수정도 있었습니다. 한국어가 글자 단위로 줄바꿈되어 "코드"가 "코/드"로 갈라지던 문제, 화면 아래에 고정된 요소 두 개가 같은 층위에서 겹치던 문제를 고쳤습니다.

동적 QR: 계정 대신 관리 링크

6월 23일 저녁에는 인쇄한 뒤에도 목적지를 바꿀 수 있는 동적 QR을 붙였습니다. 하루 전 커밋에는 "무료·클라이언트 구조 유지 (백엔드 불필요)"라고 적었지만, 동적 QR은 서버 없이는 불가능해서 이 기능에만 Supabase 데이터베이스를 붙였습니다. 일반 QR 생성은 지금도 서버를 거치지 않습니다.

구조는 이렇습니다.

  • 동적 QR을 만들면 서버가 6글자 짧은 주소를 만듭니다. 헷갈리기 쉬운 글자(0, 1, l, o 등)를 뺀 32자 중에서 암호학적 난수로 고르고, 겹치면 최대 5번 다시 뽑습니다.
  • 동시에 추측할 수 없는 관리 토큰을 발급합니다. 계정이 없는 대신 이 토큰이 담긴 관리 링크가 곧 소유권입니다. 만든 기기에도 최근 링크 30개까지 기억해 둡니다.
  • QR을 찍으면 짧은 주소로 들어와 실제 목적지로 넘겨집니다. 이때 기기 종류, 국가, 유입 경로만 기록하고 IP 주소는 저장하지 않습니다. 기록이 실패해도 이동은 막지 않습니다.
  • 관리 화면에서는 최근 14일 스캔 그래프와 최근 스캔 100건을 볼 수 있습니다.

여러 프로젝트가 같은 Supabase 프로젝트를 함께 쓰고 있어서, 충돌을 막기 위해 테이블과 함수 이름에 qrforge_ 접두사를 붙이는 정리도 했습니다.

유료 요금제를 붙였다가 하루 만에 걷어내다

같은 날 저녁 커밋 제목에는 "수익화 ①~③"이 붙어 있습니다. 인쇄 서비스 제휴 링크와 후원 링크를 넣고, 대량 생성 개수·고해상도·PDF 저장을 제한하는 Pro 요금제를 결제 서비스와 연결했습니다. 하단에는 제휴 광고 배너를 달았습니다. 배너를 단 이유는 커밋에 "기본 QR 생성은 무료 경쟁이 치열하므로"라고 적혀 있습니다.

그리고 다음 날, 커밋 제목은 "Pro 유료화 제거, 쿠팡 파트너스 광고로 단순화"입니다. 요금제 관련 파일을 모두 지우고 모든 기능을 무료로 풀었습니다. 하단 고정 배너도 "스크롤/모바일에서 가려질 수 있어" QR 아래 링크 하나로 바꿨습니다. 요금제를 왜 걷어냈는지는 "수익화를 광고 중심으로 단순화"라는 말 외에 기록에 남아 있지 않습니다.

지금의 QRforge

  • 콘텐츠 타입 7가지, 스타일 프리셋 12종, 샘플 로고 10종
  • 점 모양 6가지(사각·원·둥근 사각·다이아몬드·하트·별)
  • 틀 모양 9가지와 표시 방식 3가지(감싸기·변형·오려 내기)
  • 오류 정정 레벨 4단계. 로고를 넣거나 틀을 쓰면 자동으로 가장 높은 H로 올라갑니다.
  • PNG 최대 2048px, 대량 생성 최대 500개

기술적으로는 Next.js와 TypeScript 위에서 qrcode로 칸 격자를 만들고 직접 만든 SVG 렌더러로 그립니다. 스캔 검증에는 jsqr, 대량 생성에는 jszip을 씁니다.

돌아보며, 그리고 남은 숙제

기록을 다시 읽으면 QRforge의 대부분은 "예쁜 것"과 "읽히는 것" 사이의 조율이었습니다. 라이브러리를 버린 것도, 위치 패턴을 따로 그린 것도, 오려 내기를 장식으로 격하한 것도, 스캔 검증을 세 가지 크기로 바꾼 것도 모두 같은 질문에 대한 답이었습니다. 이 모양, 정말 찍히는가?

남은 숙제도 분명합니다. 지금 저장소에는 자동 테스트가 하나도 없습니다. 디코더로 확인한 결과들이 커밋 메시지에만 남아 있고, 다음 수정에서 같은 문제가 다시 생겨도 알아챌 장치가 없습니다. 소개 문서(README)도 이미 지운 라이브러리와 요금제를 여전히 설명하고 있어서 정리가 필요합니다.

QR 코드를 인쇄할 때 확인할 것들은 인쇄해도 잘 찍히는 QR 코드 만드는 법에, 관리 링크 같은 가입 없는 설계는 회원가입 없는 웹 서비스를 만드는 네 가지 방법에 따로 정리했습니다.