QRReader 컴포넌트[FC]

2025. 4. 1. 14:58프론트엔드/REACT

728x90

1. QRReader 컴포넌트 개요

QRReader 컴포넌트는 두 가지 주요 방식으로 QR 코드를 인식합니다.

  • 실시간 카메라 스트림
    사용자의 후면 카메라를 통해 실시간 영상을 받아오고, <canvas> 요소를 사용해 현재 프레임을 캡처합니다. 캡처된 이미지 데이터는 jsQR 라이브러리로 분석되어 QR 코드가 포함되어 있는지 확인합니다.
  • 이미지 업로드 지원
    카메라 사용이 어려운 경우 사용자가 이미지를 업로드하면, 해당 이미지를 <canvas>에 그려 QR 코드를 인식합니다.

추출된 QR 코드가 유효한 티켓 코드(정규 표현식으로 'T'로 시작하고 뒤에 10자리 숫자)인지 검사한 후, API를 호출하여 로그인 혹은 회원가입 처리를 진행합니다.


2. 주요 코드 흐름

2.1. 카메라 스트림과 캔버스 처리

아래 코드는 사용자의 카메라를 활성화하여 실시간 영상을 받아오고, 캔버스를 사용해 프레임 단위로 이미지를 캡처하는 과정을 보여줍니다.

useEffect(() => {
  // video와 canvas DOM 요소에 접근
  const video = videoRef.current;
  const canvas = canvasRef.current;
  // 캔버스의 2D 컨텍스트를 가져오며, 빈번한 읽기를 위해 옵션 지정
  const context = canvas.getContext("2d", { willReadFrequently: true });

  // 사용자의 후면 카메라(환경 모드)로부터 스트림을 받아옴
  navigator.mediaDevices
    .getUserMedia({ video: { facingMode: "environment" } })
    .then((stream) => {
      video.srcObject = stream; // video 태그에 스트림 연결
      video.setAttribute("playsinline", true); // iOS 등에서 전체 화면 방지
      video.play(); // 비디오 재생 시작
      requestAnimationFrame(tick); // 첫 프레임 분석 시작

      // 컴포넌트 언마운트 시 스트림 종료를 위한 클린업 함수 반환
      return () => {
        if (stream) {
          const tracks = stream.getTracks();
          tracks.forEach((track) => track.stop());
        }
      };
    })
    .catch(() => {
      console.error("카메라 접근 불가");
    });

  // 매 프레임마다 호출되는 함수 (tick)
  function tick() {
    // 비디오가 충분한 데이터를 가지고 있고, QR 코드 처리 중이 아니라면
    if (video.readyState === video.HAVE_ENOUGH_DATA && !requestLock) {
      // 캔버스 크기를 비디오 크기에 맞춤
      canvas.height = video.videoHeight;
      canvas.width = video.videoWidth;
      // 현재 비디오 프레임을 캔버스에 그림
      context.drawImage(video, 0, 0, canvas.width, canvas.height);
      // 캔버스의 이미지 데이터를 추출
      const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
      // jsQR 라이브러리를 사용해 이미지 데이터에서 QR 코드 검색
      const code = jsQR(imageData.data, imageData.width, imageData.height, {
        inversionAttempts: "dontInvert", // 반전 시도 옵션
      });
      if (code) {
        // QR 코드가 검출되면 처리 함수 호출
        sendCode(code.data);
      }
    }
    // 다음 프레임을 위해 tick 함수를 재귀 호출
    requestAnimationFrame(tick);
  }
}, [isRequestSent]);

핵심 포인트

  • 카메라 활성화
    • navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })를 통해 후면 카메라 스트림을 받아옵니다.
    • 받아온 스트림은 <video> 태그의 srcObject에 할당되며, playsinline 속성을 설정해 iOS에서 전체 화면 전환을 방지합니다.
    • 스트림이 시작되면 video.play()와 requestAnimationFrame(tick)을 호출하여 매 프레임마다 QR 코드를 검사합니다.
  • 영상 캡처와 QR 코드 분석
    • 매 프레임마다 tick() 함수가 실행되며, 비디오의 현재 프레임을 <canvas>에 그립니다.
    • <canvas>의 getImageData 메서드로 이미지 데이터를 추출하고, jsQR 라이브러리로 해당 데이터를 분석하여 QR 코드를 검색합니다.
    • QR 코드가 검출되면 sendCode 혹은 handleLogin 같은 처리 함수를 호출하여 후속 작업을 진행합니다.

2.2. 이미지 업로드를 통한 QR 인식

카메라 사용이 불가능할 때, 사용자가 이미지 파일을 업로드하여 QR 코드를 인식하는 기능입니다.

const handleImageUpload = (event) => {
  // 업로드된 파일 가져오기
  const file = event.target.files[0];
  if (file) {
    // 파일을 읽기 위한 FileReader 생성
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        // 업로드된 이미지를 그릴 임시 canvas 생성
        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        // 이미지를 canvas에 그림
        ctx.drawImage(img, 0, 0, img.width, img.height);
        // canvas의 이미지 데이터를 추출
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        // jsQR 라이브러리로 QR 코드 분석
        const code = jsQR(imageData.data, imageData.width, imageData.height);
        if (code) {
          // 유효한 QR 코드인지 확인 후 로그인 처리
          if (validateCode(code.data)) {
            handleLogin(code.data);
          } else {
            alert("유효하지 않은 티켓 코드입니다. 'T'로 시작하고 10자리 숫자여야 합니다.");
          }
        } else {
          alert("QR 코드를 인식할 수 없습니다.");
        }
      };
      // FileReader로 읽은 이미지 데이터를 이미지 소스로 지정
      img.src = e.target.result;
    };
    // 파일을 Data URL 형식으로 읽어들임
    reader.readAsDataURL(file);
  }
};

핵심 포인트

  • 파일 읽기와 캔버스 활용
    • 사용자가 파일 입력으로 업로드한 이미지를 FileReader를 통해 읽습니다.
    • 읽어온 데이터는 새로운 Image 객체의 소스로 지정되고, 이미지가 로드되면 임시 <canvas>를 생성하여 이미지를 그립니다.
    • 이후 <canvas>에서 이미지 데이터를 추출하고, jsQR 라이브러리를 사용해 QR 코드를 분석합니다.
  • QR 코드 유효성 검사 및 처리
    • 검출된 QR 코드가 티켓 코드 형식(정규 표현식 /^T\d{10}$/)에 부합하는지 검사합니다.
    • 유효한 코드인 경우 handleLogin 함수를 호출해 로그인 처리를, 유효하지 않으면 사용자에게 경고 메시지를 출력합니다.

2.3. QR 코드 유효성 검사

티켓 코드가 올바른 형식인지 검사하는 함수입니다.

const validateCode = (code) => {
  // 정규 표현식: 'T'로 시작하고 10자리 숫자인지 확인
  const codePattern = /^T\d{10}$/;
  return codePattern.test(code);
};

핵심 포인트

  • 정규 표현식: /^T\d{10}$/를 사용해 티켓 코드가 'T'로 시작하고 뒤에 정확히 10자리 숫자인지 검사합니다.

 

3. 요청 중복 방지

QR 코드를 인식할 때, 한 번 검출된 후에 API 요청이 여러 번 전송되지 않도록 아래 두 변수로 중복 호출을 방지합니다.

  • isRequestSent (React 상태)
    • API 요청이 시작되면 isRequestSent를 true로 설정하여, 이미 요청이 진행 중임을 상태로 관리합니다.
  • requestLock (일반 변수)
    • 함수 내부에서 즉각적으로 요청 중임을 표시하기 위해 사용합니다.
    • handleLogin 함수의 시작 부분에서 if (isRequestSent || requestLock) return;를 통해 이미 요청이 진행 중인 경우 함수 실행을 종료합니다.
    • 요청이 시작되기 전에 requestLock = true;와 setIsRequestSent(true);를 호출해 요청 잠금을 활성화합니다.

예시 코드:

// API 요청 중복을 막기 위한 변수들
const [isRequestSent, setIsRequestSent] = useState(false);
let requestLock = false; // 함수 스코프 변수

const handleLogin = async (code) => {
  // 이미 요청이 진행 중이면 함수 종료
  if (isRequestSent || requestLock) return;
  
  requestLock = true;         // 즉시 요청 잠금
  setIsRequestSent(true);     // 상태 업데이트: 요청 시작
  
  const postData = {
    type: "online",
    ticket: code,
  };
  
  // 쿠키 삭제 등 초기화 작업 수행
  deleteCookie("accesstoken");
  try {
    const response = await instance.post("/user/login", postData);
    // 응답 코드에 따른 처리
    if (response.data.code === "GEN-000") {
      if (response.data.data === "ROLE_USER") {
        navigate("/");
      } else {
        navigate("/register");
      }
      setProgressState((prevProgress) => ({
        progressState: prevProgress.progressState + 100 / 13,
      }));
    } else if (response.data.code === "SEC-007") {
      alert("온라인 예매가 아니거나 인증이 불가한 티켓입니다.");
      navigate("/", { replace: true });
    }
  } catch (error) {
    console.error("Error:", error);
    alert("로그인 중 오류가 발생했습니다.");
    navigate("/", { replace: true });
  }
};

요약하자면:

  • 중복 요청 방지 로직은 한 번 QR 코드가 인식되어 API 요청을 보내면, 이후 같은 프레임 또는 연속된 프레임에서 발생할 수 있는 불필요한 요청을 방지하는 역할을 합니다.
  • isRequestSentrequestLock을 적절히 활용하여, 한 번의 성공적인 요청 후에는 추가 요청이 발생하지 않도록 안전하게 제어합니다.

 

.

728x90