#명시적 검색 파라미터 Cookbook
버전: 0.44.0 | 업데이트: 2026-03-29 | 적용 대상: ranvier-http 0.44+ | 카테고리: 쿡북
#개요
Ranvier는 쿼리 파라미터 추출에 derive 매크로 대신 명시적 검색 구조체를 사용합니다. 각 파라미터의 이름, 타입, 기본값이 코드에서 직접 보입니다 — P4(Bus = 명시적 타입 인덱스), P5(Convention-over-config 금지) 준수.
핵심 타입: PageParams, Paginated<T> (ranvier_http::prelude에서 제공)
#1. `PageParams` — 내장 페이지네이션
PageParams는 page와 per_page 쿼리 파라미터를 자동 클램핑으로 추출합니다:
use ranvier_core::prelude::*;
use ranvier_http::prelude::*;
#[transition]
async fn list_items(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<Item>, String> {
let page = PageParams::from_bus(bus);
// page.page = 1 (기본값), page.per_page = 20 (기본값)
// ?page=3&per_page=50 요청 시 → page.page = 3, page.per_page = 50
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");
let total: i64 = try_outcome!(
sqlx::query_scalar("SELECT COUNT(*) FROM items")
.fetch_one(&*pool).await, "count"
);
let items: Vec<Item> = try_outcome!(
sqlx::query_as("SELECT * FROM items ORDER BY id LIMIT $1 OFFSET $2")
.bind(page.per_page)
.bind(page.offset())
.fetch_all(&*pool).await, "query"
);
Outcome::Next(Paginated::new(items, total, &page))
}응답:
{
"items": [...],
"total_count": 142,
"page": 3,
"per_page": 50,
"total_pages": 3
}#2. 커스텀 검색 구조체 — 명시적 패턴
도메인별 필터가 필요한 경우, from_bus 메서드를 가진 일반 구조체를 작성합니다:
use ranvier_http::prelude::*;
struct InterfaceSearch {
page: PageParams,
intf_id: Option<String>,
intf_nm: Option<String>,
status: Option<String>,
use_yn: Option<String>,
}
impl InterfaceSearch {
fn from_bus(bus: &Bus) -> Self {
Self {
page: PageParams::from_bus(bus),
intf_id: bus.query_param("intf_id"),
intf_nm: bus.query_param("intf_nm"),
status: bus.query_param("status"),
use_yn: bus.query_param("use_yn"),
}
}
}Transition에서의 사용:
use crate::safe_query_builder::QueryBuilder;
#[transition]
async fn search_interfaces(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<InterfaceInfo>, String> {
let search = InterfaceSearch::from_bus(bus);
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");
// db-sqlx-demo/src/safe_query_builder.rs helper를 앱 코드에 복사해 사용
let query = QueryBuilder::new("SELECT * FROM tb_intf_inf")
.filter_optional("intf_id", &search.intf_id)
.filter_optional("intf_nm", &search.intf_nm)
.filter_optional("use_yn", &search.use_yn)
.paginate(search.page.per_page, search.page.offset())
.build();
// 실행 및 반환...
Outcome::Next(Paginated::new(items, total, &search.page))
}#3. `#[derive(QueryParams)]`를 사용하지 않는 이유
derive 매크로는 필드명으로 자동 추출합니다:
// ❌ 미지원 — P4/P5 위반
#[derive(QueryParams)]
struct Search { intf_id: Option<String>, page: i64 }이 접근법은 어떤 쿼리 파라미터가 읽히는지, 어떤 기본값이 적용되는지 숨깁니다. 명시적 패턴은:
from_bus()에서 모든 파라미터명을 확인 가능 — 네이밍 컨벤션 암기 불필요- 파라미터별 기본값 제어 —
query_param_or("page", 1)vsquery_param("status") - 인라인으로 검증 로직 추가 가능 —
if per_page > 200 { ... } from_bus()직접 테스트 가능 —Bus에QueryParams::from_query("...")로 구성
비용은 검색 구조체당 약 10행. EIMS 기준 3개 검색 엔드포인트 = 약 30행 — 완전한 가시성의 합리적 대가.
#관련 문서
- `PageParams` API — 내장 페이지네이션 타입
- Outcome 패턴 Cookbook — Outcome 합성
- JSON Outcomes Cookbook —
get_json_out/post_typed_json_out