엘리스 AI트랙 4기/프로젝트

[PWA] Push Notification 안드로이드 QR 코드로 구독하기

남쪽마을밤송이 2022. 7. 25. 05:17
  • 지난 PWA 시리즈에 이어서 작성한 내용입니다.
  • 한동안 코딩 테스트 준비로 프로그래머스 문제에 집중하다가 다시 작성!!

 

 [01 QR code란? ] 

  • QR 코드(Quick Response code)는 컴퓨터가 만든 흑백 격자무늬 패턴 코드로, 정보를 나타내는 매트릭스 형식의 이차원 코드이다.
  • 기존의 바코드는 기본적으로 가로 배열에 최대 20여 자의 숫자 정보만 넣을 수 있는 1차원적 구성이지만, QR코드 원리는 가로, 세로를 활용하는 2차원적 구성이다.
    • 숫자 : 최대 7,079
    • 문자 : 최대 4,296
    • 한자 : 최대 1,817
  • QR 코드의 자세한 작동 구조가 궁금하다면 아래 참고에서 확인한다.

참고 : https://codingcoding.tistory.com/95 

 

 [02 QR code 생성 ] 

 (1) 외부 라이브러리 결정 

  • QR code를 생성해주는 라이브러리는 정말 많지만, 나는 사용하기 쉽고 가운데 로고를 넣는 기능을 제공하는 quickchart.io를 사용했다.
  • 다음과 같이 경로를 설정하면 반환값이 바로 QR code 이미지이기 때문에 편하게 사용할 수 있다.
    • 나는 처음에 axios로 요청을 보내야하는 줄 알았는데 그냥 React 컴포넌트 img 태그 안에서 src를 해당 링크로 설정해주는 것만으로 QR code가 잘 출력된다!
https://quickchart.io/qr?text=Here's%20my%20text&dark=f00&light=0ff&ecLevel=Q&format=svg\

 (2) img 호스팅 서버 

  • 개인 S3를 사용하면 좋지만 AWS 과금 문제 때문에 함부로 사용하기가 무섭기(?) 때문에..! 그럴 때 편하게 사용할 수 있는 무료 이미지 호스팅 서버들이 있다.
  • 보안은 보장하지 못하니 중요한 문서보다는 이렇게 프로젝트에서 사용할 이미지 정도 업로드하고 사용하기 좋을 것 같다.
  • 내가 사용한 곳은 ifh.cc 여기인데 무료 버전은 만료 기한이 있어서 나중에 찾은 imgbb가 더 좋은 것 같다.
  • 아래와 같이 링크로 간단히 접근할 수 있다! QR code 가운데에 넣을 이미지 경로 준비 완료!!
<img src="https://i.ibb.co/TRS5yP4/favico4.png" alt="favico4" border="0">
favico4

 (3) QR code 

  • QR code로는 데이터를 전송하거나 redirect할 URL을 넣어줄 수 있다.
    • 내가 사용한 quickchart.io 라이브러리는 text 파라미터에 그 값을 넣어주면 됐다.
    • link는 구독 버튼을 누를 수 있는 모바일용 페이지를 encoding하여 넣어주었다.
    • 모바일 접근시 사용자 인증을 위해 query string으로 JWT Token을 암호화 전송했는데, 해당 내용은 다음 포스팅에서 다룰 예정이다.
    • 그외에도 공식문서를 참고해 ecLevel과 size, centerImageUrl 옵션을 설정해주었다.
const QRcode = `https://quickchart.io/qr?text=${encodedPageLink}&ecLevel=L&size=200&centerImageUrl=https://ifh.cc/g/Y4Z5z3.png`;
더보기

모달창 component

<input type="checkbox" id="QRcode" className="modal-toggle" />
<label htmlFor="QRcode" className="modal cursor-pointer">
  <label className="modal-box relative" htmlFor="">
    <h2 className="card-title mb-1 text-teal-500">영양제 일정 알림 구독 서비스</h2>
    <div className="divider">Web</div>

    <div className="items-center justify-center flex flex-row space-x-2 p-2">
      <button
        onClick={subscribe}
        className="px-4 py-1 text-sm text-teal-600 font-semibold rounded-full border border-teal-200 hover:text-white hover:bg-teal-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-600 focus:ring-offset-2"
      >
        구독하기
      </button>
      <button
        onClick={unsubscribe}
        className="px-4 py-1 text-sm text-teal-600 font-semibold rounded-full border border-teal-200 hover:text-white hover:bg-teal-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-600 focus:ring-offset-2"
      >
        구독 취소
      </button>
    </div>

    <div className="divider">Mobile</div>
    <div className="items-center justify-center flex flex-col">
      <img src={useMemo(() => QRcode, [])} alt="QRcode" width="170" height="170" className="rounded-xl" />
      <h3 className="text-slate-900 mt-5 text-base font-medium tracking-tight">모바일 알림 구독 QR</h3>
      <p className="text-slate-500  mt-2 text-sm text-left">
        영양제 일정 알림 서비스는 현재 ios에서 지원되지 않아 Android 또는 Web에서만 가능합니다. Android 접속시 google 애플리케이션의 google lens를 이용하시는 것을 추천드립니다.
      </p>
    </div>
  </label>
</label>

 

 

 [03 구독 기능 연결하기 ] 

 (1) 구독 / 구독 취소 버튼 

  • 연결된 페이지에서 구독과 구독 취소 기능을 이용할 수 있도록 버튼을 각각 만들어주었다.
  • 해당 함수는 Web에서 구독, 구독 취소할 때 사용하는 함수와 같다.
더보기

/mobile/Subscribe.tsx

/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable no-unreachable */

// 사용자가 모바일에서 qrcode 타고 넘어온 화면
// qrcode에 parameter로 jwt_token값 들어 있음
// 해당 jwt_token이랑 device_token으로 구독 정보 추가하도록 backend에 요청

import React from "react";
import axios from "axios";
import { AES, enc } from "crypto-js";

const Subscribe = () => {
  const queryString = new URLSearchParams(window.location.search);
  let encryptedToken: any = queryString.get("token");
  encryptedToken = encryptedToken.replaceAll(" ", "+");
  console.log(encryptedToken);

  const secretKey: any = process.env.REACT_APP_SECRET_KEY;
  const decryptedToken = AES.decrypt(encryptedToken, secretKey);
  const jwtToken = decryptedToken.toString(enc.Utf8);

  const subscribe = async () => {
    const sw = await navigator.serviceWorker.ready;
    // 사용자 기기 정보로 구독 요청
    const subscription = await sw.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: process.env.REACT_APP_WEB_PUSH_PUBLIC_KEY,
    });

    // 사용자 기기 정보 DB에 추가
    await axios
      .post(
        `${process.env.REACT_APP_MODE}:${process.env.REACT_APP_BACK_PORT}/subscribe/create`,
        { device_token: subscription },
        {
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${jwtToken}`,
          },
        },
      )
      .then(() => {
        alert("구독 신청이 완료되었습니다.");
      });
  };

  const unsubscribe = async () => {
    console.log("unsubscribe function");
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();
    if (!subscription) {
      alert("구독 정보가 없는 기기입니다.");
      return;
    }

    // 사용자 기기 정보 DB에서 삭제
    await axios
      .post(
        `${process.env.REACT_APP_MODE}:${process.env.REACT_APP_BACK_PORT}/subscribe/delete`,
        { device_token: subscription },
        {
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${jwtToken}`,
          },
        },
      )
      .then(() => {
        alert("구독이 취소되었습니다.");
      });

    // 사용자 기기 정보로 구독 취소 요청
    await subscription.unsubscribe();
  };

  return (
    <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Subscribe our Service</h2>
          <br />
          <p className="mt-2 text-center text-sm text-gray-600">
            Web 혹은 Android에서만 가능합니다. <br />
            알림 권한 요청에 "허용"을 눌러주세요.
          </p>
        </div>
        <div className="flex justify-center">
          <button
            className="px-4 py-1 text-sm text-teal-600 font-semibold rounded-full border border-teal-200 hover:text-white hover:bg-teal-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-600 focus:ring-offset-2"
            onClick={() => subscribe()}
          >
            구독하기
          </button>
        </div>
        <div className="flex justify-center">
          <button
            className="mb-32 px-4 py-1 text-sm text-teal-600 font-semibold rounded-full border border-teal-200 hover:text-white hover:bg-teal-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-600 focus:ring-offset-2"
            onClick={() => unsubscribe()}
          >
            구독 취소하기
          </button>
        </div>
      </div>
    </div>
  );
};
export default Subscribe;

 

 

 (2) 고려할 점 

  • 나는 각각의 버튼을 분리해놓는게 좋다고 판단해서 QR 접속 이후 본인이 클릭하도록 했지만, QR code 접속시 바로 구독이 되도록 하려면 subscribe( ) 함수를 useEffect 훅에 넣는 방식으로 수정할 수 있을 것 같다.

  • 여기까지 해서 기능구현은 모두 완료가 되었는데, 이후 개선한 점은 외전이라고 붙여서 올릴 예정이다.