네트워크가 닿지 않는 두 기기 사이에서 화면에 QR 을 연속 재생하고 카메라로 읽어 파일을 옮기는 방식이 있다. 보내는 쪽은 디렉터리를 tar.gz 로 묶고 base64 로 인코딩한 뒤 일정 길이로 잘라 조각마다 QR 을 만들어 순서대로 화면에 띄우고, 받는 쪽은 카메라로 조각을 모아 다시 이어 붙인다.
이 방식은 수백 KB 까지가 실용 범위다. 그 이상은 만들어지기는 해도 스캔이 끝나지 않는다. 이 문서는 용량 판단 기준과, 실제로 부딪히는 실패 원인을 정리한다.
tar.gz 크기를 S, 조각 길이를 C 자라 하면 장수는 대략 S × 1.33 ÷ C 다. base64 가 3바이트를 4자로 늘리기 때문이다. 이미 압축된 파일이 섞여 있으면 gzip 이 거의 줄여 주지 못한다.
| 원본 | 조각 600자 기준 장수 | 재생 간격 450ms 기준 전송 시간 | 판단 |
|---|---|---|---|
| 300 KB | 약 700 | 약 5분 | 무난하다 |
| 1 MiB | 약 2,300 | 약 20분 | 가능하다 |
| 7 MiB | 약 16,000 | 2시간 이상 | 힘들다 |
| 20 MiB | 약 46,000 | 6시간 이상 | 사실상 불가능하다 |
한 장이라도 놓치면 그 조각이 다시 나올 때까지 한 바퀴를 더 기다려야 하므로 실제 소요는 위 값보다 길다. 바이너리 · 이미지 · .git · node_modules 를 빼고 소스만 남기는 것이 가장 확실한 해결책이다.
병목은 tar 가 아니라 QR 생성이다. 조각 하나마다 QR 인코딩, 이미지 렌더링, PNG 압축, base64 래핑이 붙는다. 기본 설정(오류정정 M, box_size 10)이면 이미지가 커서 PNG 압축 비용도 같이 커진다.
box_size 를 줄이고 PNG 압축 수준을 낮춘다. 오류정정 L 은 장수를 줄여 주지만 카메라 인식이 까다로워지는 맞교환이다.창은 뜨는데 카운터가 오르지 않는 경우가 대부분이고, 원인은 순서대로 셋이다.
OpenCV 의 기본 QRCodeDetector 는 화면을 촬영한 고밀도 QR 을 잘 읽지 못한다. pyzbar 로 바꾸면 같은 조건에서도 대체로 읽힌다.
# pip install pyzbar (Ubuntu: apt install libzbar0 / macOS: brew install zbar)
from pyzbar.pyzbar import decode
text = ""
for r in decode(frame):
text = r.data.decode("utf-8", "ignore")
break
두 디텍터를 모두 시도하도록 짜 두면 특정 조각만 못 읽는 상황에서 성공률이 올라간다.
무한 루프를 도는데 한 조각도 저장되지 않으면 조각 번호 파싱이 실패하는 것이다. 흔한 원인은 번호 패딩이다. 보내는 쪽이 f"{i:04d}/{n:04d}:{data}" 가 아니라 f"{i:4d}/..." 로 되어 있으면 번호가 " 492" 처럼 공백으로 채워져 isdigit() 검사에서 떨어진다. 파싱에서 공백을 허용하면 기존 QR 을 다시 만들지 않아도 된다.
def parse(text):
head, sep, data = text.partition(":")
if not sep or "/" not in head:
return None
idx, tot = head.split("/", 1)
idx, tot = idx.strip(), tot.strip() # 공백 패딩 허용
if not (idx.isdigit() and tot.isdigit()):
return None
return int(idx), int(tot), data
받은 조각을 복원 폴더 안의 상태 파일에 번호를 키로 저장하고, 임시 파일에 쓴 뒤 이름을 바꾸는 방식으로 갱신한다. 중간에 리더를 껐다 켜도 이어받을 수 있다. 복원에 성공하면 상태 파일을 지운다. 다른 전송을 시작할 때는 반드시 새 폴더를 쓴다. 안 그러면 이전 조각이 섞인다.
전체 기준으로 계산하면 첫 바퀴 내내 아직 순서가 오지 않은 번호까지 미수신으로 잡혀 화면이 쓸모없어진다.
need = expected_ids()
if store:
need = {i for i in need if i <= max(store)} # 아직 순서가 안 온 것은 제외
missing = sorted(need - set(store)) if need else []
종료 조건은 이 missing 이 아니라 전체 번호 집합이 모두 모였는가로 둔다.
조각이 500 장이고 간격이 2초면 한 바퀴가 16분이다. 마지막 몇 장을 받으려고 매번 한 바퀴를 기다리는 것이 전체 시간의 대부분을 차지한다. HTML 에 번호 목록 입력칸과 재생 간격 조절을 넣어 두면 몇 초 만에 끝난다.
리더는 번호별로 저장하므로 순서는 상관없다. 이미 받은 번호는 건너뛰고, 마지막 하나가 들어오는 순간 복원이 시작된다.
특정 번호만 계속 안 들어올 때, 로그에 "이미 수신됨" 도 "저장" 도 찍히지 않으면 리더 로직까지 도달하지 못한 것이다. 즉 디텍터가 그 QR 자체를 해독하지 못했다는 뜻이고, 코드를 고칠 일이 아니라 촬영 조건을 바꿔야 한다. 반대로 "해독은 됐는데 형식이 안 맞는다" 는 로그가 찍히면 디텍터가 데이터를 일부 잘못 읽은 것이다. 두 경우를 구분해 출력하도록 로그를 나눠 두면 헛수고를 줄인다.
이미 받은 조각이 다시 보일 때 아무 로그도 찍지 않으면 "갑자기 안 읽힌다" 고 오해하기 쉽다. 건너뛴 것도 한 줄 남긴다.