"Pagination 있으면 client 가 다음에 묻는 세 가지: 'X 로 filter,' 'Y 로 sort,' '필드 A, B, C 만 줘.' GraphQL 필요 없어 — 일관된 query convention 필요해."
흔한 query 면 네 가지
List endpoint 가 성장하면서 query parameter 네 종류 누적:
- Filtering — set 좁힘:
?status=active&role=admin - Sorting — set 순서:
?sort=-created_at,name - Sparse fieldset — 각 항목 trim:
?fields=id,name - 관련 include — 관련 resource 확장:
?include=orders,profile
각각 경쟁 convention 여럿. 하나 골라서 전체 API 에 고수; 어느 specific convention 골랐는지보다 일관성이 더 중요.
Filtering convention
단순 equality (가장 흔함): ?status=active 가 "status 가 active 인 항목" 의미. ?status=active&role=admin 이 AND. Parse 쉽고, 문서 쉽고, 80% 의 필요 커버.
IN 에 comma-separated: ?status=active,pending 이 "status 가 이 중 하나" 의미. 어떤 API 는 대신 반복 param (?status=active&status=pending) 씀 — 둘 다 흔함.
Range 엔 operator 접미사: ?created_at_gte=2026-01-01&created_at_lt=2026-06-01. 접미사 convention 다양 (Django 는 __gte, REST API 는 자주 _gte 나 [gte]=).
복잡 query 엔 RSQL / FIQL: ?filter=status==active;role==admin;created_at=ge=2026-01-01. 미니 query 언어. 진짜 "power user" segment 없으면 복잡도 가치 거의 없음.
Filtering 에 query body 필요할 때: filter 가 진짜 복잡 (중첩 boolean 로직, 큰 IN list, operator 가진 full-text) 면 POST + JSON body 로 전환. URL 길이 limit 실제 (대부분 server 8KB cap). 더 이상 GET 아님 — state 안 바꾸는 POST, 정신 면에서 safety 위반 인데 실용적 우회. 일탈 문서화.
Sorting convention
흔한 패턴 셋:
- Sign prefix (terse):
?sort=-created_at,name— descending 에 minus 접두사, multi-field 에 comma. 가장 compact; 널리 이해. - Colon suffix:
?sort=created_at:desc,name:asc— 명시적, 더 장황, 일부 tooling 에 친절. - JSON:API 스타일:
?sort=-created_at,name(sign prefix 와 같음). JSON:API 가 표준화.
허용 sort 필드를 server 쪽에서 validate — client 가 임의 column 에 sort 하게 두면 index 없는 sort 통한 denial-of-service 만든 거. Whitelist: {created_at, updated_at, name} 허용; 나머지는 400 으로 거부.
Sparse fieldset — GraphQL 없이 byte 절약
가끔 client 가 필드 몇 개만 필요. Typeahead 가 {id, display_name} 만 필요한데 왜 full user record download?
GET /users/42?fields=id,display_name
→ {"id":"u_42","display_name":"Pippa"}
GET /users?fields=id,display_name,avatar_url
→ {"items":[{"id":"u_42","display_name":"Pippa","avatar_url":"..."}, ...]}
JSON:API 가 type 당 fieldset 공식화: ?fields[user]=name,email&fields[order]=total. Include 통해 여러 resource type 포함하는 response 에 유용.
요약만 필요한 client 한테 bandwidth 절약 10x+ 가능. 구현 비용: server 가 serializer 에서 필드 filter; caching 층이 다른 필드 선택이 stale 안 서빙되게 Vary: query (혹은 구체적으로 Vary: Accept-Encoding + query-aware caching) 필요.
관련 include — 'N+1 회피' query
User 가 order 가짐. User 표시하는 client 가 자주 order 도 필요. 두 방식:
- N+1 (나쁨): GET /users/42, 그 다음 각 order 마다 GET /orders/{id}. User 하나에 request N 개.
- Include parameter (좋음): GET /users/42?include=orders 가 user 와 관련 order 한 번에 돌려줌.
JSON:API 가 최상위 included 배열로 표준화; ad-hoc API 는 inline 으로 관련 resource nest. 요점 같음: request-amplification trap 피하기.
GraphQL 이나 POST-body query 잡는 때
Client 가 정기적으로 "이 user, 그 마지막 order 10 개, order 당 특정 필드만, 더하기 user 의 계정 잔고" 원하면 — GraphQL 영토 혹은 POST-body-query 영토. Query parameter 가 그 복잡도 위해 절대 설계 안 됨.
Query parameter 자랐다는 신호:
- URL 이 2KB 접근 (혹은 CDN/proxy limit 침).
- 중첩 AND/OR/NOT 가진 filter 로직.
- Client 가 정기적으로 query parameter 5+ 조립.
- 별개 "query 문법" 페이지 필요한 문서.
여러 다른 query 필요 가진 많은 client 있을 때만 GraphQL 잡아. 대부분 REST API 엔 query parameter + sparse fieldset + include 가 충분.
cwkPippa 의 query 현실
?brain=claude, ?archived=true, ?folder_id=.... Sorting parameter 없음 (기본 created_at desc); fieldset 없음 (response 이미 lean); include 없음 (React client 이 다 local 이라 N+1 괜찮). Council list 가 round 100 넘었을 때 기존 offset pagination + ?status=active filter 가 처리. Query 복잡도 낮게 유지; query convention 단순 유지. 운 아냐 — 의도적 "필요 증명 없이 feature 키우지 마" 입장.