Promise 야매 캐싱으로 API 호출 횟수 줄이기
호출 횟수 제한은 무섭다
현황
이 블로그는 Notion API를 사용하고 있습니다. 이 API는 초당 평균 3회라는 호출 횟수 제한을 가지고 있습니다. SSG(ISR)로 정적 사이트 생성을 하고 있고 호출 횟수 제한은 시간당 10800회니까 글 수가 엄청나게 방대해지지 않는 이상 문제가 없고, 조금 추하지만 글이 너무 많아지면 글을 제외하고 사이트를 빌드한 후 글만 따로 포스팅해도 될 일입니다. ISR revalidate 주기는 1시간으로 지정해 놓았고, 그림 파일만 필요할 때 클라이언트 사이드에서 리로드하고 있으니 빌드 시점이 아니라면 10800회를 넘을 일은 없어 보입니다.
현재 시점에서 블로그 프로덕션 코드가 푸시되어 빌드될 때에 API를 몇 회나 호출하는지 살펴 봤습니다.
POSTLIST
FETCH 40회 (글 리스트)BLOCK
FETCH 103회 (글 본문을 쪼갠 것)
포스트는 현재 20개인데, 전체 호출 수는 143회였습니다. 많긴 하지만 이 정도면 십 년은 거뜬해 보이는데요…
글 수에 비해 호출 횟수가 이렇게 많은 이유는 크게 두 가지 때문인데, POSTLIST
의 경우 Next.js에서 동적으로 라우트 및 메타데이터를 생성하기 위해 전체 글 리스트를 포스트마다 중복호출하기 때문이고 BLOCK
의 경우에는 Notion API의 특수성 때문인데, 본문 내용을 쪼개어 재귀호출해야 하기 때문입니다. 왜 이렇게 호출해야 하는지에 대해서나, Notion API의 구조에 대해 더 알고싶으신 분들은 읽어보시면 도움이 됩니다.
인 메모리 프로미스 캐싱
아직 널널하지만 어쨌든 신경이 쓰이는 건 어쩔 수 없는 것 같습니다. 제 블로그 방문자가 큰 기업 테크 블로그처럼 많아지지는 않겠지만 그냥 최적화 측면에서 저 호출 횟수가 불편해 보이기 때문입니다. 같은 데이터를 불필요하게 여러 번 가져오고 있는데 그걸 가만두고 볼 개발자가 있을까요…
문제는 배포 환경입니다. 저는 Vercel을 이용해 편하게 배포하고 있었는데, 이걸 위해 redis, upstash, kv같은 걸 추가로 쓰기는 아쉬운 상황입니다. 이사를 가거나 리모델링하기는 싫고 지금 있는 가구만 잘 옮겨서 발 뻗고 눕고 싶은 욕심이라고 할까요. 그래서 인 메모리에 야매로 캐싱을 해 보는 것으로 결론을 냈습니다.
ISR을 이용해 페이지 자체는 캐싱이 되기 때문에 빌드 타임에만 캐싱을 해 주면 되니, 아주 짧은 시간 동안에만 유효한 데이터가 메모리에 할당되어 있으면 되지 않을까 싶었습니다. vercel이 주는 메모리 환경도 널널하고, 블로그 포스트 내부의 그림 파일같은 것들은 파일 객체가 아니어서, 링크 형태로 저장해 뒀다가 로드하기 때문에 메모리에 부하가 적을 것이라고 판단했습니다.
1. 일단 막 구현하기
interface CACHE {
id: string;
timestamp: string;
data: PostListObject;
}
모듈 전역에 객체를 두고, 초기 호출에 데이터를 저장한 뒤 그 다음부터는 캐시에 저장한 것을 빼내 오면 되지 않을까 생각했습니다. 타임스탬프를 두고 짧은 시간이 지난 뒤에는 데이터를 지우고 재호출하는 식으로요. 그대로 구현했는데, 캐시가 작동하지 않았습니다. 아래는 POSTLIST 로그의 일부입니다. CACHE
라는 단어가 하나도 보이질 않습니다. 여러 번 다시 빌드해 봐도 어쩌다 한두 번 보이기는 했지만, 제대로 작동한다고 보기는 어려웠습니다.

사실 저는 이 원인을 생각도 못 하고 있다가 며칠 전에 TIL 페이지를 만들고, 글 리스트 id값이 두 개가 되면서 단서를 발견하게 됐습니다. 빌드가 이루어지는 과정의 로그를 살펴보면 7702
로 끝나는 것과 f819
로 끝나는 것 두 종류의 id값이 규칙성 없이 번갈아서 나오는 것을 보실 수 있습니다. next build
로 페이지를 빌드할 때 순차적으로 빌드하는 게 아니라 병렬적으로 빌드한다는 것을 추론해볼 수 있습니다. (Next.js
는 내부적으로 webpack
과 swc
를 사용합니다)
이렇게 병렬 빌드를 하게 되면 일반적인 모듈의 전역 변수는 제대로 작동하지 않을 가능성이 높습니다. 왜냐하면 첫 번째 비동기 요청이 완료되기 전에 다른 정적 페이지 빌드가 시작되어 버릴 가능성이 있기 때문입니다.
2. Promise Memoization
const CACHE: Record<string, Promise<PostListObject>> = ...
병렬 빌드에 따른 해결 방법은 생각보다 빨리 생각났는데, 인터페이스를 이렇게만 바꾸고, 비동기 요청의 결과값이 아닌 Promise를 저장하는 것입니다. 동시성 문제나 ISR 시점에서의 정보 불일치 문제 때문에 저장한 타임스탬프 같은 상태 정보도 다 떼 버리고요.
Promise는 pending
상태였다가 fulfilled
또는 rejected
로, 자동으로 상태가 변화되는 특성을 가지고 있어 동시성 문제를 알아서 해결하고, .then
체인 또는 await
으로 그 결과값을 Single Source로서 사용할 수 있기 때문입니다.
이렇게 캐싱한 후의 결과를 살펴보겠습니다.

전체 요청 143개 중 캐시된 것은 41개이고, 그 중 캐시된 글 리스트 API의 개수를 세어 보면 34개입니다. 글 리스트를 40번 요청하고 있었는데 6번 호출하는 것으로 줄였습니다. 로그를 잘 보면 2종류의 글 리스트 API (7702
와 f819
) 를 3회씩 호출하고 있는 것을 보실 수 있습니다.
즉 본문 캐싱은 여러 번 호출될 일이 없기 때문에 사실 (예상하다시피) 무용합니다. 그럼에도 블록에 대한 캐싱을 적용한 이유는 작동할 여지가 있어서인데, 본문이 아니라 글 리스트 데이터베이스에 대한 메타데이터를 담고 있는 블록(1c9f
)이 존재해서입니다. (Notion API는 페이지나 데이터베이스도 블록으로 간주하지만, 다른 API 엔드포인트를 사용합니다). 아래에 전체 로그를 제공하니, 로그를 잘 검색해 보시면 나머지 7개가 하나의 캐싱된 블록을 불러오고 있는 것을 보실 수 있습니다.
마무리 및 후기
솔직히 85% 줄였다고 하고 싶은데 표본이 적어서 어폐가 있는 것 같고요, 제대로 된 캐싱도 아니고, 인 메모리 캐싱이어서 기술적으로 대단한 것은 아니지만 자바스크립트를 간만에 제대로 쓴 것에 의의가 있다고 생각합니다. 프론트엔드 빌드 환경에 대해 공부했던 것이 번뜩이고 지나가서 이런 문제도 해결해 보네요. 블로그를 만들 때부터 찝찝한 것이었는데 이렇게 해결하게 되어서 기분이 좋습니다.
전체 빌드 로그
데이터베이스의 전체 id값 등 필요하지 않은 정보는 제외했습니다
[16:23:06.320] Running build in Washington, D.C., USA (East) – iad1
...
[16:23:26.595] ****************************7702 >>>> POSTLIST FETCH CALL
[16:23:26.611] ****************************f819 >>>> POSTLIST FETCH CALL
[16:23:27.878] Generating static pages (0/30) ...
[16:23:29.577] Generating static pages (7/30)
[16:23:29.577] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.577] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.577] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.577] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.578] ****************************7702 >>>> POSTLIST FETCH CALL
[16:23:29.578] 1b9d9500-6769-42dd-bc7c-71f2f921a773 >>>> BLOCK FETCH CALL
[16:23:29.578] b56915e4-a7a3-4b7c-ade7-1ba387b34d6c >>>> BLOCK FETCH CALL
[16:23:29.578] 941d8639-4d6a-4acc-9b33-5d08f896959a >>>> BLOCK FETCH CALL
[16:23:29.579] bd637800-2097-40f9-8d6e-5ed6c7478c6b >>>> BLOCK FETCH CALL
[16:23:29.580] 1252e507-b0e8-460d-a8fc-b47a34c80df4 >>>> BLOCK FETCH CALL
[16:23:29.580] 770d171b-d2e9-4dfe-a2e8-f17a0fc1e8ca >>>> BLOCK FETCH CALL
[16:23:29.580] 434e3d87-7014-4c01-a64d-c5bbf491001d >>>> BLOCK FETCH CALL
[16:23:29.580] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.580] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.580] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.580] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.580] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.580] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.580] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.581] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.581] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.581] ****************************1c9f >>>> BLOCK FETCH CALL
[16:23:29.581] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.581] ****************************7702 >>>> CACHED POSTLIST
[16:23:29.581] ****************************7702 >>>> POSTLIST FETCH CALL
[16:23:29.581] ****************************f819 >>>> POSTLIST FETCH CALL
[16:23:29.581] 161c0364-d05b-8056-8a12-ed11d0e4dae5 >>>> BLOCK FETCH CALL
[16:23:29.581] 2d114dbd-358b-491c-a841-8b9f67d81541 >>>> BLOCK FETCH CALL
[16:23:29.920] cc28c086-c596-4bb9-a47d-12b0f3b74612 >>>> BLOCK FETCH CALL
[16:23:30.380] 6c4b8b07-4deb-415d-a294-54d99dedec3c >>>> BLOCK FETCH CALL
[16:23:30.380] f8d9ab5f-765c-47a2-881e-dad4934b18ee >>>> BLOCK FETCH CALL
[16:23:30.380] 97e41f60-9964-4763-96da-32a16fd9cf61 >>>> BLOCK FETCH CALL
[16:23:30.380] 00aeab51-cc0c-462f-b491-d4f842af1be2 >>>> BLOCK FETCH CALL
[16:23:30.380] 1adf037f-e30b-4246-ac10-8ad12b0091b9 >>>> BLOCK FETCH CALL
[16:23:30.380] e4c9ac4a-c99f-4c10-bef5-78b4f2f026a6 >>>> BLOCK FETCH CALL
[16:23:30.381] 5a991c3c-ecb1-43e4-afb1-617121815f05 >>>> BLOCK FETCH CALL
[16:23:30.381] 231c4110-cb58-4128-b059-728d02b20301 >>>> BLOCK FETCH CALL
[16:23:30.520] Generating static pages (14/30)
[16:23:31.094] 135c0364-d05b-8001-a860-eda543ddafa6 >>>> BLOCK FETCH CALL
[16:23:31.094] 135c0364-d05b-80a9-996d-c41ede3558f4 >>>> BLOCK FETCH CALL
[16:23:32.068] Generating static pages (22/30)
[16:23:32.068] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.068] ****************************7702 >>>> CACHED POSTLIST
[16:23:32.068] ****************************7702 >>>> CACHED POSTLIST
[16:23:32.068] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.068] ****************************7702 >>>> CACHED POSTLIST
[16:23:32.068] ****************************7702 >>>> CACHED POSTLIST
[16:23:32.069] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************1c9f >>>> CACHED BLOCK
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.069] ****************************f819 >>>> CACHED POSTLIST
[16:23:32.330] c8f6527a-3e1c-4791-baaa-b4c5fddc244b >>>> BLOCK FETCH CALL
[16:23:32.331] 011bd5f2-ad0d-426a-a0b9-e4fa38858aca >>>> BLOCK FETCH CALL
[16:23:32.388] ****************************f819 >>>> POSTLIST FETCH CALL
[16:23:33.023] 163c0364-d05b-8089-b1e2-d34e63ce4216 >>>> BLOCK FETCH CALL
[16:23:33.023] 15fc0364-d05b-8022-8670-e32154a82935 >>>> BLOCK FETCH CALL
[16:23:33.024] 163c0364-d05b-8090-ba54-ea905d8d3fbe >>>> BLOCK FETCH CALL
[16:23:33.024] 163c0364-d05b-80ad-8a19-ff49291b39a4 >>>> BLOCK FETCH CALL
[16:23:33.024] 163c0364-d05b-80fa-964b-e8f818f97fa5 >>>> BLOCK FETCH CALL
[16:23:33.024] 8a7fe1b8-3ae4-4d20-9f51-28a58d1f9c18 >>>> BLOCK FETCH CALL
[16:23:33.024] 6c80fd39-de05-4046-b4f2-0dab0d7d0b1c >>>> BLOCK FETCH CALL
[16:23:33.024] 6a604a6a-070a-4af6-829a-6280a6c20948 >>>> BLOCK FETCH CALL
[16:23:33.024] 69f17ac3-d542-45ff-a684-3046e93b3df8 >>>> BLOCK FETCH CALL
[16:23:33.024] e2e36074-5f97-463f-97bf-0cf9f07443ea >>>> BLOCK FETCH CALL
[16:23:33.024] 69c0e8c8-853a-4073-9ce9-28ecce2ff0a3 >>>> BLOCK FETCH CALL
[16:23:33.446] f1641929-99aa-464c-9083-11763afba6c9 >>>> BLOCK FETCH CALL
[16:23:33.446] f1bc63b9-22b4-4269-bc7e-bc028527f27b >>>> BLOCK FETCH CALL
[16:23:33.446] d7354f31-6c71-4932-be9f-c88e5a7597e0 >>>> BLOCK FETCH CALL
[16:23:33.446] 113ed6e1-1ad2-4b76-84f7-2919569e3c13 >>>> BLOCK FETCH CALL
[16:23:33.446] 13c2a201-7f2c-404f-96a5-ace1a0815673 >>>> BLOCK FETCH CALL
[16:23:33.446] be32b29c-a054-481e-99b9-1063a7d3fcf3 >>>> BLOCK FETCH CALL
[16:23:33.446] bd654394-03f5-46f6-9cfa-91fe653e2d07 >>>> BLOCK FETCH CALL
[16:23:33.446] 9e7030b5-ea22-4eda-98dc-23d0b7d5f4d2 >>>> BLOCK FETCH CALL
[16:23:33.446] 1afa505f-d242-4df9-8aab-5e902c6654ed >>>> BLOCK FETCH CALL
[16:23:33.446] f4d1ec8c-3d1b-4557-94f0-fd581a3ebae0 >>>> BLOCK FETCH CALL
[16:23:33.446] 2136082d-071c-48a6-b228-2193580ae897 >>>> BLOCK FETCH CALL
[16:23:33.446] b72eb31d-9edc-4c59-9943-3d6991ceaa41 >>>> BLOCK FETCH CALL
[16:23:33.446] f68afabf-efb1-4cc4-9d5b-97927e985c9a >>>> BLOCK FETCH CALL
[16:23:33.446] 7582a658-4d76-4738-8259-1f5c51d10a85 >>>> BLOCK FETCH CALL
[16:23:33.446] a0c69c9d-5d74-4c8e-a8be-ecf5b76e5c3a >>>> BLOCK FETCH CALL
[16:23:33.446] 55d1d7aa-2ef6-4e90-9d42-3161faeaf759 >>>> BLOCK FETCH CALL
[16:23:33.447] be5b73bc-e85f-41a1-9146-01438bae0ce7 >>>> BLOCK FETCH CALL
[16:23:33.447] 54f50ea9-ecb6-40dc-aca9-637ab05230cd >>>> BLOCK FETCH CALL
[16:23:33.447] 87449dc1-ade3-4644-8d18-abc4b80099d9 >>>> BLOCK FETCH CALL
[16:23:33.447] f3001dd8-16e1-475c-b077-d5efcc2c1d3e >>>> BLOCK FETCH CALL
[16:23:33.447] 3fc055f7-b985-4b15-a611-76d6b432894b >>>> BLOCK FETCH CALL
[16:23:33.447] d5fd9abe-d7ea-4ca2-a284-833cc255d63a >>>> BLOCK FETCH CALL
[16:23:33.666] b93b44fa-196e-4de7-878f-f0cd8f591d2a >>>> BLOCK FETCH CALL
[16:23:33.666] e5ae209d-8d19-4479-9b1a-4dce920b72e2 >>>> BLOCK FETCH CALL
[16:23:33.666] 4a88d44a-8396-4d78-afb9-588b2b228a1e >>>> BLOCK FETCH CALL
[16:23:33.667] 203e512c-4cea-4a5e-889c-480d9e7d7d7a >>>> BLOCK FETCH CALL
[16:23:33.667] aa665394-3398-4bae-a5ff-6941e2829b90 >>>> BLOCK FETCH CALL
[16:23:33.667] 10e6d367-d2cc-428d-a213-efd83a43511f >>>> BLOCK FETCH CALL
[16:23:33.667] aca02632-380e-43ac-ad81-a75c01b27b21 >>>> BLOCK FETCH CALL
[16:23:33.667] 66b300bc-c5e0-427b-9144-fddf1cce1590 >>>> BLOCK FETCH CALL
[16:23:33.668] 0e394b61-76b2-442b-a47f-e980c07a5ded >>>> BLOCK FETCH CALL
[16:23:33.668] d3ececc7-88ca-4a16-ba80-5d8966b89a6e >>>> BLOCK FETCH CALL
[16:23:33.668] 158b83be-64b6-4b29-9b0f-62943373ad14 >>>> BLOCK FETCH CALL
[16:23:33.668] ea0ba911-77f8-4789-a76b-6d5ffe1b819d >>>> BLOCK FETCH CALL
[16:23:33.668] 97c267e2-34ef-4342-bfb5-6ead27ba0b6b >>>> BLOCK FETCH CALL
[16:23:33.669] f944007a-9f64-4d02-9c1d-f72b26c18293 >>>> BLOCK FETCH CALL
[16:23:33.669] 097d111b-f250-453e-a994-d3a8e4b2296a >>>> BLOCK FETCH CALL
[16:23:34.080] bf394e83-b233-458b-9407-8acec29e2f9e >>>> BLOCK FETCH CALL
[16:23:34.080] de68369f-e72e-4d47-9514-04e5d7def130 >>>> BLOCK FETCH CALL
[16:23:34.081] f7ed3534-024d-4c32-b110-ac94787aa138 >>>> BLOCK FETCH CALL
[16:23:34.081] a6db945d-1b39-46b4-87d6-de3468dc4a79 >>>> BLOCK FETCH CALL
[16:23:34.081] 60792e91-7897-46ed-9d8a-c98294d9d877 >>>> BLOCK FETCH CALL
[16:23:34.081] 2b8aed24-2075-4476-9da2-7a200c27fe39 >>>> BLOCK FETCH CALL
[16:23:34.081] c5298a67-25b5-4a16-97f7-0471ec74fb66 >>>> BLOCK FETCH CALL
[16:23:34.081] f4ed5be3-cc1a-4ccd-b334-a5263d5a9dc9 >>>> BLOCK FETCH CALL
[16:23:34.081] ac7a89ef-ef21-4e03-bb61-31144f79d621 >>>> BLOCK FETCH CALL
[16:23:34.081] e18849f6-5360-4e59-9213-a9db701e301b >>>> BLOCK FETCH CALL
[16:23:34.081] e46ae503-76bd-40f9-a95e-098b2812ad22 >>>> BLOCK FETCH CALL
[16:23:34.081] 40af39af-f990-4ddb-aea1-7f83ef04d438 >>>> BLOCK FETCH CALL
[16:23:36.900] ✓ Generating static pages (30/30)
[16:23:38.144] Finalizing page optimization ...
[16:23:38.144] Collecting build traces ...
...
[16:23:58.915] Uploading build cache [135.35 MB]...
[16:24:00.494] Build cache uploaded: 1.579s