공공데이터 API 를 붙이면 인증키가 하나 생깁니다.
그 키를 앱 코드에 그대로 적으면 당장은 잘 돌아갑니다. 문제가 드러나는 건 한참 뒤입니다.
이번 포스팅에서는 인증키를 클라이언트에서 걷어내고 Cloudflare Workers 뒤로 옮기는 방법을 정리해 보도록 하겠습니다.
먼저 확인할 것
2026년 9월 기준입니다. Cloudflare Workers 무료 플랜과 wrangler 4 계열에서 확인했습니다.
앱스인토스 미니앱을 예로 들지만 구조 자체는 웹·리액트 네이티브 어디서나 같습니다.
그러면 왜 앱 안에 두면 안 되는지부터 보겠습니다.
번들에 적은 문자열은 그대로 읽힙니다
리액트 네이티브 번들은 난독화가 아니라 압축일 뿐입니다.
앱스인토스 제출물인 .ait 파일 안의 bundle.*.js 는 아예 압축도 되어 있지 않습니다.
저희는 이 사실을 좋은 일로 알게 됐습니다. 로컬 소스를 통째로 날렸을 때 번들 안에서 원문을 다시 건져냈기 때문입니다.
복구가 됐다는 건 누구나 그 안의 문자열을 읽을 수 있다는 뜻이기도 합니다.
공공데이터 키가 새면 일일 한도가 남의 트래픽으로 소진됩니다. 키를 바꾸려면 배포된 앱을 전부 다시 올려야 합니다.
키를 들고 있는 쪽을 한 대만 둡니다
저희는 얇은 워커 한 대가 미니앱 50개의 요청을 전부 받습니다.
앱은 키를 모릅니다. 앱이 아는 것은 워커 주소뿐입니다.
키는 코드나 설정 파일이 아니라 시크릿으로 넣습니다.
npx wrangler secret put DATA_GO_KR_KEY
npx wrangler secret put SEOUL_KEY
이렇게 넣은 값은 env 로만 읽힙니다. 리포지터리에도 빌드 산출물에도 남지 않습니다.
export default {
async fetch(req: Request, env: Env) {
const url = new URL(req.url);
if (req.method === 'OPTIONS') return preflight();
if (url.pathname === '/air') {
const sido = url.searchParams.get('sido') ?? '서울';
const stations = await airBySido(env.DATA_GO_KR_KEY, sido);
return json({ stations }, { maxAge: 900 });
}
return fail(404, 'not_found', '없는 경로입니다.');
},
};
앱 쪽 코드는 이 한 줄이 전부입니다.
const res = await fetch(`${PROXY}/air?sido=${encodeURIComponent(sido)}`);
캐시는 선택이 아니라 필수입니다
공공데이터 개발 계정은 보통 하루 1,000건입니다.
앱이 늘면 그 한도는 금방 찹니다. 그래서 워커 안에서 한 번 더 캐시합니다.
export async function cached<T>(key: string, ttl: number, produce: () => Promise<T>) {
const cache = caches.default;
const req = new Request(`https://cache.internal/${encodeURIComponent(key)}`);
const hit = await cache.match(req);
if (hit) return (await hit.json()) as T;
const value = await produce();
await cache.put(req, new Response(JSON.stringify(value), {
headers: { 'Cache-Control': `public, max-age=${ttl}` },
}));
return value;
}
미세먼지는 20분, 날씨는 20분처럼 데이터가 실제로 바뀌는 주기에 맞춰 TTL 을 답니다.
키를 가리려고 만든 프록시가 한도까지 같이 지켜 줍니다.
여기서 실제로 넘어진 곳이 넷 있습니다
첫째, wrangler.toml 에 [[routes]] 를 추가하면 workers_dev 가 자동으로 꺼집니다.
기본값이 꺼짐이기 때문입니다. 저희는 이걸 모르고 라우트를 넣었다가 미니앱 50개에 박혀 있던 약관 주소와 아이콘 주소가 한꺼번에 404 가 됐습니다.
둘째, 그 workers_dev = true 는 모든 테이블 헤더보다 위에 적어야 합니다.
TOML 은 헤더 뒤에 나오는 키를 그 테이블 소속으로 읽기 때문입니다. 아래에 두면 triggers.workers_dev 가 되어 조용히 무시됩니다.
셋째, 무료 플랜 워커 번들은 3MiB 가 상한입니다.
이미지나 영상을 base64 로 워커에 넣으면 금방 걸립니다. 미디어는 R2 에 두고 워커는 흘려보내기만 하는 쪽이 맞습니다.
넷째, 공공데이터 포털은 키를 두 벌 줍니다. Encoding 키를 써야 합니다.
Decoding 키를 쓰면 SERVICE_KEY_IS_NOT_REGISTERED_ERROR 가 떠서, 키가 아직 승인되지 않은 것으로 오해하기 쉽습니다.
내려주는 값도 같이 줄입니다
프록시를 뒀으니 상류 응답을 그대로 넘길 이유가 없습니다.
저희는 앱이 실제로 쓰는 필드만 남겨서 내려보냅니다. 응답이 작아지면 느린 네트워크에서 체감이 달라지기 때문입니다.
상류가 값을 문자열 "-" 로 주는 것처럼 지저분한 부분도 여기서 null 로 정리합니다.
앱이 여러 개면 이 정리를 한 곳에서만 하면 됩니다. 그게 프록시를 두는 두 번째 이유입니다.
여섯 줄로 줄이면
- 번들의 문자열은 읽힙니다. 키를 앱에 두면 언젠가 새어 나갑니다.
- 키를 아는 쪽을 워커 한 대로 모으고, 값은
wrangler secret put으로 넣습니다. - 앱은 워커 주소만 압니다. 키를 바꿔도 앱을 다시 배포하지 않습니다.
- 상류 한도 때문에 캐시는 필수입니다. TTL 은 데이터가 바뀌는 주기에 맞춥니다.
[[routes]]를 넣을 땐workers_dev를 명시하고, 위치는 모든 헤더 위입니다.- 무료 플랜 번들 3MiB 를 넘기지 않도록 미디어는 R2 로 뺍니다.
지금 앱 코드에 인증키 문자열이 들어 있다면, 다음 배포 전에 한 번 검색해 보시는 건 어떨까요?
그러면 오늘도 모두 스테이블 하세요.
만든 것들
'Coding, Testing, Challenge' 카테고리의 다른 글
| 앱스인토스 공식 문서대로 하면 안 되는 것, 정본은 .d.ts 입니다 (0) | 2026.09.29 |
|---|---|
| 앱스인토스 미니앱 개발 환경 세팅, 윈도우에서 막히는 네 곳 (0) | 2026.09.28 |
| 미니앱 광고, 배너광고에서 전면광고로 바꾸니 수익이 스무 배가 됐습니다 (0) | 2026.09.26 |
| 한 줄 고치려다 53개 파일의 한글을 전부 날렸습니다 (0) | 2026.09.24 |
| 100MB를 15밀리초에 보냈다고 믿었습니다 (0) | 2026.09.23 |