1. Client-Side에서는 API Key를 숨길 수 없다

Front-End 개발을 하다보면, Youtube API 와 같은 Third Pary API를 사용하여 개발을 하게 될 때가 있습니다. API Key를 발급받은 후 fetch를 이용하여 데이터를 받는 것은 비교적 간단합니다. 다만, 문제가 되는 것은 API Key를 URL에 parameter로 같이 보냄으로써 API Key가 노출된다는 점입니다.

그렇다면 Client Side에서 API Key를 숨긴채 Third Pary API와 통신할 수는 없는 것일까요?

결론은 '불가능하다'입니다.

Third Party API는 API Key를 URL의 parameter로 같이 보내야지만 제대로된 응답을 보내줍니다. Client-Side에서 직접 Third Party API와 통신하는 경우에는 개발자 도구의 Networks 탭 등을 이용하여 Request 의 헤더를 다 확인할 수 있으므로, URL의 parameter로 같이 보내지는 API Key는 무조건 노출되게 됩니다.

만약 Server Side에서 Third Party API 와 통신한 후 그 결과를 Client-Side로 보내준다면 이와 같은 문제를 겪지 않을 텐데... 라는 생각이 들지 않으신가요?

2. Netlify Functions를 이용해 나만의 Redirect Server를 만들어보자

Serverless Function 이라는 개념을 도입하여 간단히 나만의 Redirect Server를 만들수 있습니다.

Serverless Function은 백엔드 서비스를 단일 목적으로만 사용할 수 있는 서비스입니다. Serveless Function은 클라우드 컴퓨팅 회사에서 호스팅하고 유지관리되므로, Serveless Function을 통해 유저는 인프라에 대한 걱정 없이(서버 관리에 대한 걱정 없이) Server-Side 코드를 작성하고 구현할 수 있습니다.

즉, 서버 관리가 전혀 필요없이 단일 목적으로만 사용되는 Server 라고 생각하시면 좋을 것 같습니다. 이번 블로그에서 사용할 Serverless Function은 Netlify Functions입니다. (Netlify Functions는 내부적으로 aws lambda를 사용하고 있습니다.)

아래의 그림과 함께 간단히 흐름을 설명해보겠습니다.

0. Redirect Server에 API Key를 저장해둡니다.

1. Client-Side: Youtube API Server와 직접 통신하지 않고, Redirect Server와 통신합니다.
(Redirect Server는 이미 API Key를 알고 있으므로 API Key를 첨부하지 않습니다)

2. Redirect Server: Client-Side로부터 받은 Request에 API Key를 붙여서 Youtube API Server에 요청합니다. Youtube API Server로 받은 데이터를 Client-Side로 반환합니다.

이제는 구체적인 Netlify Functions 설정방법에 대하여 알아보겠습니다.

3. 설정방법

  1. 아래의 repository를 fork 합니다.

  2. netlify 에 github repository를 등록합니다.

    • Netlify signup 링크
    • fork한 repository 와 main 브랜치를 선택하며, 이외의 설정(Build Command, Publish Directory)은 공란으로 두세요. (Functions directory 설정은 netlify.toml에서 하고 있으므로 신경쓰지 않아도 됩니다.)
  3. netlify 에 환경변수를 설정합니다.

    아래의 환경변수는 반드시 설정해야 합니다. 또한 환경변수를 설정한 후에는 반드시 deploy를 하여야 합니다. 새로이 deploy한 후에 변경된 환경변수가 적용됩니다.

    • API_KEY: Youtube API Key

    • HOST: CORS 를 위한 Origin으로, Response 헤더의 Access-Control-Allow-Origin: HOST 로 설정됩니다.

     예시) *,  https://bigsaigon333.github.io, http://localhost:5500, http://127.0.0.1:5500 

     ※ HOST는 하나만 설정할 수 있습니다. 따라서, 두군데 이상을 설정하고자 하는 경우에는 * 로 하여야 합니다.


⇒ 이로써 설정을 모두 마쳤습니다. 환경변수 설정후 deploy하는거 꼭 잊지 마세요!


4. Client-Side 사용법

기존에는 Youtube API Endpoint 인 https://www.googleapis.com/youtube/v3/search 으로 직접 통신하였다면 이제부터는 방금 만든 Netlify Functions의 Endpoint와 통신하시면 됩니다.

// 기존
https://www.googleapis.com/youtube/v3/search

// Endpoint
https://my-netlify-site-name.netlify.app/youtube/v3/search

// 🌟New Feature: dummy data를 반환하는 Endpoint🌟
https://my-netlify-site-name.netlify.app/dummy/youtube/v3/search

구체적인 Client-Side 사용법 예시

try {
  // const ORIGINAL_HOST = "https://www.googleapis.com"; // 기존 유튜브 API 호스트
  const REDIRECT_SERVER_HOST = "https://bigsaigon333.netlify.app"; // my own redirect server hostname

  const url = new URL("youtube/search", REDIRECT_SERVER_HOST);
  const parameters = new URLSearchParams({
    part: "snippet",
    type: "video",
    maxResults: 10,
    regionCode: "kr",
    safeSearch: "strict",
    pageToken: nextPageToken || "",
    q: query,
    // key: "Abdsklfulasdkf-d0f9"     // key를 절대로 포함해서 보내지 마세요!
  });
  url.search = parameters.toString();

  const response = await fetch(url, { method: "GET" });
  const body = await response.json();

  if (!response.ok) {
    throw new Error(body.error.message); //  <-- 이렇게 하시면 디버깅하실때 매우 편합니다.
  }

  // write a code below that you want to do here!
} catch (error) {
  console.error(error);
}


5. (심화) Server-Side Code 파헤치기

const fetch = require("node-fetch");
const querystring = require("querystring");
const stringify = require("../utils/stringify.js");

const GOOGLEAPIS_ORIGIN = "https://www.googleapis.com";
const headers = {
  "Access-Control-Allow-Origin": process.env.HOST,
  Vary: "Origin",
  "Cache-Control": "max-age=86400",
  "Content-Type": "application/json; charset=utf-8",
};

exports.handler = async (event) => {
  const { path, queryStringParameters } = event;

  const url = new URL(path, GOOGLEAPIS_ORIGIN);
  const parameters = querystring.stringify({
    ...queryStringParameters,
    key: process.env.API_KEY,
  });

  url.search = parameters;

  try {
    const response = await fetch(url);
    const body = await response.json();

    if (body.error) {
      return {
        statusCode: body.error.code,
        ok: false,
        headers,
        body: stringify(body),
      };
    }

    return {
      statusCode: 200,
      ok: true,
      headers,
      body: stringify(body),
    };
  } catch (error) {
    return {
      statusCode: 400,
      ok: false,
      headers,
      body: stringify(error),
    };
  }
};

6. 추가 업데이트 사항

🌟 New Feature: Dummy Data를 반환하는 Endpoint 추가

Youtube API는 일일 제한량이 있으므로 이를 초과하여 사용한 경우에 403 Error를 보냅니다.

Youtube API를 사용하지 않아 제한량에 영향을 주지 않고, 서버에 저장되어 있는 Dummy Data를 랜덤하게 반환하는 Endpoint를 추가하였습니다.
(현재 33종류의 데이터를 랜덤으로 반환합니다. )

// 🌟 New Feature: dummy data를 반환하는 Endpoint 🌟
https://my-netlify-site-name.netlify.app/dummy/youtube/v3

🛠 Fix: Fetch Error 메세지 속 API_KEY를 그대로 반환하는 에러 수정

zych1751 님께서 유튜브 에러를 그대로 내려줄 경우 에러 메세지 속에 API_KEY가 포함되어 있어 API_KEY가 노출될 수 있는 점을 지적해주셨습니다.

이에 JSON.stringify의 replacer로 API_KEY를 모두 빈 문자열("")으로 치환하는 함수를 전달하여, response의 body 내 API_KEY가 절대 포함되지 않도록 수정하였습니다.

// stringify.js
const keyReplacer = (_, value) => {
  if (typeof value !== "string") {
    return value;
  }

  return value.replace(process.env.API_KEY, "");
};

const stringify = (subject) => JSON.stringify(subject, keyReplacer, " ");

module.exports = stringify;

※ 추후 추가하고 싶은 내용

profile
Front-End Programmer

9개의 댓글

comment-user-thumbnail
2021년 3월 8일

레포만 보다가 이거 보니깐 이해가 더 잘가네용!! 좋은글 감사합니다🙇‍♀️

1개의 답글
comment-user-thumbnail
2021년 3월 8일

글 잘 봤습니다!멋져요 동동👍👍👍

1개의 답글
comment-user-thumbnail
2021년 3월 8일

정말 도움 많이 됐습니다!! 감사해요😍😍😍 어떻게 이런 생각을!!

1개의 답글
comment-user-thumbnail
2021년 3월 12일

글 잘봤습니다 :)

이 글의 주제와는 좀 벗어나지만, 서버에서 유튜브 에러를 그대로 내려주면 api key가 노출될 수도 있는 점 참고만 해주세요.

{
"message": "invalid json response body at https://www.googleapis.com/youtube/v3/youtube?key=AIzaSyCHVzh37vU***** reason: Unexpected end of JSON input",
"type": "invalid-json"
}

2개의 답글