본문 바로가기

React

React 앱에서 QR 코드 생성 및 스캐닝 기능 추가하기

QR 코드는 로그인, 결제, 딥링크 공유 등 다양한 UX를 빠르게 개선할 수 있는 도구입니다. 이 글에서는 React 앱에 QR 코드 생성과 스캐닝(카메라 인식) 기능을 간단히 붙이는 실무 가이드를 제공합니다.

1. 라이브러리 선택

생성(Generate): react-qr-code 또는 qrcode.react를 권장합니다. 둘 다 가볍고 SVG 출력이 깔끔합니다. 여기서는 react-qr-code를 사용합니다.

스캔(Scan): react-qr-reader가 접근성이 좋고 구현이 간단합니다. 모바일 브라우저 호환도 양호합니다.

2. 설치

// npm
npm i react-qr-code react-qr-reader

// yarn
yarn add react-qr-code react-qr-reader

// pnpm
pnpm add react-qr-code react-qr-reader

3. QR 코드 생성 컴포넌트

입력한 텍스트/URL을 즉시 QR 코드로 렌더링하고, PNG로 저장하는 예시입니다(SVG를 Canvas로 변환).

import { useState, useRef } from 'react';
import QRCode from 'react-qr-code';

export default function QrGenerator() {
  const [value, setValue] = useState('https://example.com');
  const wrapperRef = useRef(null);

  const downloadPng = () => {
    const svg = wrapperRef.current?.querySelector('svg');
    if (!svg) return;

    const serializer = new XMLSerializer();
    const svgStr = serializer.serializeToString(svg);
    const img = new Image();
    const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
    const url = URL.createObjectURL(svgBlob);

    img.onload = () => {
      const size = 512; // 내보낼 해상도
      const canvas = document.createElement('canvas');
      canvas.width = size;
      canvas.height = size;
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, size, size);
      ctx.drawImage(img, 0, 0, size, size);
      const a = document.createElement('a');
      a.download = 'qr.png';
      a.href = canvas.toDataURL('image/png');
      a.click();
      URL.revokeObjectURL(url);
    };

    img.src = url;
  };

  return (
    <div style={{ maxWidth: 320 }}>
      <label>
        인코딩할 텍스트/URL
        <input
          value={value}
          onChange={(e) => setValue(e.target.value)}
          placeholder="https://your.app"
          style={{ width: '100%', marginTop: 8 }}
        />
      </label>

      <div ref={wrapperRef} style={{ background: 'white', padding: 12, marginTop: 12 }}>
        <QRCode
          value={value || ' '}
          size={256}
          level="M"
          fgColor="#111"
          bgColor="#ffffff"
        />
      </div>

      <button onClick={downloadPng} style={{ marginTop: 8 }}>
        PNG로 다운로드
      </button>
    </div>
  );
}

팁: QR 데이터가 비어 있으면 스캐너가 실패할 수 있으므로 빈 문자열 대신 공백을 넣습니다(value || ' '). 브랜드 컬러 대비가 낮으면 인식률이 떨어지므로 배경은 밝게, 전경은 어둡게 유지합니다.

4. QR 코드 스캐너 컴포넌트

카메라로 QR 코드를 읽고 결과를 일시정지하는 예시입니다. HTTPS 환경에서 테스트하세요.

import { useRef, useState } from 'react';
import { QrReader } from 'react-qr-reader';

export default function QrScanner() {
  const [data, setData] = useState('');
  const [paused, setPaused] = useState(false);
  const lastAtRef = useRef(0);

  const onResult = (result, error) => {
    if (result) {
      const text = result.getText ? result.getText() : result?.text;
      const now = Date.now();
      // 과도한 콜백 방지(0.8초 스로틀)
      if (now - lastAtRef.current < 800) return;
      lastAtRef.current = now;
      if (text) {
        setData(text);
        setPaused(true); // 자동 일시정지로 중복 실행 방지
      }
    }
    // error는 필요 시 로깅 정도로 처리합니다.
  };

  return (
    <div>
      <div style={{ aspectRatio: '1 / 1', maxWidth: 360, position: 'relative' }}>
        {!paused && (
          <QrReader
            constraints={{ facingMode: 'environment' }}
            onResult={onResult}
            videoStyle={{ width: '100%', borderRadius: 12 }}
            containerStyle={{ width: '100%' }}
          />
        )}
        {/* 간단한 조준 가이드 오버레이 */}
        <div style={{
          position: 'absolute', inset: 0, pointerEvents: 'none',
          boxShadow: 'inset 0 0 0 2px rgba(255,255,255,0.7)'
        }} />
      </div>

      <p>인식 결과: {data || '대기 중...'}</p>
      <div style={{ display: 'flex', gap: 8 }}>
        <button onClick={() => setPaused((p) => !p)}>
          {paused ? '스캔 재개' : '스캔 일시정지'}
        </button>
        {data && (/^https?:\/\//.test(data) ? (
          <a href={data} target="_blank" rel="noreferrer">링크 열기</a>
        ) : null)}
      </div>
    </div>
  );
}

보안 팁: 스캔 결과를 자동으로 이동시키지 말고, 사용자 확인 후 이동하도록 합니다. 악성 QR로 인한 피싱을 방지합니다.

5. 권한, HTTPS, 호환성 체크

카메라 접근은 보안 컨텍스트(HTTPS 또는 localhost)에서만 동작합니다. iOS Safari는 파일 스킴/HTTP에서 카메라 접근이 차단됩니다.

// 카메라 사용 가능 여부 사전 점검 (선택)
export async function canUseCamera() {
  if (!navigator.mediaDevices?.getUserMedia) return false;
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    stream.getTracks().forEach(t => t.stop());
    return true;
  } catch (e) {
    return false;
  }
}

SSR(Next.js 등)에서는 스캐너를 클라이언트에서만 로드하세요.

// Next.js 예시: 동적 임포트로 SSR 방지
import dynamic from 'next/dynamic';
const SafeQrScanner = dynamic(() => import('./QrScanner'), { ssr: false });

export default function Page() {
  return <SafeQrScanner />;
}

6. UX/성능 최적화 팁

1) 스캔 상태 관리: 결과가 나올 때 자동으로 일시정지 후, 버튼으로 재개하면 중복 이벤트를 방지합니다. 2) 조도 환경 안내: 어두운 환경에서는 플래시를 켜 달라는 안내를 합니다(Android 일부는 torch 제어 가능). 3) 프레임 제한: 스로틀/디바운스로 onResult 호출을 제한합니다. 4) 폴백 제공: 카메라가 없는 디바이스에는 이미지 업로드로 QR 인식을 제공하는 UX를 고려합니다(zxing/browser 등 활용). 5) 접근성: 중요한 버튼에는 role/label을 명확히 지정하고, 스캔 결과를 라이브 리전에 읽히도록 설정하면 좋습니다.

7. 디버깅 체크리스트

- 브라우저 콘솔에 NotAllowedError: 권한 거부입니다. 설정에서 카메라 허용 또는 페이지를 HTTPS로 서빙합니다. - NotFoundError: 카메라가 없거나 다른 앱이 사용 중입니다. - iOS에서 빈 화면: PWA standalone + iOS에서는 일부 제약이 있으므로 최신 iOS 및 HTTPS에서 테스트합니다. - 인식률 낮음: 대비 부족, 너무 작은 코드, 모션 블러가 원인입니다. QR 크기를 키우고 조명을 개선합니다.

8. 배포 전 점검

1) 배포 URL이 HTTPS인지 확인합니다. 2) 개인정보 처리 고지: 카메라 프리뷰는 인식 용도로만 사용한다고 명시합니다. 3) Lighthouse로 모바일 성능과 접근성을 점검합니다. 4) 다양한 기기(안드로이드/아이폰, 크롬/사파리)에서 실기 테스트를 수행합니다.

위 구성만으로도 대부분의 서비스에 즉시 적용 가능한 QR 생성/스캔 기능을 제공할 수 있습니다. 비즈니스 플로우에 맞춰 결과 처리 로직만 연결하면 끝입니다.