Logo
Published on
·11 min read

서버리스 API, Cloudflare Workers로 json 데이터 제공하기

들어가며

이전 글에서 교육부 영어 단어를 검색하고, 각 단어의 뜻을 스크래핑하도록 했습니다.

그렇게 만든 자료를 가공해서, 조건(초등/중고등/그 외)에 따라 데이터를 json으로 제공하고 싶습니다.
(제공된 데이터는 React Native로 만들고 있는 앱에서 사용할 예정입니다.)

그렇게 찾은 것이 Cloudflare Workers입니다.

Cloudflare Pages

저는 Cloudflare Pages를 이용해서 정적 웹 서비스를 운영하고 있습니다. 여기에 json 파일을 올려서 사용할까 생각했습니다.

하지만, 저의 요구 사항은 아래와 같았습니다.

  • 하나의 데이터로 요청(초등/중고등/그 외)에 따라 다른 결과물(json) 제공
  • Header를 체크해서 허용된 요청 외에는 접근 차단

그래서 Cloudflare Workers를 사용하기로 했습니다.

Cloudflare Workers

서버리스 애플리케이션을 빌드하고 전 세계에 즉시 배포하여 우수한 성능, 신뢰성 및 확장성을 제공합니다. 모든 요금제(무료/유료)에서 사용 가능합니다.

Cloudflare Workers는 인프라를 구성하거나 유지보수하지 않고도 새로운 애플리케이션을 만들거나 기존 애플리케이션을 보완할 수 있는 서버리스 실행 환경을 제공합니다.

가격

가격 정책은 계속 변경될 수 있으니 Cloudflare Workers > Pricing 페이지를 참고해주세요.

Cloudflare Workers 가격 정책 캡쳐

2023.11.30 기준 Cloudflare Workers 가격

하루 100,000개의 요청이면, 현재 제 기준에는 무료 플랜으로 충분합니다.

Playground

Cloudflare Workers를 사용하기 전, Playground를 이용해서 간단하게 테스트해볼 수 있습니다. 또한, 제공된 예제 코드를 보면 어떻게 사용하는지 감을 잡을 수 있습니다.

저는 Playground 에서 약간의 테스트를 해보고 Workers를 사용하기로 했습니다.

로컬에서 작업

대시보드 (Workers 및 Pages > 응용 프로그램 생성) 에서 프로젝트를 생성하고 바로 코드 작성 및 배포가 가능합니다.

하지만 저는 앱에서 사용할 수 있도록 데이터를 가공하고, 연동 테스트를 위해 로컬에서 작업을 진행했습니다.

Cloudflare Workers > Get started > Guide 페이지를 참고했습니다.

전제 조건

  • Cloudflare 계정
  • Node.js / npm 설치

프로젝트 생성

아래 명령어를 통해 프로젝트를 생성합니다.

npm create cloudflare@latest

위 명령어를 실행하면, 디렉토리명/프로젝트명 지정 및 타입을 선택하는 과정이 진행됩니다.

  • 디렉토리명/프로젝트명 지정

    • 디렉토리명이 프로젝트 및 도메인명이 됩니다.
    • 프로젝트명을 길게 적으니 도메인명이 너무 길어져서, 새로 프로젝트를 생성했습니다.
      (대시보드에서 수정해도 되긴 하지만, 저는 삭제 후 다시 생성했습니다.)
  • 어플리케이션 타입 선택

    • "Hello World" Worker: 저는 api 호출이 목적이라 이것으로 선택했습니다.
      ("Website or web app" 선택 시 React / angular / vue 등의 프레임워크를 선택하고 그에 맞게 프로젝트가 만들어집니다)
  • 타입스크립트 사용 여부 선택

  • git 사용 여부 선택

    • git 사용: git init 및 초기 커밋까지 진행됩니다.
  • 배포 여부 선택

    • 배포 선택 시 Cloudflare 계정에 로그인하고, 자동으로 만들어진 프로젝트로 배포까지 진행됩니다.

터미널에서 진행된 내용은 아래 "진행 과정 펼치기" 버튼을 눌러 확인할 수 있습니다.

  • 진행 완료 후 대시보드에서 확인해보면, Workers 및 Pages디렉토리명/프로젝트명으로 프로젝트가 생성된 것을 확인할 수 있습니다.

  • 생성된 도메인으로 접속해보면, 'Hello World!'가 출력되는 것을 확인할 수 있습니다.

로컬에서 서버 실행

터미널에서 다음 명령어를 통해 로컬에서 서버를 실행합니다.

npm run start
[mf:inf] Ready on http://*:8787
[mf:inf] - http://ip:8787
[mf:inf] - http://127.0.0.1:8787
[mf:inf] - http://localhost:8787
[mf:inf] - http://[::1]:8787

위와 같이 로컬 서버가 실행되고, 접속 가능한 주소가 표시됩니다.
그리고 브라우저에서 확인하면 정상적으로 'Hello World!'가 출력되는 것을 확인할 수 있습니다.

예제 코드

아래와 같은 요청을 처리하는 코드를 작성해보겠습니다.

  • /api/words: 전체 단어
  • /api/words/e: 초등 단어
  • /api/words/mh: 중고등 단어
  • /api/words/etc: 기타 단어
  • 그 외 요청: 404 (Not found)
  • Header 확인 후 맞지 않으면: 403 (Forbidden)

index.js

src/index.js

  • 요청 url에 따라 다른 데이터를 반환하는 코드입니다.
import { wordsDataFilter } from "./wordsDataHandler";

export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);

		if (url.pathname.startsWith("/api/words")) {
			const resultData = await wordsDataFilter(url); // 요청 url에 따라 다른 데이터를 반환하는 함수
			return Response.json(resultData);
		}

		// 404
		return new Response("Not found", { status: 404 });
	},
};

단어 목록

src/wordsData.js

// 예제 단어 목록 object
export default {
	data: [
		{
			word: 'a / an',
			mean: '영어 알파벳의 첫째 자, 특정한 하나의, ~ 이라는 것',
			group: [ 'E' ],
		},
		{
			word: 'abandon',
			mean: '버리다, 방종, 버리고 떠나다',
			group: [],
		},
		{
			word: 'able',
			mean: '…할 수 있는, 재능 있는',
			group: [ 'M', 'H' ],
		},
		{
			word: 'aboard',
			mean: '탄, 탑승한',
			group: [],
		},
		// ... 생략
		{
			word: 'zebra',
			mean: '얼룩말',
			group: [ 'M', 'H' ],
		},
		{
			word: 'zero',
			mean: '0, 영에 맞추다, 영도',
			group: [ 'M', 'H' ],
		},
		{
			word: 'zone',
			mean: '지역, 지역로 정해 두다, 구획짓다',
			group: [],
		},
		{
			word: 'zoo',
			mean: '동물원',
			group: [ 'E' ],
		}
	]
}

단어 목록 필터링

src/wordsDataHandler.js

import wordsData from "./wordsData.js";

export async function wordsDataFilter(url) {
	const originData = wordsData.data;
	let filterData = wordsData.data;

	if (url.pathname === "/api/words/e") {
		filterData = originData.filter((word) => {
			return word.group.includes('E');
		});
	} else if (url.pathname === "/api/words/mh") {
		filterData = originData.filter((word) => {
			return word.group.includes('M') || word.group.includes('H');
		});
	} else if (url.pathname === "/api/words/etc") {
		filterData = originData.filter((word) => {
			return word.group.length === 0;
		});
	}

	return filterData;
}

확인

  • http://127.0.0.1:8787/ : Not Found 표시되는 것을 확인
  • http://127.0.0.1:8787/api/words : 전체 단어가 출력되는 것을 확인
  • http://127.0.0.1:8787/api/words/(e/mh/etc) : 초등/중고등/그 외 단어가 출력되는 것을 확인
그 외 단어가 출력되는 것을 확인

HEADER 확인 코드

모든 요청에 응답하지 않고, 특정 HEADER가 있는 요청에만 응답하도록 설정하고 싶습니다.

src/index.js 최종 코드는 다음과 같습니다.

import { wordsDataFilter } from "./wordsDataHandler";

// Header Key/Value 설정
const AUTH_HEADER_KEY = "X-My-Auth-Header";
const AUTH_HEADER_VALUE = "blahblahblah";

export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);

		// Header 체크
		const clientAppHeader = request.headers.get(AUTH_HEADER_KEY)
		if(clientAppHeader !== AUTH_HEADER_VALUE) {
			return new Response("Forbidden", { status: 403 });
		}

		if (url.pathname.startsWith("/api/words")) {
			const resultData = await wordsDataFilter(url);
			return Response.json(resultData);
		}

		// 404
		return new Response("Not found", { status: 404 });
	},
};

확인

  • 브라우저에서 http://127.0.0.1:8787/을 포함한 모든 요청에 대해 Forbidden 표시되는 것을 확인
  • postman 등에서 위 코드에서 지정한 Header Key/Value 포함하여 요청시 정상적으로 응답되는 것을 확인
응답 확인

배포

다음 명령어를 실행하여 배포합니다.

npm run deploy

대시보드에서 배포된 것을 확인할 수 있습니다.

앱에서 호출

React Native 앱에서 호출하는 간략한 예시 입니다.

export const getTest = async () => {
  const url = `https://도메인/api/words/etc`;

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'X-My-Auth-Header': 'blahblahblah' // <-️ Header Key/Value 설정
    }
  });

  return await response.json();
}

로컬 환경에서 확인 시

앱 개발 환경에서 로컬 서버 (http://127.0.0.1:8787/api/words/etc) 호출 시 Network request failed 에러가 발생할 수 있습니다.

이떄는 아래 명령어를 통해 포트 포워딩을 해주어야 합니다.

adb -s 디바이스_이름 reverse tcp:8787 tcp:8787

참고로, 디바이스_이름adb devices 명령어로 확인할 수 있습니다.

[영진닷컴]그림으로 배우는 서버 구조, 영진닷컴  생활코딩! Node.js 노드제이에스 프로그래밍, 위키북스  자바스크립트 완벽 가이드, 인사이트
(위 링크는 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.)