Engineering · Platform Engineering

목록 렉의 원인은 분할 방식이 아니라 페이로드였다: GET 336개 전수 측정과 하위호환 서버 페이지네이션

채팅 목록 렉의 원인이 클라이언트 분할이 아니라 응답에 실린 캔버스 JSON 1.6MB였음을 실측으로 확인하고, 인자 없는 GET 336개를 전수 측정해 같은 부류를 소비처 기준으로 판정했습니다. 필드 경량화와 하위호환 서버 페이지네이션으로 고치면서 전환 과정의 필터 의미 회귀와 숨은 1,000건 상한도 함께 정리했습니다.

기간
2026.08 – 2026.09
소속
고객사 구축 · 은행
역할
성능 감사·백엔드·프론트엔드 수정

Key Results

64분의 1
채팅하기 목록 API 응답로컬 스택 213,002 → 3,309바이트, 캔버스 JSON 필드 제거
2.9MB → 0.95MB
도구 목록 API 응답개발 환경, OpenAPI 스키마 등 무거운 필드 3개 제외
336개
응답 크기를 전수 측정한 GET 엔드포인트개발 환경, 인자 없는 호출의 응답 바이트·시간

은행에서 쓰는 XGEN의 에이전트 채팅에는 대화를 시작할 에이전트를 고르는 "채팅하기" 목록이 있습니다. 이 목록의 로딩과 1페이지에서 2페이지로의 이동이 느리다는 제보가 들어왔습니다. 제보자는 "전부 불러와서 클라이언트에서 나누는 것 아니냐, 지식관리 목록은 렉이 없다"고 원인까지 지목했는데, 실측해 보니 그 지목은 절반만 맞았습니다. 클라이언트에서 나누는 것은 사실이었지만, 느린 이유는 나누는 방식이 아니라 응답에 실린 데이터의 무게였습니다.

이 건을 계기로 같은 부류의 문제가 다른 목록에도 있는지 전수로 감사했습니다. 프론트엔드의 목록 UI 40여 개를 정적으로 훑고, 개발 환경의 인자 없는 GET 엔드포인트 336개를 모두 호출해 응답 크기와 시간을 쟀습니다. 이 글은 그 과정에서 세운 판정 기준, 무거운 필드를 걷어낸 방식, 배포 순서와 무관하게 동작하도록 설계한 서버 페이지네이션, 그리고 전환 과정에서 생긴 의미 회귀와 숨어 있던 상한을 다룹니다. 한 달 뒤 반대 방향에서 나온 결함까지 포함해, 페이지네이션 계약을 다룰 때 무엇을 따로 봐야 하는지로 끝맺습니다.

목록 UI와 API가 맞물리는 방식

XGEN 프론트엔드의 목록 UI는 크게 두 가지 방식으로 데이터를 가져옵니다. 하나는 목록 API를 인자 없이 호출해 전체를 받은 뒤 클라이언트에서 페이지 단위로 자르는 방식이고, 다른 하나는 page와 page_size를 서버에 넘겨 필요한 만큼만 받는 서버 페이지네이션입니다. 에이전트 관리 목록은 이미 서버 페이지네이션 규약을 쓰고 있었고, 채팅하기와 채팅 이력 같은 목록은 전자였습니다.

운영 조건도 설계에 영향을 줬습니다. 같은 기능이 제품 기본 라인과 고객 배포 라인 두 브랜치에 각각 반영되고, 백엔드와 프론트엔드는 별도 저장소라 머지와 배포 시점이 어긋날 수 있습니다. 은행 폐쇄망에는 외부망 데모 환경에서 검증을 마친 빌드만 수동으로 반입하므로, 머지 실수가 곧바로 은행에 반영되지는 않습니다. 그래도 구 백엔드에 새 프론트엔드가, 또는 그 반대 조합이 일정 기간 함께 돌 수 있다는 전제는 지켜야 했습니다.

[클라이언트 분할]  GET /목록 (인자 없음) → 전체 응답 → 클라이언트 slice → 렌더
[서버 페이지네이션] GET /목록?page=&page_size= → 해당 페이지만 응답 → 렌더
                   (파라미터 미지정 시 기존처럼 전체 반환 → 하위호환)

채팅하기 목록: 지목은 절반만 맞았다

고객 배포 라인의 외부망 데모 환경에서 두 목록의 응답을 직접 쟀습니다.

API 응답 크기 항목 수
채팅하기 목록 1,642,340 바이트 57건
지식관리 컬렉션 목록 7,429 바이트 10건

57건에 1.6MB였고, 그 무게의 99.2%가 워크플로우마다 실린 캔버스 JSON 전체(건당 약 29KB)였습니다. 서버는 하네스 워크플로우인지 판별하려고 이 컬럼을 따로 SELECT해 응답 객체에 추가한 뒤, 판별이 끝나고도 응답에서 빼지 않고 그대로 흘려보내고 있었습니다. 프론트엔드는 이름·설명·id만 썼습니다.

클라이언트 분할 자체는 문제가 아니라는 것도 확인했습니다. 카드 컴포넌트는 순수 렌더였고, 페이지를 클릭해도 재요청이 없었습니다. 페이로드가 13KB 수준이면 전량을 받아 클라이언트에서 나눠도 문제가 없어, 이 시점에는 서버 페이지네이션이 필요 없다고 판단했습니다. 대신 다른 낭비가 하나 더 있었습니다. 워크플로우 비교 모달이 열릴 때마다 이 1.6MB를 다시 요청하고 있었습니다.

수정은 응답 직전에 캔버스 JSON 필드를 빼는 6줄이었습니다. 소비처 3곳(새 채팅, 채팅 이력, 비교 모달)을 전수로 확인해 이 필드를 쓰는 곳이 없음을 보았고, 프론트엔드 타입에서도 선택 필드라 타입 변경 없이 호환됐습니다. 로컬 스택에서 응답은 213,002바이트에서 3,309바이트로 64분의 1이 됐고, Playwright로 카드·필터·검색·페이지네이션이 정상 렌더되는 것을 확인했습니다.

이후 지시에 따라 에이전트 관리 목록과 같은 서버 페이지네이션 규약으로도 전환했습니다. page, page_size(기본 24), 검색어, 공유 탭, 즐겨찾기용 id 목록을 모두 선택 파라미터로 두어, 미지정 시에는 기존처럼 전체를 반환합니다. 로컬에서 하위호환, 분할, 범위 초과 보정, 검색, id 조회, 공유 탭, 조합의 7가지 케이스를 확인했습니다. 프론트엔드는 검색어를 300ms debounce로 서버에 넘기고, 개인·공유 탭은 공유 파라미터로, 즐겨찾기는 id 목록 경량 조회로 바꿨습니다. 머지 순서는 백엔드 먼저로 명시했습니다. 반대로 나가더라도 구 백엔드가 파라미터를 무시해 전체를 한 페이지에 렌더할 뿐, 기능은 동작합니다.

배포 뒤 "채팅 이력과 현재 채팅도 여전히 느리다"는 제보가 이어졌습니다. 채팅 이력 목록은 1,106행에 2,106,002바이트였고, 그중 91.8%가 행마다 실린 첫 메시지 전문이었습니다. 반면 현재 채팅의 대화 기록 조회는 51턴 대화가 16,923바이트, 0.05초라 무혐의로 판정하고 손대지 않았습니다. 이력 목록은 같은 규약으로 처리했습니다. 첫 메시지를 200자로 자르고, page·page_size·검색을 넣고, 활성·배포·삭제 상태 탭 필터를 서버로 옮겼습니다. 이 과정에서 400 응답이 500으로 감싸지던 예외 처리도 바로잡았습니다.

같은 부류를 전수로 찾기: 크기가 아니라 소비처로 판정

채팅하기와 같은 부류가 다른 목록에도 있는지 세 단계로 감사했습니다. 먼저 프론트엔드 전 feature를 정적으로 훑어 목록 UI 40여 개의 로드 방식, 클라이언트 분할 여부, 무거운 필드 위험을 표로 정리했습니다. 다음으로 개발 환경에서 인자 없는 GET 336개를 전부 호출해 응답 바이트와 시간을 쟀습니다. 마지막으로 상위 후보는 고객 데모 환경에서 다시 측정해 환경별 데이터 차이를 반영했습니다.

엔드포인트 개발 환경 고객 데모 환경 소비처 판정
컬렉션 전체 문서 상세 24.2MB - 프론트엔드 소비처 없음 API 리스크만, UI 렉 아님
도구 목록 2종 각 2.9MB 24KB 채팅 도구 picker, 업로드 모달 수정
에이전트 스토어 목록 2.1MB - 인자 없는 호출의 소비처 없음 하위호환 응답, 유지
지식관리 문서 요약 컬렉션당 620KB 컬렉션당 720KB 지식관리 문서 목록 후속 1순위
거버넌스 승인 목록 163KB 133KB 승인 UI, 페이지네이션 없음 후속
하네스 도구 소스 186KB, 10.7초 - 하네스 도구 소스 크기 아닌 지연, 별건
감사 로그 25KB 21KB 관리 설정 감사 로그 보류
그 밖의 위반 약 15건 24KB 이하 - MCP 마켓, 인증 프로필 등 실측상 무해

판정은 크기가 아니라 소비처 기준으로 했습니다. 가장 큰 24.2MB짜리는 두 브랜치 모두 프론트엔드에서 호출하는 곳이 없어, API 리스크로만 기록하고 UI 렉의 원인에서는 제외했습니다. 크기 순으로 고쳤다면 사용자가 겪는 렉과 무관한 엔드포인트에 가장 먼저 시간을 썼을 것입니다. 도구 목록은 반대였습니다. 캔버스 채팅과 AI 채팅의 도구 picker가 열릴 때마다 2.9MB를 받았고, 그 64%가 OpenAPI 스키마 전문이었습니다.

수정하지 않은 항목도 근거를 남겼습니다. 지식관리 문서 요약은 컬렉션당 최대 720KB로 문서 수에 선형 비례하지만, 구현이 문서 서비스 저장소에 있어 백엔드와 프론트엔드를 묶은 별도 작업으로 넘겼습니다. 감사 로그는 서버가 페이지네이션을 완전히 지원했지만, UI의 카테고리 필터가 서버 값과 다른 목업 시절 설계라 필터 UX 결정이 먼저였습니다. 실측 21~25KB로 아직 렉도 아니었습니다. 거버넌스 승인 목록은 페이지네이션 UI도 서버 파라미터도 없어 백엔드와 UI를 함께 바꿔야 했습니다.

판단: 무엇을 경량화하고, 무엇을 페이지네이션하고, 무엇을 남기나

선택지 장점 단점 판단
모든 목록을 서버 페이지네이션으로 일괄 전환 데이터 증가에 구조적으로 대응 프론트엔드·백엔드 동시 변경, 페이로드가 작으면 효과 미미 측정 상위만
응답에서 무거운 필드 제거 백엔드 한 곳 변경, 즉시 효과 소비처 전수 확인이 전제 채택 (채팅하기, 도구 목록)
하위호환 선택 파라미터로 페이지네이션 배포 순서와 무관하게 동작 파라미터를 안 넘기는 호출은 여전히 전량 채택
측정상 무해한 항목은 보류 위험 없는 변경을 피함 데이터가 늘면 다시 봐야 함 성장 리스크 표로 기록

필드 제거는 소비처를 전수로 확인한 뒤에만 했습니다. 도구 목록은 picker 2곳, 업로드 모달, API 불러오기 노드의 드롭다운(서버 내부 호출)이 소비처였고, 모두 id·함수 id·이름·설명·컬렉션 id·사용자명만 썼습니다. 단건 실행과 편집은 권한 확인과 함께 전체 행을 따로 조회하므로, 목록 응답에서 OpenAPI 스키마, API 바디 스키마, 고정 바디 세 필드를 뺐습니다. 고객 배포 라인은 목록에서 사용자 권한을 파생하는 단계가 있어, 파생이 끝난 뒤에 필드를 빼도록 순서를 맞췄습니다.

리뷰에서는 "추가만 하지 말고 정리도 하라"는 피드백을 받았습니다. 도구 목록 두 핸들러에 복사돼 있던 SQL과 후처리를 공용 헬퍼 하나로 합쳐 브랜치별로 30줄 남짓을 줄였고, 동작이 같은지 라이브로 재검증했습니다.

전환이 만든 의미 회귀와 숨은 상한

서버 페이지네이션 전환은 필터의 의미를 바꿀 수 있습니다. 점검 이력 UI가 그 사례였습니다. 제품 기본 라인은 이미 서버 페이징으로 전환돼 있었는데, 서버의 동등 비교 필터가 "점검 기록이 아예 없는 배포 대상"을 미점검에서 빠뜨리고 있었습니다. 개발 환경 대상 48건 중 47건이 기록이 없어, 미점검 필터가 1건만 반환했습니다. 고객 배포 라인 UI의 코드 주석에 "이력 없는 항목도 포함"이 명시된 의도적 제품 동작이었으므로, 목록과 엑셀 내보내기 조건 모두에서 미점검·점검 예정에 inspection_date IS NULL OR inspection_date = ''를 포함하게 고쳤습니다. 로컬 시드(대상 3, 기록 1)로 미점검 2, 정상 1 불변, 점검 예정 2, limit·offset 합성 정상을 확인했습니다.

고객 배포 라인의 같은 UI에는 다른 문제가 있었습니다. page를 서버에 넘기지 않고 클라이언트에서 자르지도 않아, 전 행이 한 번에 렌더되고 페이지를 넘겨도 그대로인 죽은 페이지네이션이었습니다. 그 백엔드가 limit 미지정 시 전체를 돌려줘서 드러나지 않았을 뿐, 제품 기본 라인 백엔드처럼 기본 limit이 20이었다면 21번째 행부터 아예 보이지 않았을 구조입니다. 제품 기본 라인의 전환 구조를 고객 UI 형태에 맞춰 이식하고, 머지 순서는 서버 필터 수정을 먼저로 정했습니다. 순서가 바뀌면 미점검 필터가 기록 있는 행만 반환하기 때문입니다.

도구 목록에는 숨은 상한이 있었습니다. 배포 후 "API 도구 렌더링이 느리다"는 제보를 조사해 보니 도구 관리 UI 자체는 무죄였습니다. 개별 도구 탭은 17.7KB, 컬렉션 멤버 목록은 50개 단위 서버 페이징과 250ms debounce, 그래프는 노드 80·엣지 250 제한으로 롱태스크가 0이었고, 탭 전환은 21~31ms였습니다. 대신 조사 중에 다른 낭비를 찾았습니다. 팀의 규모 테스트로 컬렉션 멤버 도구가 1,133개 넘게 등록되면서 목록 API의 1,000건 상한이 걸려, 응답이 1,000행 0.95MB가 됐습니다. picker는 컬렉션을 별도 항목으로 보여 주므로 받은 멤버 도구의 99%를 클라이언트에서 바로 버리고 있었습니다.

[전] picker 열기
  브라우저 --GET 도구 목록--> 서버: 도구 1,000행 전부 (0.95MB)
  브라우저: 컬렉션 멤버 993행 폐기 → 7행 표시
 
[후] picker 열기
  브라우저 --GET 도구 목록?exclude_collection_members=true-->
  서버: 컬렉션 멤버를 제외하고 응답
  브라우저: 받은 그대로 표시 (표시 결과는 전과 동일)

서버에 컬렉션 멤버 제외 파라미터를 추가하되 기본값은 꺼 두어 기존 호출에는 영향이 없게 했고, picker 2곳만 켜서 요청하게 했습니다. 기존 클라이언트 필터는 구 백엔드에 대비한 방어선으로 남겨 머지 순서와 무관하게 했습니다. API 불러오기 노드의 드롭다운은 서버 내부에서 함수를 직접 호출해 프레임워크의 의존성 주입을 거치지 않으므로, 파라미터 자리에 기본값 객체가 그대로 들어옵니다. 그래서 is True로 비교해 기존 동작(멤버 포함 목록)을 유지했습니다.

결과

지표 이전 이후 측정 조건
채팅하기 목록 응답 213,002 바이트 3,309 바이트 로컬 스택, 캔버스 JSON 필드 제거
도구 목록 응답 2.9MB 0.95MB 개발 환경, 무거운 필드 3개 제외
도구 picker 요청 - 16KB, 서버 처리 10ms 배포 후 고객 데모 환경, Playwright
미점검 필터 기록 없는 대상 누락 31건 반환, 기록 없는 대상 26건 포함 이전은 개발 환경 48개 대상 중 47개 누락, 이후는 배포 후 고객 데모 환경
점검 이력 페이지 이동 넘겨도 같은 행 2페이지 클릭 시 행 전체 교체 (총 42건, 페이지당 20건) 배포 후 고객 데모 환경, Playwright
채팅 이력 목록 2,106,002 바이트 (1,106행) 첫 메시지 200자 절단, 서버 페이징 백엔드 라이브 8케이스, UI E2E

미점검 숫자가 갑자기 늘어나는 것은 버그가 아니라 숨어 있던 미점검 대상이 드러난 것이라, 거버넌스 담당 쪽에 미리 안내할 항목으로 남겼습니다. 감사 결과는 데이터가 늘었을 때 문제가 될 지점의 표로도 정리했습니다. 도구 목록의 1,000건 상한은 초과분이 드롭다운에서 조용히 잘리는 정합성 문제로 바뀌었고, 노드 드롭다운의 1,140개 옵션과 함께 검색형 셀렉터나 페이징 같은 제품 결정이 필요한 항목으로 넘겼습니다.

회고: 페이지네이션 계약에는 두 종류의 소비자가 있다

한 달 뒤 반대 방향의 결함이 나왔습니다. 은행에서 "스케줄러의 워크플로우 검색에 최신 것만 나오고 과거 것은 안 나온다, 템플릿 업로드도 같고 검색란도 없다"는 제보가 들어왔습니다. 워크플로우 목록 API는 그해 5월부터 기본 24건, 최근 수정순의 서버 페이지네이션이었는데, 스케줄 생성, 템플릿 업로드, 품질 평가 생성 모달이 첫 페이지만 돌려주는 호출을 "목록 전체가 온다"는 옛 전제로 쓰고 있었습니다. 은행의 워크플로우가 24건을 넘으면서 25번째 이후가 선택지에서 사라졌습니다. 서버 검색은 이름과 작성자만 보고 워크플로우 id는 찾지 못해, 클라이언트 검색이 필요한 상황이기도 했습니다.

같은 문제를 앞서 겪은 보드 구성 기능이 만들어 둔 전체 조회 헬퍼(200건씩 마지막 페이지까지 순회)로 세 모달을 모두 교체하고, 템플릿 업로드에 검색 입력을 추가했습니다. 백엔드는 바꾸지 않았습니다. 빌드본을 워크플로우 74개가 있는 외부망 데모 환경에 연결해 Playwright로 확인하니, 스케줄 생성 모달이 74건을 모두 로드했고 가장 오래된 워크플로우가 검색으로 1건 표시됐습니다. 템플릿 업로드도 74건과 검색 입력이 함께 동작했습니다. 워크플로우가 200건을 넘으면 모달을 열 때 요청이 ⌈n/200⌉회로 늘어난다는 점은 비용으로 기록해 두었습니다.

이 결함과 8월의 감사는 같은 계약의 양면입니다. 8월에는 한 페이지만 필요한 소비자가 전량을 받고 있었고, 9월에는 전량이 필요한 소비자가 한 페이지만 받고 있었습니다. 목록 API를 감사할 때는 응답 크기뿐 아니라 소비자가 "한 페이지가 필요한가, 전체가 필요한가"를 따로 표시해야 합니다. 그리고 서버 페이지네이션으로 바꿀 때는 필터의 의미, 숨은 상한, 배포 순서를 함께 확인해야 한다는 것도 이번 감사에서 얻은 기준입니다. 측정 없이 "전량 조회는 나쁘다"는 원칙만으로 움직였다면, 24.2MB짜리 무해한 엔드포인트와 무혐의였던 대화 기록 조회에 먼저 손을 댔을 것입니다.