발행일

네임스페이스 색인에 검색칸을 달았다 — 서버를 안 부른 이유와 그 전제가 깨지는 날

네임스페이스 색인에 검색칸을 달았다

클래스 관리와 용어 관리 화면은 왼쪽에 네임스페이스 트리, 오른쪽에 그 네임스페이스에 속한 용어 목록이 서는 구조입니다. 네임스페이스를 고르면 소속 용어가 표로 쭉 나오는데, 가장 큰 네임스페이스가 117건이에요. 원하는 용어를 찾는 방법이 눈으로 훑는 것밖에 없었습니다.

표준데이터 관리 화면에는 이미 데이터 이름으로 검색 칸이 있었습니다. 같은 자리, 같은 생김새로 하나 더 다는 일이었어요(7da7f1ad).

서버를 안 부른 이유

같은 시스템 안에 이미 목록 검색이 하나 있습니다. 데이터 관리 화면의 검색인데, 그쪽은 AJAX로 서버에 다시 묻습니다. 서버 페이지네이션이라 화면에 없는 행이 있기 때문이에요. 눈앞의 20건만 걸러 봐야 나머지 페이지의 결과를 못 찾습니다.

색인표는 사정이 다릅니다. 페이지 나눔이 없어서 고른 네임스페이스의 용어가 처음부터 전부 그려져 있어요. 그러면 서버에 물을 이유가 없습니다.

// 목록이 통째로 그려져 있어(가장 큰 네임스페이스가 117건) 서버에 다시 묻지 않고 그려진
// 행만 감춘다. 데이터 관리 화면(instanceManage.js)의 목록 검색은 서버 페이지네이션이라
// AJAX 를 쓰지만, 여기는 페이지 나눔이 없어 그럴 이유가 없다.

서버 변경이 0줄입니다. 엔드포인트도, 쿼리 파라미터도, 테스트도 안 건드렸어요.

화면에 보이는 글자가 곧 검색 대상

이 표는 용어명과 레이블 두 열입니다. 그리고 화면에는 영문/한글 토글이 있어서 레이블 열의 내용이 바뀌어요. 열별로 검색 대상을 지정하면 토글 상태에 따라 결과가 달라집니다.

그래서 행 전체의 글자를 봅니다.

function matches(row, needle) {
  return row.textContent.toLowerCase().includes(needle);
}

사용자가 보고 있는 글자가 곧 검색 대상이라 토글과 맞출 것이 없습니다. 열을 하나 더 붙여도 따라올 코드가 없고요.

그룹 머리 행을 어떻게 같이 감출까

표에는 클래스 (30) 같은 그룹 머리 행이 섞여 있습니다. 아래 행이 전부 걸러졌는데 머리 행만 남으면 빈 제목이 떠 있는 꼴이 돼요.

머리 행은 자기 아래 행들의 결과를 알아야 합니다. 앞에서부터 훑으면 머리 행을 만난 시점에는 아직 모르니, 판정을 미뤄 두었다가 나중에 돌아와야 해요. 그냥 뒤에서 앞으로 훑었습니다.

// 뒤에서 앞으로 훑어 그룹 머리 행이 자기 아래 행의 결과를 이미 알고 있게 한다.
let groupHasHit = false;
for (let i = rows.length - 1; i >= 0; i -= 1) {
  const row = rows[i];
  if (isGroupHeader(row)) {
    row.hidden = needle ? !groupHasHit : false;
    groupHasHit = false;
    continue;
  }
  // …행 판정, 걸리면 groupHasHit = true
}

한 번만 훑고 끝납니다. 머리 행 판별은 colspan이 2 이상이면서 data-node-id가 없는 행이라는 조건으로 했어요 — 용어 행에는 그 속성이 늘 붙어 있습니다.

검색칸이 지워지는 세 자리

기능 자체보다 손이 더 간 건 언제 검색어를 비우고 언제 칸을 감추느냐였습니다.

  • 다른 네임스페이스로 옮길 때 검색어를 비웁니다. 안 비우면 새 네임스페이스가 앞의 검색어로 걸러진 0건 표로 열려요. 표를 다시 그리는 함수에서 같이 처리했습니다.
  • 클래스를 고르면 칸을 감춥니다. 같은 자리의 표가 구성 속성 표로 바뀌거든요. 검색 대상이 아닌 표 위에 검색칸이 떠 있으면 안 됩니다.
  • 아무것도 안 걸리면 안내 행을 세웁니다. 빈 표만 남으면 고장으로 읽혀요.

그리고 form을 두지 않았습니다. 제출이 일어나면 화면이 리로드되고 왼쪽 트리가 접힙니다. 입력하는 대로 걸러지니 제출할 일도 없고요. 옆의 돋보기는 img라 누를 수 없는 자리인데, 마침 같은 주에 "누를 수 없는 것에는 링크 단서를 주지 않는다"는 규칙을 정리해 둔 참이었습니다.

로드 순서도 한 번 확인하고 넘어갔어요. 검색 초기화는 metaClassTree.js의 최상위에서 부르는데, 이 파일은 module이라 defer로 먼저 실행된 indexTableSearch.js가 그 시점에 이미 올라와 있습니다. 9월 1일 스피너 작업 때 defer 순서 하나로 그림이 통째로 안 그려질 수 있는 자리를 피해 간 적이 있어서, 이번엔 주석으로 근거를 남겨 뒀습니다.

남은 것 · 한계

  • 검색 범위가 고른 네임스페이스 안입니다. 전체 용어를 한 번에 찾으려면 평평한 목록 엔드포인트가 있어야 하고, 그건 이번 범위 밖으로 뒀습니다.
  • "전부 렌더돼 있다"는 전제에 기대고 있습니다. 지금은 참이지만 용어가 늘어 이 표에 페이지 나눔이 생기는 날, 이 검색칸은 그날부터 조용히 거짓말을 합니다 — 현재 페이지에 없는 용어는 "검색 결과가 없습니다"가 돼요. 그때는 데이터 관리 화면처럼 AJAX로 갈아타야 합니다. 전제를 주석에 적어 뒀지만, 전제가 깨졌을 때 빨개지는 테스트는 없습니다.
  • 회귀 잠금을 안 만들었습니다. 서버가 안 바뀌어 단위 테스트를 붙일 자리가 없었고, e2e 스펙도 추가하지 않았어요. 두 화면이 같은 함수를 쓰니 한쪽이 깨지면 양쪽이 같이 깨지는 구조라는 게 위안이라면 위안입니다.
  • 판정을 indexTableSearch.js 한 곳에 둔 건 두 화면의 표가 같은 모양이기 때문입니다. 모양이 갈라지는 순간 이 공유는 부담이 돼요.

관련 글: 밑줄과 정렬은 취향이 아니라 규칙으로 · 스피너 하나 넣으려다 jQuery를 두 번 실을 뻔했다 · 신청서 항목 모달에서 후보 조회 20회를 2회로 줄였다