qrcode
서버에서 QR 코드를 동적으로 생성해야 할 때가 있다. 포토부스 세션마다 고유한 다운로드 링크를 QR로 만들어 출력한다거나, 결제 페이지 URL을 QR로 변환해서 화면에 띄우는 경우다. 클라이언트에서 Canvas로 직접 그릴 수도 있지만, 서버에서 생성하면 이미지를 S3에 업로드하거나 PDF에 삽입하거나 프린터로 바로 보내는 등 후처리가 훨씬 유연하다.
qrcode는 Node.js에서 가장 널리 쓰이는 QR 코드 생성 라이브러리다. 브라우저와 Node.js 양쪽에서 동작하고, PNG Buffer, Data URL, SVG, 터미널 출력 등 다양한 출력 형식을 지원한다.
QR 코드의 동작 원리
QR 코드를 제대로 활용하려면 내부 구조를 이해하는 게 도움이 된다. QR 코드는 단순히 "흑백 점 찍기"가 아니라 꽤 정교한 인코딩 과정을 거친다.
데이터 인코딩 모드
QR 코드는 입력 데이터의 종류에 따라 인코딩 모드를 자동으로 선택한다. 모드에 따라 같은 데이터라도 필요한 공간이 크게 달라진다.
| 모드 | 대상 문자 | 효율 |
|---|---|---|
| Numeric | 0-9 | 가장 높음 (3자리 → 10비트) |
| Alphanumeric | 0-9, A-Z, 공백, $%*+-./: | 높음 (2자리 → 11비트) |
| Byte | UTF-8 전체 | 보통 (1바이트 → 8비트) |
| Kanji | Shift JIS 한자 | 2바이트 → 13비트 |
URL을 인코딩하면 소문자가 포함되어 있으므로 Byte 모드가 선택된다. 만약 URL을 대문자로 변환할 수 있다면 Alphanumeric 모드가 적용되어 더 작은 QR 코드를 생성할 수 있다. 실제로 일부 URL shortener들이 대문자 경로를 쓰는 이유 중 하나다.
버전과 크기
QR 코드에는 1~40까지 버전이 있다. 버전 1은 21×21 모듈, 버전이 하나 올라갈 때마다 가로세로 4모듈씩 늘어나서 버전 40은 177×177 모듈이 된다. 데이터가 많을수록 높은 버전이 필요하고, QR 코드가 물리적으로 커진다.
qrcode 라이브러리는 입력 데이터 크기와 오류 정정 레벨에 따라 최소 버전을 자동으로 계산한다. 수동으로 지정할 수도 있지만, 데이터가 해당 버전의 용량을 초과하면 에러가 발생한다.
오류 정정 (Error Correction)
QR 코드의 핵심 기능 중 하나다. Reed-Solomon 오류 정정 코드를 사용해서 QR 코드 일부가 손상되거나 가려져도 데이터를 복원할 수 있다.
| 레벨 | 복원 가능 비율 | 용도 |
|---|---|---|
| L (Low) | ~7% | 깨끗한 환경, 작은 크기 우선 |
| M (Medium) | ~15% | 일반적인 용도 (기본값) |
| Q (Quartile) | ~25% | 인쇄물, 약간의 손상 예상 |
| H (High) | ~30% | 로고 삽입, 거친 환경 |
오류 정정 레벨이 높을수록 더 많은 데이터를 추가로 저장해야 하므로 QR 코드가 커진다. 로고를 중앙에 삽입하려면 H 레벨을 써야 하는데, 같은 데이터라도 L 레벨 대비 QR 코드 크기가 상당히 커진다.
QR 코드의 구조 요소
QR 코드 이미지를 자세히 보면 데이터 영역 외에 여러 고정 패턴이 있다.
- 파인더 패턴 (Finder Pattern): 세 모서리에 있는 큰 사각형. 스캐너가 QR 코드의 위치와 방향을 인식하는 데 사용한다.
- 얼라인먼트 패턴 (Alignment Pattern): 버전 2 이상에서 추가되는 작은 사각형. QR 코드가 곡면에 붙어 있거나 비스듬히 촬영됐을 때 왜곡을 보정한다.
- 타이밍 패턴 (Timing Pattern): 파인더 패턴 사이를 연결하는 흑백 교대 줄. 모듈 좌표를 결정하는 기준선 역할을 한다.
- 포맷 정보: 오류 정정 레벨과 마스크 패턴 정보를 담고 있다.
- 데이터 + 오류 정정 코드워드: 실제 데이터와 Reed-Solomon 코드가 저장되는 영역.
마스크 패턴
인코딩된 데이터를 그대로 배치하면 특정 패턴(큰 흰색/검은색 영역, 파인더 패턴과 유사한 패턴)이 생겨서 스캐너가 혼동할 수 있다. 이를 방지하기 위해 8가지 마스크 패턴 중 하나를 XOR 연산으로 적용한다. 라이브러리가 8가지를 모두 시도해서 가장 균형 잡힌 결과를 자동으로 선택한다.
설치
npm install qrcode
# TypeScript 사용 시
npm install -D @types/qrcode
Node.js 환경에서는 Canvas 기반 PNG 렌더링을 위해 canvas 패키지가 필요할 수 있지만, toBuffer와 toDataURL은 내장 구현을 사용하므로 별도 설치 없이 동작한다. SVG 출력은 순수 문자열 생성이라 의존성이 전혀 없다.
기본 사용법
Data URL 생성
가장 간단한 방법이다. data:image/png;base64,... 형태의 문자열을 반환한다. HTML <img> 태그의 src에 바로 넣을 수 있다.
import * as QRCode from 'qrcode';
const dataUrl = await QRCode.toDataURL('https://example.com');
// "data:image/png;base64,iVBORw0KGgo..."
작은 QR 코드를 인라인으로 보여줘야 할 때 유용하다. 별도 파일 저장이나 URL 관리 없이 바로 렌더링할 수 있기 때문이다. 다만 Base64 인코딩으로 원본 대비 약 33% 크기가 증가하므로, 큰 이미지에는 적합하지 않다.
Buffer 생성
PNG 이미지를 Node.js Buffer로 반환한다. S3 업로드, HTTP 응답, sharp 합성 등 바이너리 처리가 필요할 때 사용한다.
const buffer = await QRCode.toBuffer('https://example.com', {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff',
},
});
// S3 업로드 예시
await s3.putObject({
Bucket: 'my-bucket',
Key: 'qr/session-123.png',
Body: buffer,
ContentType: 'image/png',
});
파일 저장
파일 시스템에 직접 저장한다. 확장자에 따라 PNG 또는 SVG로 자동 선택된다.
// PNG로 저장
await QRCode.toFile('/tmp/qr.png', 'https://example.com', {
width: 300,
margin: 2,
});
// SVG로 저장
await QRCode.toFile('/tmp/qr.svg', 'https://example.com', {
type: 'svg',
});
SVG 문자열 생성
벡터 형식이므로 어떤 크기로 확대해도 깨지지 않는다. 인쇄물이나 고해상도 디스플레이에 적합하다.
const svgString = await QRCode.toString('https://example.com', {
type: 'svg',
width: 300,
margin: 2,
});
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ..."
SVG는 XML 문자열이므로 DOM에 직접 삽입하거나, 서버에서 HTML 템플릿에 인라인으로 넣을 수 있다. PDF 생성 라이브러리에 SVG를 전달하면 래스터 변환 없이 벡터 품질 그대로 삽입된다.
터미널 출력
디버깅이나 CLI 도구에서 유용하다.
const terminalString = await QRCode.toString('https://example.com', {
type: 'terminal',
small: true,
});
console.log(terminalString);
small: true를 설정하면 유니코드 반블록 문자(▀▄█)를 사용해서 한 줄에 두 행을 표현한다. 터미널 공간을 절반으로 줄일 수 있다.
옵션 상세
크기 관련
const options = {
width: 300, // 출력 이미지 너비 (px). 기본값: 자동 계산
scale: 4, // 모듈 1개의 픽셀 크기. width 미지정 시 사용. 기본값: 4
margin: 4, // 여백 (모듈 단위, QR 표준 권장: 4). 기본값: 4
};
width와 scale을 동시에 지정하면 width가 우선한다. QR 코드의 모듈 수에 딱 맞지 않는 width를 지정하면 일부 모듈이 다른 크기로 렌더링될 수 있다. 깔끔한 결과를 원하면 scale을 사용하는 게 낫다.
margin은 QR 코드 주변의 여백이다. QR 표준에서는 최소 4모듈의 quiet zone을 권장한다. 이 여백이 없으면 스캐너가 QR 코드 경계를 제대로 인식하지 못할 수 있다. 디자인상 여백을 줄이고 싶다면 2 정도까지는 대부분의 스캐너에서 문제없이 인식된다.
오류 정정 레벨
const options = {
errorCorrectionLevel: 'M', // 'L' | 'M' | 'Q' | 'H'. 기본값: 'M'
};
앞서 설명한 대로 레벨이 높을수록 복원 능력이 강하지만 QR 코드가 커진다. 화면에만 표시하고 로고 삽입이 없다면 L이면 충분하다. 인쇄해서 사용하거나 중앙에 로고를 넣을 계획이라면 H를 선택해야 한다.
색상
const options = {
color: {
dark: '#000000', // 모듈(점) 색상. 기본값: '#000000'
light: '#ffffff', // 배경 색상. 기본값: '#ffffff'
},
};
투명 배경을 원하면 light: '#0000' (8자리 hex, 알파 0)을 사용한다. 다만 투명 배경에 어두운 배경색이 깔리면 인식률이 떨어질 수 있으니 주의해야 한다.
색상을 커스터마이징할 때 주의할 점이 있다. QR 코드 스캐너는 dark 모듈과 light 배경 사이의 명도 대비로 인식한다. 대비가 낮으면 인식 실패율이 급격히 올라간다. dark를 밝은 색으로, light를 어두운 색으로 반전시켜도 대부분의 스캐너가 인식하지만, 일부 오래된 스캐너에서는 실패할 수 있다.
버전 지정
const options = {
version: 5, // 1~40. 기본값: 자동 (최소 버전 선택)
};
보통은 자동 선택에 맡기는 게 좋다. 수동 지정이 필요한 경우는 여러 QR 코드의 물리적 크기를 통일하고 싶을 때 정도다.
Byte 모드 강제
const options = {
mode: 'byte', // 인코딩 모드 강제 지정
};
자동 모드 선택을 무시하고 특정 모드를 강제할 수 있다. 숫자만 있는 데이터를 Byte 모드로 인코딩하면 QR 코드가 불필요하게 커지므로 일반적으로 쓸 일은 없다. 바이너리 데이터를 직접 인코딩해야 할 때 명시적으로 지정하는 용도다.
세그먼트 분할 인코딩
하나의 QR 코드 안에서 데이터 구간별로 다른 인코딩 모드를 적용할 수 있다. 이렇게 하면 전체를 Byte 모드로 인코딩하는 것보다 QR 코드를 더 작게 만들 수 있다.
const segments = [
{ data: 'HTTPS://EXAMPLE.COM/', mode: 'alphanumeric' },
{ data: 'path/to/page', mode: 'byte' },
];
const dataUrl = await QRCode.toDataURL(segments);
URL의 프로토콜과 도메인은 대문자로 변환해서 Alphanumeric 모드를 적용하고, 소문자가 필요한 경로 부분만 Byte 모드를 사용한다. HTTP URL은 대소문자를 구분하지 않으므로(도메인 부분) 이 방법이 유효하다.
실제로 이 최적화가 의미 있는 건 데이터가 커서 버전 경계에 걸릴 때다. 세그먼트 분할로 한 단계 낮은 버전에 들어갈 수 있다면 물리적 크기가 눈에 띄게 줄어든다.
실전 패턴
NestJS에서 QR 코드 서비스
실제 서비스에서는 QR 생성 로직을 서비스 클래스로 분리하는 게 깔끔하다.
import { Injectable } from '@nestjs/common';
import * as QRCode from 'qrcode';
@Injectable()
export class QrService {
async generateQrImage(url: string): Promise<Buffer> {
return QRCode.toBuffer(url, {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff',
},
});
}
async generateQrBase64(url: string): Promise<string> {
return QRCode.toDataURL(url, {
width: 300,
margin: 2,
});
}
}
여러 곳에서 QR 생성이 필요할 때 옵션을 중앙에서 관리할 수 있고, 나중에 로고 삽입이나 캐싱 같은 기능을 추가하기도 편하다.
sharp와 결합한 이미지 합성
QR 코드를 사진 위에 합성하거나, 로고가 들어간 QR 코드를 만들 때 sharp와 함께 사용한다.
import * as QRCode from 'qrcode';
import sharp from 'sharp';
async function createQrWithLogo(url: string, logoPath: string): Promise<Buffer> {
// 1. QR 코드 생성 (H 레벨로 오류 정정 극대화)
const qrBuffer = await QRCode.toBuffer(url, {
width: 400,
margin: 2,
errorCorrectionLevel: 'H',
color: {
dark: '#000000',
light: '#ffffff',
},
});
// 2. 로고 리사이즈 (QR 전체의 ~15% 크기)
const logo = await sharp(logoPath)
.resize(60, 60, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
.toBuffer();
// 3. QR 중앙에 로고 합성
return sharp(qrBuffer)
.composite([{
input: logo,
gravity: 'center',
}])
.png()
.toBuffer();
}
오류 정정 레벨 H에서는 약 30%까지 손상을 복원할 수 있으므로, 중앙 15% 정도를 로고로 가려도 인식에 문제없다. 다만 로고 크기를 20% 이상으로 키우면 인식 실패가 발생할 수 있으니 꼭 테스트해야 한다.
사진 위에 QR 코드 배치
포토부스처럼 촬영한 사진에 다운로드 QR을 삽입하는 패턴이다.
async function embedQrOnPhoto(
photoBuffer: Buffer,
downloadUrl: string,
): Promise<Buffer> {
const qrBuffer = await QRCode.toBuffer(downloadUrl, {
width: 150,
margin: 1,
errorCorrectionLevel: 'M',
});
return sharp(photoBuffer)
.composite([{
input: qrBuffer,
gravity: 'southeast', // 우하단 배치
blend: 'over',
}])
.jpeg({ quality: 90 })
.toBuffer();
}
HTTP 응답으로 직접 전송
// Express/NestJS 컨트롤러
@Get('qr/:sessionId')
async getQrCode(@Param('sessionId') sessionId: string, @Res() res: Response) {
const url = `https://app.example.com/download/${sessionId}`;
const buffer = await this.qrService.generateQrImage(url);
res.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
'Cache-Control': 'public, max-age=3600',
});
res.send(buffer);
}
QR 코드의 내용이 변하지 않는다면 Cache-Control 헤더로 캐싱을 적용해서 불필요한 재생성을 방지할 수 있다.
브라우저에서 사용
qrcode는 Node.js 전용이 아니다. 브라우저에서도 동작한다.
<canvas id="qr-canvas"></canvas>
<script src="https://unpkg.com/qrcode/build/qrcode.min.js"></script>
<script>
QRCode.toCanvas(document.getElementById('qr-canvas'), 'https://example.com', {
width: 200,
margin: 2,
});
</script>
toCanvas는 브라우저 전용 메서드로, DOM의 Canvas 요소에 직접 QR 코드를 렌더링한다. React에서는 useRef로 Canvas를 참조해서 useEffect 안에서 호출하면 된다.
function QrCodeCanvas({ value }: { value: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (canvasRef.current) {
QRCode.toCanvas(canvasRef.current, value, { width: 200 });
}
}, [value]);
return <canvas ref={canvasRef} />;
}
대안 라이브러리
| 라이브러리 | 특징 | 적합한 상황 |
|---|---|---|
qrcode | 풀 기능, Node + 브라우저 | 대부분의 경우 |
qrcode-generator | 경량, 의존성 없음 | 번들 크기가 중요할 때 |
qrcode-terminal | 터미널 전용 | CLI 도구 |
qr-image | 순수 JS, PNG/SVG/EPS | Canvas 의존성 피하고 싶을 때 |
qrcode가 기능과 생태계 면에서 가장 균형 잡혀 있어서 특별한 이유가 없다면 이걸 쓰면 된다.
주의사항
데이터 용량 한계
QR 코드에 담을 수 있는 데이터량은 제한적이다. 최대 버전(40) + 최소 오류 정정(L) 기준으로도 약 4,296자(영숫자)가 한계다. URL 단축 서비스를 활용해서 데이터를 줄이거나, QR 코드에는 ID만 담고 실제 데이터는 서버에서 조회하는 패턴이 현실적이다.
인식률 확보
- quiet zone(여백)을 최소 2모듈 이상 유지한다
- dark/light 색상 간 명도 대비를 충분히 확보한다
- 로고 삽입 시 오류 정정 레벨 H를 사용하고, 로고 크기를 전체의 15% 이내로 제한한다
- 물리적 인쇄 크기는 스캔 거리에 비례해야 한다 (일반적으로 스캔 거리의 1/10)
보안 고려
QR 코드에 민감한 정보를 직접 담지 않는 게 좋다. QR 코드는 누구나 스캔할 수 있으므로, 토큰이나 개인정보 대신 서버에서 검증 가능한 단기 ID를 사용하고 실제 데이터는 인증 후 서버에서 전달하는 구조가 안전하다.