- 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 페이지를 참고해주세요.
하루 100,000개의 요청이면, 현재 제 기준에는 무료 플랜으로 충분합니다.
Playground
Cloudflare Workers를 사용하기 전, Playground를 이용해서 간단하게 테스트해볼 수 있습니다. 또한, 제공된 예제 코드를 보면 어떻게 사용하는지 감을 잡을 수 있습니다.
저는 Playground 에서 약간의 테스트를 해보고 Workers를 사용하기로 했습니다.
- Playground
- 더 자세한 내용은 Cloudflare Workers > Playground 페이지를 참고해주세요.
- 예제 코드들은 Cloudflare Workers > Examples 페이지에서 확인할 수 있습니다.
로컬에서 작업
대시보드 (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 등의 프레임워크를 선택하고 그에 맞게 프로젝트가 만들어집니다)
- "Hello World" Worker: 저는 api 호출이 목적이라 이것으로 선택했습니다.
타입스크립트 사용 여부 선택
git 사용 여부 선택
- git 사용: git init 및 초기 커밋까지 진행됩니다.
배포 여부 선택
- 배포 선택 시 Cloudflare 계정에 로그인하고, 자동으로 만들어진 프로젝트로 배포까지 진행됩니다.
- 배포 선택 시 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
명령어로 확인할 수 있습니다.