본문 바로가기
프론트엔드 개발/개발 로그

구글 시트 API를 활용하여 프로모션 신청자 접수하기

by cozyzoey 2022. 6. 18.

적용 배경

이 프로젝트는 프로모션 랜딩 페이지를 만들게 되면서 시작되었습니다.
사용자들이 방문하여 아직 출시되지 않은 제품을 소개하고 사전 예약을 신청할 수 있는 웹페이지인데요.
이를 위해서는 사용자들에게 사전 예약 정보를 받아서 해당 데이터를 저장하는 기능이 필요했습니다.

기존에 사용하는 서버와 DB를 떠나서 가능한 "가볍게" 구현하고자 했는데요. 아래와 같은 요구조건이 있었기 때문입니다.

1. 예상 사전예약 신청자 수가 1천명 미만으로 예상한다는 점
2. 프로모션 진행기간이 3개월 가량으로 단기간 진행된다는 점
3. 개발자 외에 누구나 신청자 현황을 바로 확인할 수 있으면 좋다는 점

 

요구조건을 고려하여 구글 시트 API를 활용하는 것이 좋겠다고 판단했습니다.

서버 개발 공수가 별도로 들어가지 않고 클라이언트단에서 핸들링할 수 있다는 점, API 사용이 무료라는 점(예상하는 신청자 수가 API의 무료 플랜 수준을 초과하지 않음), 누구나 회사 대표 지메일 계정으로 접속하면 구글시트로 신청자 현황을 확인할 수 있다는 점이 좋았습니다.

계획대로라면 프로모션 랜딩 페이지에서 누군가 사전예약을 신청하면 신청 정보가 구글 시트에 쌓이게 되는 것이죠.

 

사전 설정하기

구글 시트 API를 사용하려면 몇 가지 사전 설정이 필요합니다.

우선은 당연히 구글 계정이 필요하겠습니다. 아래의 모든 과정은 해당 대표 계정으로 로그인하여 진행합니다.


  1. GCP에서 해당 프로젝트에 접속합니다. (프로젝트가 없다면 만들어줘야 합니다.)
  2. GCP의 API 라이브러리에서 Google Sheets API를 사용할 수 있도록 활성화 시켜줍니다. 
  3. GCP > APIs & Services > Credentials 에서 API Key를 생성합니다. GAPI를 초기화하려면 이 API Key가 필요합니다. 제한 조건을 걸어줘서 만에 하나 API Key가 노출되더라도 심각한 문제가 생기지 않도록 합니다. 저는 해당 API Key는 HTTP 요청, 특정 도메인, 구글 시트 API만 사용될 수 있도록 제한했습니다.
  4. GCP > APIs & Services > Credentials 에서 서비스 어카운트 및 키(공개/개인 키 쌍)를 생성합니다. 이 서비스 어카운트는 우리의 애플리케이션에서 사용자를 대신하여 구글 OAuth 2.0 인증을 받을 것입니다(서버 간 애플리케이션에 OAuth 2.0 사용). 생성된 이메일과 개인키(Private Key)를 메모합니다.
  5. 구글 드라이브에 스프레드시트 문서를 하나 만듭니다. 이 문서에 신청자 정보가 차곡차곡 쌓이게 될 것입니다. 스프레드시트 우측 상단의 "공유" 버튼을 눌러서 서비스 어카운트 계정의 이메일을 편집자로 추가해 줍니다. 혹은 이 서비스 어카운트에 domain-wide delegation 설정을 해줘야 합니다.(서비스 계정에 도메인 차원 권한 위임) 스프레드시트 ID도 메모해 둡니다. 스프레드시트 ID는 스프레드시트 URL에서 확인할 수 있습니다.

    https://docs.google.com/spreadsheets/d/{spreadSheetDocId}/edit#gid=0

사전 설정은 모두 마쳤습니다. 이제 자바스크립트 코드 상에서 구글 시트 API를 호출해서 스프레드시트를 수정할 수 있도록 합니다.

 

구글 시트 API 호출 과정

애플리케이션에서 구글 시트 API를 호출하려면 아래와 같은 과정이 거치게 됩니다. (승인된 API 호출 준비

  1. JSON  웹 토큰 생성
  2. 구글 OAuth 2.0 서버에 액세스 토큰 요청
  3. 서버가 반환하는 응답 처리

 

첫번째 방법,
JSON 웹 토큰을 직접 만드는 방식

우선 웹페이지가 로드될 때 구글 API를 초기화 해줍니다. 이를 위해서는 사전설정 단계에서 생성한 API Key가 필요합니다. 아래 코드는 Next.js 환경에서 작성된 코드입니다.

<Script
        strategy="afterInteractive"
        src="https://apis.google.com/js/api.js"
        onLoad={() => {
          gapi.load("client", intializeGapiClient);
          async function intializeGapiClient() {
            await gapi.client.init({
              apiKey: process.env.GAPI_KEY,
              discoveryDocs: [
                "https://sheets.googleapis.com/$discovery/rest?version=v4",
              ],
            });
            console.log("GAPI가 초기화되었습니다");
          }
        }}
      />

이제 사용자가 사전예약 신청폼을 작성하고 신청하기 버튼을 클릭하면 아래 코드가 실행됩니다. jsonwebtoken이라는 라이브러리를 사용하여 JSON 웹 토큰을 만들었습니다. JWT의 유효기간은 3분으로 설정했습니다.

  const payload = {
    iss: "서비스 어카운트 이메일",
    scope: "https://www.googleapis.com/auth/spreadsheets",
    aud: "https://oauth2.googleapis.com/token",
  };

  const token = jwt.sign(
    payload,
    publicRuntimeConfig.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY,
    {
      algorithm: "RS256",
      expiresIn: "3m",
    }
  );

  const response = await axios.request({
    url: "/token",
    method: "post",
    baseURL: "https://oauth2.googleapis.com",
    data:
      "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=" +
      token,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });

  const params = {
    spreadsheetId: "스프레드시트 ID",
    range: "applicant!A1:D1",
    valueInputOption: "USER_ENTERED",
    access_token: response.data.access_token,
    resource: {
      values: [data],
    },
  };

  await window.gapi.client.sheets.spreadsheets.values.append(params);

 

두번째 방법,
google-spreadsheet 라이브러리 활용 (권장)

구글 문서에 따르면 구글 OAuth 2.0 시스템에 직접 상호작용하여 JWT를 만들어 사용하는 것은 보안상 문제가 생길 여지가 있어, 구글 API 클라이언트 라이브러리를 사용하도록 권장하고 있습니다.(승인된 API 호출 준비)

권고를 준수하여 google-spreadsheet 라이브러리를 사용하여 코드를 수정하면 아래와 같습니다. data에는 스프레드시트에 추가하고자 하는 사용자 입력 정보가 들어가 있습니다.

const doc = new GoogleSpreadsheet(
    "스프레드시트 ID"
);

await doc.useServiceAccountAuth({
	client_email: "서비스 어카운트 이메일",
	private_key: publicRuntimeConfig.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY,
});

await doc.loadInfo();
const sheet = doc.sheetsByIndex[0];

await sheet.addRow(data, { insert: true });

 

댓글