발행일

필터 선택지 옆 건수를 어느 모집단으로 셀 것인가 — 걸린 조건에서 세면 나머지가 전부 0건

필터 선택지 옆 건수를 어느 모집단으로 셀 것인가

통합 검색 화면의 필터줄에는 카탈로그 칸과 개념체계 칸이 있습니다. 9월 4일 아침까지 둘 다 뿌리만 실린 셀렉트였어요. 운영은 카탈로그 뿌리가 사업 하나라 고를 게 없었고, 개념체계 아래 분류 90여 줄은 이 칸에서 닿지도 않았습니다. 8월 21일 판에는 3단 체크박스가 그 일을 했는데 화면을 갈아엎으면서 사라진 상태였어요.

이 글은 그 칸을 하루 동안 세 번 고친 기록입니다. 셀렉트를 체크박스 드롭다운으로, 평면 목록을 접이식 트리로, 그리고 선택지마다 건수를 붙이는 것까지. 마지막 것이 제일 작아 보이는데 제일 생각할 게 많았습니다.

낮 12시 43분: 체크할 때마다 조회

먼저 카탈로그 칸을 계층 전체를 들여쓴 체크박스 패널로 바꿨습니다(4d243c1b). 여러 카탈로그를 동시에 고를 수 있고, 주소는 옛 계약 그대로 catalog_id 반복 파라미터예요. 서버는 그걸 축 안 합집합으로 겁니다. 백엔드 수정이 없었습니다.

조회 시점은 셀렉트 칸과 같은 규칙으로 뒀어요. 체크할 때마다 즉시 조회. 적용 버튼이 없습니다.

 *  옛 화면의 체크박스 패널처럼 `catalog_id` 를 반복 파라미터로 보내고 서버는 합집합으로
 *  건다. 체크할 때마다 즉시 조회한다(셀렉트 칸과 같은 규칙, 적용 버튼 없음). */

같은 시각에 옆의 필터 선택 드롭다운도 정돈했습니다(cbc193fc). 눈에 걸리는 게 셋 있었는데 그중 하나가 패널 오른쪽에 흰 띠가 남는 것이었어요. 패널이 본문 프레임용 스크롤 클래스를 쓰고 있었는데, 그 클래스의 scrollbar-gutter: stable이 오른쪽 14px를 늘 비워 둬서 선택·호버 행의 배경이 패널 끝까지 안 찼습니다. 스크롤 클래스를 바꿨어요.

오후 2시 36분: 접이식 트리, 조회는 닫을 때

두 시간 뒤 두 가지를 뒤집었습니다(ddb7c2ed).

하나는 모양입니다. 평면 목록에 들여쓰기만으로는 분류 90여 줄의 위계가 안 읽혔어요. 처음엔 뿌리만 보이고 ▸로 펼치는 접이식 트리로 바꿨고, 개념체계 칸도 같은 드롭다운으로 교체해 체계 아래 1·2·3차 분류까지 한 칸에서 고르게 했습니다.

다른 하나는 조회 시점입니다. 두 시간 전에 정한 "체크 즉시"를 버렸어요.

 *  ⚠️ 체크는 쌓아 두고 **패널을 닫을 때 한 번만** 조회한다(🔄 2026-09-04, 셀렉트 칸은 즉시
 *  조회 그대로). 체크마다 이동하면 패널이 닫혀 여러 개를 고르려면 매번 다시 펼쳐 내려가야
 *  했다. 닫힐 때 고른 것이 처음과 같으면 이동하지 않는다.

셀렉트는 하나를 고르면 끝이라 즉시 조회가 맞습니다. 체크박스는 여러 개를 고르는 도구인데, 체크마다 페이지가 이동하면 패널이 닫히고 다시 펼쳐 내려가야 해요. 접이식 트리가 되면서 "다시 펼쳐 내려가는" 비용이 커졌고, 그래서 즉시 조회가 같은 날 틀린 답이 됐습니다. 두 칸 모두 고르고 닫으면 조회된다는 안내를 붙였습니다.

접힘 판정에서 하나 걸린 게 있습니다. 분류는 skos:broader가 둘일 수 있어서 부모가 둘인 분류는 트리에 두 자리에 섭니다. id로 접힘을 판정하면 한쪽을 펼칠 때 다른 쪽도 열려요.

// 뿌리부터 부모까지 — 부모가 둘인 분류(`skos:broader` 둘)는 두 자리에 서므로
// id 가 아니라 이 경로로 자기 가지의 접힘을 판정한다
ancestors: ancestors,

각 행이 자기 조상 경로를 들고 있고, 조상이 전부 펼쳐져 있을 때만 보입니다. 같은 분류가 두 자리에 서더라도 값은 하나라 한쪽을 체크하면 둘 다 체크돼요.

상위를 고르면 하위는 체크된 채 잠깁니다. 서버가 상위를 걸면 하위를 함께 거는 규칙이 있는데, 그걸 눈에 보이게 한 겁니다. 주소에는 상위 하나만 들어갑니다.

오후 3시 54분: 건수를 붙이는데, 무엇을 세나

선택지마다 "고르면 몇 건이 나오나"를 붙였습니다(ae14a587). 세 탭 전부, 셀렉트 칸과 트리 드롭다운 모두요.

문제는 세는 모집단이었습니다. 화면에는 이미 조건이 걸려 있어요. 검색어, 유형 칩, 그리고 카탈로그·개념체계 범위. 이 조건이 다 걸린 목록에서 선택지별 건수를 세면 어떻게 될까요.

세는 모집단결과
지금 화면의 목록(범위 조건 포함)고른 카탈로그 A의 건수만 남고 나머지 선택지는 전부 0건 — A를 골랐으니 B에 속한 건 목록에 없음
범위 조건을 뺀 모집단(검색어·유형만 반영)각 선택지가 "이걸 골랐을 때 나올 건수"를 말함

첫 번째는 정확하지만 쓸모가 없습니다. 지금 걸린 범위 안에서 세면 그 범위 밖의 선택지가 0으로 보이고, 사용자는 "B에는 데이터가 없구나"로 읽어요. 실제로는 A를 골라서 안 보이는 것뿐인데.

# 선택지마다 '고르면 나올 건수' — 세는 모집단은 **범위 조건을 뺀 것**이다.
# 지금 걸린 범위에서 세면 고른 항목만 남아 나머지 선택지가 전부 0건으로 보인다.
_attach_scope_option_counts(
    catalog_filter_data,
    concept_filter_data,
    visible
    if scope["panel_scope_ids"] is None
    else { ... },
)

계산은 _attach_scope_option_counts() 한 곳에서 합니다. 쿼리 수는 선택지 개수와 무관하게 고정이에요. 씨앗을 모아 위계 확장을 한 번만 돌아서, dev 실측으로 개념 239개와 체계 4개에 13쿼리, 0.02초였습니다.

"""
⚠️ `visible` 은 **범위 조건을 뺀** 모집단이어야 한다(검색어·유형만 반영). 계층이라
상위 칸의 선택은 하위 선택지의 부분집합이므로 따로 곱할 것이 없다.
"""

계층이라는 성질이 여기서 일을 덜어 줍니다. 1차 칸에서 카탈로그 A를 고른 뒤 2차 칸의 선택지는 전부 A의 자식이라, A의 건수 안에서 다시 걸러질 뿐 따로 곱할 조건이 없어요. 아래 칸의 "전체" 행은 위에서 고른 것의 건수가 됩니다.

화면 쪽에서는 건수를 이름과 다른 span에 뒀습니다.

// 건수는 이름과 따로 둔다 — 한 span 에 이어 붙이면 긴 이름이 잘릴 때 건수까지 잘린다.
if (node.count != null) {
  var count = document.createElement("span");
  count.className = "shrink-0";
  count.textContent = "(" + node.count.toLocaleString() + ")";

truncate는 한 span의 끝을 자릅니다. 이름 뒤에 건수를 이어 붙이면 긴 이름에서 잘려 나가는 게 건수예요. 제일 짧고 제일 중요한 정보가 제일 먼저 사라집니다.

사흘 뒤: "전체" 옆의 건수는 뺐다

9월 7일에 "전체 카탈로그 (1,490)" 같은 첫 선택지와 드롭다운 버튼의 건수 꼬리를 뗐습니다(2fc567f8). 목업이 그렇게 돼 있었어요. 조건 없음은 조건 없음만 말하고, 개별 선택지 옆 건수는 그대로 둡니다.

그 건수만 만들던 TOTAL·COUNTS·renderFieldallCount 인자를 지웠더니 46줄이 빠졌습니다. 사흘 전에 "아래 칸의 전체 행은 위에서 고른 것의 건수"라고 공들여 계산했던 값이 화면에서 없어진 거예요. 계산 규칙은 맞았는데 보여 줄 자리가 없어졌습니다.

검증

  • 드롭다운에서 카탈로그 둘을 체크하고 닫으면 한 번만 조회되고, 조건 칩이 항목마다 하나씩 서는 것. ×로 하나만 빼면 다른 하나는 남는 것.
  • 고른 것이 처음과 같은 채로 닫으면 이동하지 않는 것.
  • 상위 분류를 체크하면 하위가 체크된 채 잠기고, 주소에는 상위 id만 실리는 것.
  • 부모가 둘인 분류를 한쪽에서 펼쳤을 때 다른 자리는 접힌 채인 것.
  • 카탈로그 A를 고른 상태에서 드롭다운을 다시 열었을 때 B의 건수가 0이 아닌 것. 고치기 전에는 0이었습니다.
  • e2e 헬퍼가 이름 (99) 꼬리표를 못 잡던 정규식을 고쳤습니다. 건수를 붙이자마자 기존 스펙이 깨졌어요.
  • 스펙 dataset-list-scope-filter.spec.js에 다중 선택·× 하나만 빼기 시나리오 추가.

남은 것 · 한계

  • 조회 시점을 두 시간 만에 뒤집었습니다. 체크 즉시 조회는 커밋할 때도 "셀렉트와 같은 규칙"이라는 이유뿐이었고, 체크박스라는 도구의 성격은 안 봤어요. 트리로 바꾸고 나서야 비용이 보였습니다.
  • 셀렉트 칸은 즉시 조회, 드롭다운은 닫을 때 조회입니다. 같은 필터줄에 조회 시점이 두 가지예요. 안내 문구로 메웠지만 규칙은 두 개입니다.
  • 건수는 범위 조건만 빼고 검색어·유형은 반영합니다. 그래서 검색어를 바꾸면 건수가 다시 계산돼요. 이게 맞다고 봤지만, 드롭다운을 열어 둔 채 검색어를 치면 열려 있는 패널의 건수는 옛 값입니다.
  • 13쿼리·0.02초는 dev 수치입니다. 운영 개념 수는 재 보지 않았어요. 쿼리 수가 선택지 개수와 무관하다는 건 확인했지만, 위계 확장의 비용은 트리 깊이를 따릅니다.
  • "전체" 옆 건수를 뺀 건 목업을 따른 것이지 그 정보가 쓸모없다고 판단한 게 아닙니다. 사용자가 "전체가 몇 건인가"를 알 자리는 이제 결과 머리줄뿐이에요.

관련 글: 옵션이 많아질수록 필요한 검색형 select · 분류 칩 하나를 네 번 갈아엎었다 · 탭의 '전체'는 조건 없음이 아니라 합집합이었다 · 검색 조건을 바꿀 때 새로고침 대신 조각 9개를 갈아끼웠다