이 컴포넌트는 왜 이렇게 생겼을까

이 컴포넌트는 왜 이렇게 생겼을까

June 24, 2026

인덱스 파헤쳐보기
  1. 1. 인덱스, 만들면 정말 쓰일까
  2. 2. 이 컴포넌트는 왜 이렇게 생겼을까

이 컴포넌트는 제가 처음부터 짠 게 아닙니다. 1편에서 인덱스 도메인을 익힌 뒤, 인덱스 검증 화면 코드를
한 줄씩 읽으며 “왜 이렇게 짰을까”를 도메인으로 역추적해 본 기록입니다. 그래서 이 글은 코드를 짠 사람의
설계 의도라기보다, 도메인을 손에 쥔 채 남의 코드를 읽었더니 선택 하나하나가 읽히더라는 쪽에 가깝습니다.

한 줄 요약부터 하면, **“LLM이 권고한 인덱스가 실제로 만들어졌고(created), 옵티마이저가 쓰고
있는지(utilized)를 표로 보여주는 컴포넌트”**입니다. (1편에서
본 두 질문이 그대로 들어 있어요.)

아래가 그 인덱스 검증 화면입니다.

데이터 한 행의 모양

서버가 주는 인덱스 한 줄(IndexItem)은 이렇게 생겼습니다.

interface IndexItem {
  name: string;              // 권고 인덱스명
  createIndexScript: string; // 권고 CREATE INDEX 풀 DDL (TABLESPACE·INVISIBLE 포함)
  created: boolean;          // ALL_INDEXES에 이 이름이 있나 → 만들어졌나
  utilized?: boolean;        // 새 EXPLAIN에 등장하나 → 옵티마이저가 쓰나 (created 전엔 의미 없어 optional)
}

1편에서 본 표가 그대로 필드가 됐습니다. created, utilized 두 축이 전부예요.
utilized?(optional)인 이유도 도메인에서 나옵니다. 아직 안 만든 인덱스
(created=false)는 “쓰나 안 쓰나”를 따질 대상조차 아니라서 값이 비어 있습니다(null).

참고: 이 화면 데이터는 탭에 들어가는 순간 백엔드가 실제 타깃 Oracle을 라이브로 조회
채웁니다 — ① ALL_INDEXES에 권고 인덱스명이 있나(→ created), ② 인덱스를 보이게 한 세션에서
EXPLAIN에 그 인덱스가 등장하나(→ utilized). 즉 화면 값은 “지금 이 순간의
상태”입니다. (프론트가 어떻게 호출/캐싱하는지는 생략 — 도메인만.)

표의 3개 컬럼

권고 Index생성 Index활용 여부
LLM이 권고한 이름만들었으면 그 이름, 아니면 “Index를 생성하세요”활용 / 미활용 / -
  • 권고 Index: LLM이 만들라고 권한 인덱스명. 항상 표시됩니다.
  • 생성 Index: created가 true면 실제 인덱스명을, false면 회색 글씨로
    "Index를 생성하세요." 안내를 보여줍니다. “아직 안 만들었어요”라는 시각적 신호입니다.
  • 활용 여부(= utilized): 이 화면의 핵심. IndexUtilizationCell3가지 상태
    가릅니다.
function IndexUtilizationCell({ item }: { item: IndexItem }) {
  if (!item.created) return <span>-</span>;     // ① 아직 안 만듦 → 판정 대상 아님
  if (item.utilized)  return <span>✅ 활용</span>; // ② 만들었고 옵티마이저가 씀
  return <span>✖ 미활용</span>;                   // ③ 만들었지만 옵티마이저가 안 씀
}

createdutilized 두 불리언이 순서대로 분기를 가릅니다. 각 상태를 시나리오로 보면 이렇습니다.

상태createdutilized의미사용자가 할 일
-false(null)아직 권고 인덱스를 안 만듦DDL 복사해서 만들기
✅ 활용truetrue만들었고 옵티마이저가 실제로 쓴다끝. 튜닝 성공
✖ 미활용truefalse만들었는데 옵티마이저가 안 쓴다원인 조사 필요

여기서 1편의 함정이 화면에 그대로 드러납니다 → “만들었는데 미활용”(③)이 존재해요.
왜 이런 일이 생기느냐면:

  • 이름 불일치: 권고 인덱스명과 다른 이름으로 만들어서, EXPLAIN 계획에서 그 인덱스를 못 찾음.
  • 통계 부정확: 옵티마이저가 보는 통계가 오래돼서 “굳이 안 써도 되겠다”고 잘못 판단.
  • 컬럼 가공: WHERE UPPER(name) = '홍길동'처럼 컬럼을 함수로 감싸면, name 인덱스가
    무력화돼 옵티마이저가 못 탑니다(가공된 값은 인덱스에 없으니까요).

즉 ✖ 미활용은 “인덱스는 만들었으니 절반은 했는데, 효과는 아직 못 본 상태”를 짚어주는
칸이에요.

복사 버튼이 왜 “이름”이 아니라 DDL을 복사하나

const handleCopy = () => {
  const text = rows.map(row => row.createIndexScript).filter(Boolean).join('\n'); // 빈 DDL 행은 거른다
  if (!text) { toast.error('복사할 권고 Index가 없습니다.'); return; }
  navigator.clipboard.writeText(text)            // name이 아니라 createIndexScript를 복사
    .then(() => toast.success('권고 Index DDL이 복사되었습니다.'))
    .catch(() => toast.error('복사에 실패했습니다.'));
};

저는 UI 코드를 읽을 때 “이 분기는 왜 있지”, “이건 왜 굳이 이렇게 복사하지” 같은 데서 자주 멈칫하는
편인데, 이 handleCopy가 딱 그랬습니다. 이름 두고 왜 DDL 전체를 복사하지 싶었거든요. 그런데 여기에
이 도메인의 핵심 제약이 숨어 있어요.

  • LLM은 “이 쿼리에서 이 인덱스를 써라”는 힌트를, 권고 인덱스명을 기준으로 만듭니다.
  • 사용자가 인덱스를 만들 때 이름을 다르게 지어버리면, 그 힌트가 실제 인덱스를 못
    가리켜서 송두리째 무용지물이 됩니다. (위 ③의 “이름 불일치”가 바로 이 사고예요.)
  • 그래서 사용자가 손으로 치다 오타 내지 않게, 완성된 DDL을 통째로 복사하게
    강제합니다. 권고 인덱스명 그대로에 TABLESPACE·INVISIBLE까지 다 들어간 DDL을요. 이름만
    복사하게 뒀으면 사용자가 나머지를 직접 짜다가 이름이든 INVISIBLE이든 틀릴 여지가
    생깁니다.

복사 버튼 하나의 존재 이유가 이겁니다. 도메인을 몰랐으면 그냥 item.name을 복사하는 버튼을
달았을 텐데, 그건 정확히 ③ 사고를 유발하는 구현이에요.

나머지 UI 조각

  • 상단 count 배지: 권고 인덱스 개수를 보여주는 작은 숫자 배지.
  • ZoomInIcon 버튼 → IndexExpandModal: 같은 표를 풀스크린 모달(size="fullsize")로
    크게 띄웁니다. 권고 인덱스가 많아 좁은 영역(h-[160px])에서 다 안 보일 때를 위한 확대
    보기. 표 컬럼 정의(indexColumns)를 모달과 공유해서 모양이 동일합니다.
  • 파란 안내 배너: "Invisible Index 생성이 허용되지 않아 Index를 직접 생성해야 합니다."
    → 타깃 DB 설정이 INVISIBLE 인덱스 생성을 막아두면, 1편에서 본 “안전하게 테스트” 경로를
    못 쓰고 사용자가 직접 만들어야 한다는 안내. 코드엔 TODO가 달려 있습니다. 백엔드가
    invisible-allowed 플래그를 응답에 넣어주면, 허용된 타깃에선 이 배너를 조건부로 숨길
    예정입니다.

조각을 하나씩 다 봤으니, 이제 이것들이 어떻게 묶여 있는지 전체 골격을 보겠습니다.

컴포넌트 구조 한눈에

TuningIndexVerification        ← 데이터 fetch(useQuery POST), 빈 배열이면 null
  └─ TuningIndexTable          ← 헤더(제목+count+복사+확대 버튼) / 안내 배너 / 표
        └─ IndexExpandModal    ← 확대 버튼 클릭 시 같은 표를 풀스크린으로
  indexColumns / IndexUtilizationCell  ← 표 컬럼 정의와 활용여부 3상태 렌더 (공유)

데이터 가져오는 책임(TuningIndexVerification)과 보여주는 책임(TuningIndexTable)을
나눠둔, 평범하지만 깔끔한 구조입니다.


시리즈에서 얻은 것

  • 만든 것과 쓰이는 것은 다릅니다. 쓸지 말지는 옵티마이저가 통계 보고 결정하고, 그
    결정은 EXPLAIN PLAN으로 들여다봅니다. 1편의 이 함정이 2편에선 코드의
    created/utilized 2축으로, 화면에선 “만들었는데 미활용(③)” 상태로 그대로 드러났습니다.
  • INVISIBLE은 1편에선 개념이었는데, 2편에선 화면에 박혀 있었습니다. 권고 DDL 끝의
    INVISIBLE, 그리고 “직접 생성하세요” 안내 배너가 1편의 “운영 영향 없이 테스트한다”는
    안전장치를 화면으로 옮긴 자리였습니다.
  • UI의 작은 복사 버튼 하나에도 “이름이 권고 인덱스명과 정확히 일치해야 LLM 힌트가 동작한다”는
    도메인 제약이 박혀 있었습니다. 그래서 이름이 아니라 DDL 전체를 복사합니다.

optional 필드 하나, 셀의 3상태, 복사 버튼이 복사하는 대상까지 — 컴포넌트의 선택들이
대부분 인덱스 도메인에서 나왔습니다.

On this page