Notion API 초기 페이지 로딩 속도 개선
- Notion API
들어가며
노션 API를 활용해 블로그를 만들고자 했다. 노션에서 작성한 글을 그대로 웹에 게시할 수 있다는 점이 가장 컸다. 별도의 관리자 페이지나 글쓰기 에디터를 새로 만들 필요 없이, 평소 쓰던 노션 페이지에 글을 쓰고 저장만 하면 그대로 블로그 글이 되는 구조였다. 게시글 목록 / 분류 / 태그 관리도 노션의 데이터베이스 기능을 그대로 가져다 쓸 수 있어, 별도의 백엔드 데이터베이스를 새로 설계하고 운영할 필요가 없었다.
이 구조를 그대로 살려 노션 API로 게시글 데이터베이스를 구성하고, 클라이언트에서 이를 조회하도록 구현했다. 제목과 날짜 같은 기본 정보 외에도, 분류 / 검색 / 미리보기라는 세 가지 목적에 맞춰 속성을 나눠 설계에 반영했다. 데이터베이스에는 다음과 같은 속성을 설정했다.

- name: 게시글 제목
- category: 전체 | 회고 | 학습정리 | 트러블슈팅
- date: 작성 날짜
- tag: 관련 태그들
- summary: 간략한 요약
아래와 같이 데이터베이스를 기반으로 전체 게시글 수와 카테고리별 게시글 수를 표시하도록 만들었다.

또한 한 페이지당 10개의 게시글을 불러오고, 페이지 이동을 위해 페이지네이션 기능을 구현했다.

문제점
초기에는 간단하고 직관적인 구조였다. 하지만 게시글 수가 증가하고 초기 로딩 속도를 개선하려던 과정에서 두 가지 문제점을 발견했다.
1. 페이지 네이션
현재 상황
페이지네이션을 구현하기 위해서는 정렬된 게시글 목록 중 특정 인덱스 범위(예: 21~30번째 게시글)를 가져와야 한다. 하지만 노션 API는 정렬된 상태에서 임의의 구간을 직접 지정해 가져오는 기능을 제공하지 않는다.
노션 API는 page_size와 start_cursor 매개변수를 제공하지만, 이는 커서 기반 페이지네이션이다. start_cursor는 특정 인덱스가 아니라 이전 응답의 마지막 항목을 가리키는 값이라, 3페이지(21~30번째)로 바로 이동하려면 1페이지와 2페이지를 먼저 순서대로 요청해 커서를 얻어야 했다. 사용자가 페이지 번호를 눌러 원하는 페이지로 곧장 이동하는 화면 구조와는 맞지 않았다.
순차적으로 커서를 따라가며 페이지를 하나씩 채우는 방법도 고려했지만, 그러면 3페이지로 이동할 때마다 앞선 페이지들을 매번 다시 요청해야 했다. 결국 전체 데이터를 한 번에 조회한 뒤 클라이언트 측에서 필요한 부분만 나누어 표시하는 방식을 택했다. 현재는 API의 최대 제한인 100개 단위로 데이터를 가져오도록 구현되어 있다.
문제점
처음에는 큰 문제가 없어 보였지만, 매번 전체 데이터를 가져오는 방식에 점점 의문이 들었다. 실제로 29개의 게시글 데이터를 요청했을 때, 응답으로 약 3800줄에 달하는 JSON이 전송되는 것을 보고 경악했다.

노션 API는 각 페이지의 상세한 블록 정보까지 포함해 응답하기 때문에 데이터량이 급격히 증가한다. 게시글 29개에서 3800줄이었으니, 단순 비례로만 계산해도 100개가 되면 응답이 13,000줄을 넘는 셈이었다. 게시글이 늘어날수록 초기 로딩 시간과 네트워크 비용이 함께 불어날 것이 명확했다.
필터 조회로 특정 속성만 가져오면 게시글 한 건당 응답 크기는 줄어든다. 하지만 페이지네이션에 필요한 건 21~30번째처럼 특정 구간의 게시글이었고, 그 구간을 정확히 잘라내려면 여전히 전체 게시글을 순서대로 다 가져와야 했다. 줄어드는 건 게시글 하나의 크기일 뿐, 가져와야 하는 게시글의 개수는 그대로였다.
2. 총 게시글 수의 계산
현재 상황
페이지 상단에는 총 29개의 글 처럼 게시글의 총 개수를 표시하고 있다. 하지만 노션 API는 데이터베이스 전체 페이지 개수를 직접 반환하는 엔드포인트를 제공하지 않는다. COUNT(*) 같은 쿼리도, total_count 필드도 지원하지 않았고, 목록을 조회하는 응답 어디에도 전체 개수를 알려주는 값은 없었다.
결국 앞서 페이지네이션을 위해 이미 전체 데이터를 조회하고 있었으므로, 그 배열의 results.length로 총 개수를 계산하는 쪽을 택했다. 처음에는 “어차피 데이터를 모두 가져온다면, 이 시점에서 계산하면 되지 않을까?" 라는 생각으로 페이지네이션 계산과 총 개수 계산을 한 곳에서 함께 처리했다.
문제점
원래라면 총 게시글 수는 단일 카운트 값만 가져오면 되는 상수 시간 연산이어야 한다. 하지만 현재 구조에서는 총 개수를 알기 위해 모든 게시글의 속성 데이터를 통째로 API로 요청하고 받아야 한다. 앞서 본 3800줄짜리 응답에서 실제로 쓰는 값은 배열의 길이, 숫자 하나뿐이었다. 이 과정에서 네트워크 트래픽과 JSON 파싱 비용은 게시글 수에 비례해 계속 늘어난다.
- 네트워크: 3800줄 JSON 전송
- 클라이언트: 대용량 JSON 파싱 →
length읽기 - 전체 비용: 게시글 수 N에 정비례
특히 필터링(카테고리 선택 등)을 적용할 때마다 전체 데이터를 다시 조회해야 한다는 점이 심각했다. 캐싱으로 완화할 수는 있어도, 카테고리마다 별도의 캐시를 만들어 관리해야 했고 그 카테고리를 처음 클릭하는 순간의 로딩은 여전히 느렸다. 사용자가 회고 탭을 클릭하는 순간에도 마찬가지로 방대한 JSON이 네트워크를 타고 넘어온다.
돌아보면 두 문제는 증상만 다를 뿐 뿌리가 같았다. 페이지네이션도, 총 개수도 원하는 건 전체 목록 중 일부이거나 숫자 하나였는데, 노션 API는 그 작은 것 하나를 알려주기 위해서도 항상 전체를 통째로 넘겼다. 총 게시글 수를 별도로 관리하거나, 필요한 만큼만 한 번에 받아오는 구조로 바꿔야 했다.
시도한 방법 1
시도한 부분
첫 번째 시도는 메타데이터베이스를 추가로 만드는 것이었다. 관계형 데이터베이스에서 인덱스가 테이블 전체를 훑지 않고도 원하는 행을 빠르게 찾아주는 것처럼, 원본 게시글 데이터베이스를 통째로 조회하지 않고도 필요한 범위만 짚어낼 수 있는 별도의 색인을 만들면 될 것 같았다. 페이지당 10개 게시글 기준으로 10개씩 그룹화해 관리하도록 설계했다.

- group: 페이지 그룹 번호
- count: 그룹당 게시글 개수
- posts: 해당 그룹의 게시글 ID 배열
이렇게 그룹 단위로 나눠두면 앞서 겪은 페이지네이션과 총 개수 문제를 한 번에 풀 수 있을 것 같았다.
- 특정 페이지 그룹의 게시글만 선택적으로 불러와 네트워크 부하를 줄인다.
count속성만 조회해 총합으로 전체 게시글 수를 빠르게 계산한다.group속성으로 페이지네이션에 필요한 총 페이지 수를 빠르게 파악한다.
문제점
데이터베이스를 분리해 네트워크 전송량을 줄일 수 있겠다고 생각했다. 하지만 구현에 들어가기 직전에 여러 근본적인 한계를 발견했다.
모든 관리가 수동으로 이루어진다.
메타데이터베이스는 원본 게시글 데이터베이스와 자동으로 연동되지 않았다. 그룹과 게시글의 관계를 내가 직접 최신 상태로 맞춰야 했다.
- 새 게시글 등록 시 연관 메타데이터를 수동으로 추가해야 한다.
- 게시글 삭제 시에는 영향을 받은 모든 그룹의 메타데이터를 재정리해야 한다.
- 자동화가 어렵고 유지보수 부담이 크다.
애초에 관리자 페이지 없이 노션에 글만 쓰면 되는 구조를 원해서 노션 API를 골랐는데, 메타데이터베이스가 그 자리에 새로운 수작업을 채워 넣은 셈이었다.
카테고리별로 필터링이 불가능하다.
그룹화는 단순히 순차적 개수 기준일 뿐, 카테고리나 태그 같은 조건을 반영하지 않는다. 필터링 시 여전히 원본 데이터베이스의 전체 게시글을 조회해야 한다.
데이터 조회 요청이 2배로 증가한다
1페이지를 로드한다고 가정하면 두 번의 API 호출이 필요하다.
- 메타데이터베이스에서 해당 그룹의 페이지 ID 조회
- 해당 ID들로 원본 데이터베이스에서 실제 게시글 정보 조회
필요한 것만 골라 받으려던 시도가 오히려 한 번에 받는다는 조건을 깨뜨린 셈이었다.
정리
자동화 스크립트로 수동 관리 부담을 줄일 수는 있었지만, 무료 플랜만으로 운영하고 싶었기에 그 선택지는 접었다. 문제는 그룹화 기준 자체에 있었다. 그룹을 게시글 등록 순서가 아니라 카테고리처럼 이미 존재하는 속성으로 잡으면, 필터링 문제와 수동 관리 문제를 동시에 풀 수 있을 것 같았다. 데이터베이스를 분리한다는 방향 자체는 유효하다고 보고, 그룹화 기준을 바꿔 다시 설계하기로 했다.
시도한 방법2
시도한 부분
데이터베이스 분리 방식을 유지하면서 카테고리를 기준으로 연관관계를 재설계했다. 첫 번째 시도에서 그룹을 게시글 등록 순서로 나눈 것이 문제의 뿌리였다고 보고, 이미 존재하는 카테고리 속성으로 그룹 기준을 바꿨다. 카테고리별 데이터베이스를 별도로 구성하고, 원본 게시글 데이터베이스와 연결했다.


- count: 해당 카테고리의 게시글 개수
- category: 카테고리 이름
- posts: 해당 카테고리의 게시글 ID 배열
- postDates: 게시글 작성 날짜 배열 (날짜별 정렬용)
이 구조라면 첫 번째 시도의 한계였던 필터링과 수동 관리 문제를 동시에 해결할 수 있을 것 같았다.
- 카테고리별로 직접 조회 가능하며 필터링 문제를 해결한다.
count로 총 개수를 빠르게 파악하고, 필요한 게시글 ID만 가져와 네트워크 부하를 줄인다.- 노션의
관계속성을 활용해 클릭 한 번으로 자동 동기화된다.
문제점
하지만 여전히 근본적인 한계가 남아 있다. 카테고리 데이터베이스에는 게시글 ID만 저장돼 있고, 제목 / 요약 같은 실제 내용은 원본 데이터베이스에만 있었다. 구조를 분리한 이상, 게시글 ID 목록을 얻은 후 실제 내용을 가져오기 위해 별도의 두 번째 API 요청을 거치는 건 피할 수 없는 결과였다. 카테고리 데이터베이스는 필요한 만큼만 받는다는 조건은 채웠지만, 한 번에 받는다는 조건은 이번에도 놓치고 있었다.
정리
첫 번째 시도보다 나아졌지만, 여전히 fetch 요청이 2배라는 문제가 해결되지 않았다. 네트워크 부하를 줄이려면 데이터 전송량을 최소화해야 하고, 요청 횟수를 줄이려면 단일 API 호출로 모든 정보를 얻어야 한다.
이 두 목표 사이에서 트레이드오프를 고민하게 됐다. 데이터베이스 2개 + 2번 요청 vs 단일 데이터베이스 + 최적화 중 어떤 방향이 더 나은지 결정해야 했다. 두 시도 모두 페이지네이션이라는 요구사항은 그대로 둔 채 데이터베이스만 쪼개려 했다는 공통점이 눈에 들어왔다. 요구사항 자체를 의심해볼 차례였다.
결론
어떤 방법을 선택했는가?
결국 페이지네이션을 포기하고 무한 스크롤로 방향을 틀었다. 애초에 페이지네이션이 어려웠던 이유는 노션 API가 인덱스 기반 조회를 지원하지 않고 커서 기반으로만 동작했기 때문이었다. 커서는 특정 인덱스를 가리키는 값이 아니라 이전 응답의 마지막 항목을 가리키는 값이라, 순서대로 다음 데이터를 이어받는 데는 오히려 최적화되어 있었다. 무한 스크롤은 정확히 그 방식을 그대로 쓰는 기능이었고, 페이지네이션이 꼭 필요한 기능은 아니라는 판단도 함께 작용했다.
이에 맞춰 데이터베이스 구조를 재설계하고 적용했다. 한 번에 10개씩 순차적으로 불러오며, 스크롤 시 다음 cursor로 이어서 데이터를 추가한다.

이 방식을 통해서 다음과 같은 장점들을 택했다.
- 초기 로딩 시 10개 게시글만 요청 (기존 대비 1/3 데이터량)
- 스크롤 위치를 통한 사전 로딩으로 부드러운 UX
- API 호출 1회 + 더 적은 데이터 응답으로 총 게시글 수 조회 가능
- 필터링 시에도 동일한 패턴 적용 가능
두 번의 시도가 각각 놓쳤던 조건, 곧 필요한 만큼만 받는다는 것과 한 번에 받는다는 것을 무한 스크롤에 와서야 동시에 채운 셈이다.
이전 고민들은 무의미했나?
무의미하지 않았다고 본다. 무한 스크롤로 방향을 잡기까지 거친 두 번의 시도 덕분에, 페이지네이션이 안고 있던 문제와 더 나은 구조가 무엇인지 가늠할 수 있었기 때문이다.
외부 API를 쓸 때는 내가 원하는 형태로 데이터를 받을 수 있다고 가정하지 않아야 한다는 것을 알게 됐다. 노션 API는 인덱스 기반 조회도, 카운트 쿼리도 제공하지 않았고, 그 제약 안에서 요구사항 쪽을 바꿔야 풀리는 문제였다. 이상적인 구조를 먼저 그려놓고 API에 맞추려 하기보다, API가 잘하는 방식(커서 기반 순차 조회)에 요구사항을 맞추는 편이 더 빨랐다.
이번 작업에서는 데이터 전송량과 API 호출 횟수 사이의 트레이드오프가 가장 컸다. 시도 1과 시도 2 모두 전송량은 줄였지만 호출 횟수를 하나 더 얹었고, 무한 스크롤에 와서야 둘을 동시에 줄이는 구조를 찾을 수 있었다. 복잡한 구현과 기능적 완성도, 자동화와 수동 관리 사이에서도 같은 종류의 트레이드오프가 반복해서 나타났다. 다음에 비슷한 상황을 만나면, 떠올린 대안이 정말 두 목표를 동시에 만족하는지부터 먼저 따져볼 생각이다.
앞으로 다른 외부 API를 쓸 때도 이 트레이드오프를 가장 먼저 떠올려볼 생각이다.