개요

블로그 글이 많아지면 검색창은 장식이 아니라 두 번째 목차가 된다. 그런데 이 검색창이 원하는 글을 못 찾기 시작했다.

처음에는 검색 결과가 무조건 최신 글부터 나왔다. 관련도 순으로 바꾸고 나니 이번에는 검색 자료가 너무 커졌다. 자료를 줄이려고 본문 대신 제목 헤더를 저장했더니, 혼동행렬처럼 글 안에 분명히 있는 제목이 검색되지 않았다.

겉으로 보면 모두 같은 문제처럼 보인다.

“검색이 이상하다.”

하지만 실제 원인은 매번 달랐다. 정렬 규칙, 로딩 순서, 검색 자료의 크기, 빌드 과정의 자료 형태가 차례로 문제를 만들었다.

이 글은 그 문제들을 어떤 순서로 확인했고, 여러 해결 방법 중 왜 지금의 방식을 골랐는지 정리한 기록이다.


1. 이 블로그의 검색은 어떻게 움직이나

이 블로그는 GitHub Pages와 Jekyll로 만들어진다. Jekyll이 무엇인지부터 궁금하다면 블로그 로컬 관리 환경을 정리한 글YAT 테마로 옮긴 과정을 먼저 읽어도 좋다.

현재 검색에는 별도의 검색 서버가 없다. 블로그를 만들 때 search.json이라는 검색용 목록을 함께 만들고, 방문자의 브라우저가 그 목록을 읽는다.

마크다운 글
   ↓ Jekyll 빌드
검색용 목록(search.json)
   ↓ 브라우저가 내려받기
단어 찾기 → 관련도 점수 계산 → 상위 10개 표시

도서관으로 비유하면 search.json은 책마다 붙어 있는 작은 색인 카드다. 책을 찾을 때 서가의 모든 페이지를 다시 읽지 않고, 제목·주제·목차가 적힌 카드를 먼저 살펴보는 방식이다.

이 구조는 단순하고 싸다. 서버를 따로 운영하지 않아도 된다. 대신 색인 카드에 적히지 않은 말은 검색할 수 없다. 이 사실이 마지막 문제의 핵심이었다.


2. 첫 번째 문제 — 관련 글보다 최신 글이 먼저 나왔다

처음 검색 결과는 날짜순이었다. 최신 글을 찾을 때는 편하지만, 개념을 찾을 때는 곤란하다.

예를 들어 RNN을 검색한다고 하자. RNN의 구조를 처음부터 설명한 글보다, 나중에 작성된 글에서 RNN을 한두 번 언급했다는 이유로 그 글이 먼저 나올 수 있었다.

사전에서 사과를 찾았는데 “사과를 먹었다”라는 최신 일기가 먼저 나오고, 사과의 뜻을 설명한 페이지가 뒤에 나오는 셈이다.

날짜 대신 관련도 점수를 사용했다

그래서 검색어가 어디에 등장했는지에 따라 점수를 주었다.

검색어가 발견된 곳 의미 우선순위
제목 글 전체의 대표 이름 가장 높음
태그 글의 핵심 주제 높음
부제목 글의 짧은 설명 높음
카테고리 글의 큰 분류 보통
제목 헤더 글 안에서 다루는 세부 주제 보조

관련도 점수가 같을 때만 최신 날짜를 사용했다.

1순위: 관련도 점수
2순위: 같은 점수라면 최신 날짜

이 변경 뒤에는 RNN 태그와 부제목을 가진 설명 글이, RNN을 잠깐 참조한 글보다 먼저 나오게 됐다.

이 방식은 선형 검색과 이진 검색을 정리했던 글에서 다룬 “찾기”보다 한 단계 더 나아간다. 이번에는 찾았는가뿐 아니라 찾은 것 중 무엇이 더 알맞은가까지 정해야 했다.


3. 두 번째 문제 — 검색창이 가끔 아무 반응도 없었다

관련도 정렬을 적용한 뒤에는 검색이 아예 작동하지 않는 것처럼 보이는 일이 생겼다.

당시 검색 목록은 약 1.53MB였다. 200개가 넘는 글의 본문이 들어 있었기 때문이다. 인터넷이 느리면 이 목록을 받는 데 몇 초가 걸릴 수 있었다.

더 큰 문제는 코드의 순서였다.

기존 순서
1. 검색 목록을 모두 받는다.
2. 검색창의 입력을 듣기 시작한다.

사용자가 1번 도중에 검색어를 입력하면 브라우저는 그 입력을 듣지 못했다. 문을 두드렸는데 안내 직원이 아직 출근하지 않은 것과 같다.

입력을 먼저 듣도록 바꿨다

수정한 순서는 다음과 같다.

개선한 순서
1. 검색창의 입력을 즉시 듣는다.
2. 검색 목록을 받는다.
3. 목록이 준비되면 미리 입력한 검색어를 실행한다.

검색 목록을 받는 동안에는 검색 자료를 불러오는 중입니다.라는 메시지도 보여주었다. 실패하면 새로고침을 안내한다.

여기서 배운 것은 단순하다.

느린 것과 고장 난 것은 사용자 눈에는 똑같이 보일 수 있다.


4. 검색 자료를 얼마나 넣어야 할까

본문을 많이 넣으면 더 많은 단어를 찾을 수 있다. 하지만 파일이 커지고, 방문자의 브라우저가 해야 할 일도 늘어난다.

여러 방법을 비교했다.

방법 장점 단점
본문 전체 저장 본문 속 단어를 거의 모두 찾음 파일과 계산량이 큼
본문 앞 3,000자 저장 전체 본문보다 가벼움 글 뒤쪽의 주제를 놓침
태그만 저장 매우 작고 빠름 태그를 빠뜨리면 검색 불가
외부 검색 서비스 사용 고급 기능이 많음 서버·비용·관리 범위가 커짐
제목·태그·부제목·제목 헤더 저장 작으면서 글의 구조를 반영 헤더 추출이 정확해야 함

이 블로그 규모에서는 외부 검색 서비스를 붙이는 것은 너무 큰 공사였다. 태그만 믿기에는 사람이 태그를 빠뜨릴 수 있었다.

그래서 제목 + 태그 + 부제목 + 카테고리 + 제목 헤더를 색인으로 사용하기로 했다. 검색 결과도 최대 10개만 보여주고, 목록의 대표 이미지는 제거했다.

본문 앞 3,000자를 넣던 때와 비교하면 제목 헤더의 문자 수는 약 94% 줄었다. 서점의 모든 책을 복사해 두는 대신 목차 카드만 모아둔 셈이다.


5. 띄어쓰기와 오타는 어디까지 봐줄까

사람은 다음 말을 거의 같다고 이해한다.

순환 신경망
순환신경망
Hidden State
Hidden-State

컴퓨터는 규칙을 알려주지 않으면 모두 다른 글자로 본다. 그래서 검색 전에 공백, 하이픈(-), 밑줄(_)을 비슷하게 다루도록 정리했다.

오타 검색도 넣었다. 다만 모든 글자를 마음대로 비슷하다고 처리하면 관계없는 결과가 쏟아진다. 그래서 검색어 길이에 따라 허용 범위를 제한했다.

  • 3글자 이하: 오타를 허용하지 않음
  • 4~7글자: 1글자 차이 허용
  • 8글자 이상: 2글자 차이 허용

예를 들어 TransfomerTransformer를 찾을 수 있다. 반면 AI, JS, C처럼 짧은 말은 한 글자만 달라져도 완전히 다른 말이 되므로 정확하게 일치해야 한다.

Fuzzy 검색은 “비슷한 글자 찾기”다. 글에 없는 개념을 알아서 상상해 주는 기능은 아니다. 이 차이를 놓치면 다음 문제를 이해하기 어렵다.


6. 세 번째 문제 — 글에 있는 혼동행렬을 찾지 못했다

검색 자료를 제목 헤더 중심으로 줄인 뒤 혼동행렬을 검색했다. 당연히 지도학습과 성능 평가 글이 나와야 했다.

원본 글에는 다음 제목이 분명히 있었다.

## 8. 분류의 평가 ② 혼동행렬

같은 개념을 실제 코드로 사용하는 방법은 scikit-learn 학습 순서를 정리한 글에서도 이어진다.

그런데 실제 검색 결과는 없었다. 오타 허용 범위를 넓혀도 소용없었다.

가장 먼저 생성 결과를 확인했다

소스만 계속 읽지 않고, GitHub Pages에 실제로 올라간 search.json을 열어봤다.

{
  "title": "SSAFY - < 3 >",
  "desc": "AI & 기계학습 기초 - 지도학습과 성능 평가",
  "headings": ""
}

headings가 비어 있었다. 검색 엔진은 잘못 찾은 것이 아니라, 애초에 혼동행렬이라는 단어를 전달받지 못했다.

232개 검색 문서 중 227개의 제목 헤더가 비어 있었다. 나머지 5개도 정상적인 제목이 아니라 Python 코드 블록 안의 # 주석을 잘못 가져온 값이었다.


7. 마크다운을 찾았는데 HTML이 들어왔다

원인은 Jekyll의 변환 순서였다.

글을 쓸 때는 다음처럼 마크다운을 사용한다.

## 혼동행렬

하지만 Jekyll은 이를 웹페이지용 HTML로 바꾼다.

<h2>혼동행렬</h2>

기존 제목 추출기는 ##으로 시작하는 마크다운 줄을 찾고 있었다. 그런데 추출기가 실제로 받은 내용은 이미 변환된 <h2>였다.

찾으려던 모양: ## 혼동행렬
실제로 받은 모양: <h2>혼동행렬</h2>

택배 상자에 적힌 글자를 찾는 규칙을 만들었는데, 그 사이 상자가 풀려 내용물만 도착한 것과 같다.

HTML 제목을 직접 읽도록 수정했다

해결 방법은 크지 않았다. <h1>부터 <h6>까지의 내용을 읽도록 필터를 바꿨다.

content.to_s
  .scan(/<h[1-6]\b[^>]*>(.*?)<\/h[1-6]>/im)
  .flatten

제목 안에 <strong> 같은 꾸밈 태그가 있으면 제거하고, &amp; 같은 HTML 표현은 다시 읽을 수 있는 문자로 바꿨다.

이제 코드 블록의 # 주석은 제목으로 착각하지 않는다. 실제 2026-08-09-ssafy2 페이지에서는 26개의 제목이 추출됐고, 그 안에 8. 분류의 평가 ② 혼동행렬도 포함됐다.


8. 고친 뒤 무엇을 검사했나

검색은 여러 단계가 이어진 기능이다. 한 단계만 검사하면 다른 곳에서 다시 무너질 수 있다.

그래서 다음 상황을 작은 시험으로 남겼다.

  1. 검색 목록이 늦게 와도 먼저 입력한 검색어가 사라지지 않는가
  2. RNN SSAFY처럼 단어가 서로 다른 항목에 있어도 찾는가
  3. 순환신경망처럼 띄어쓰기가 없어도 찾는가
  4. Transfomer 같은 작은 오타를 처리하는가
  5. URL에만 있는 단어는 검색에서 제외되는가
  6. 검색 결과가 최대 10개인가
  7. <h2>혼동행렬</h2>을 올바르게 추출하는가
  8. 코드 블록의 # 주석을 제목으로 착각하지 않는가

특히 마지막 두 검사는 이번 오류가 다시 생기지 않게 막는 안전망이다.


9. 이번 작업에서 배운 것

검색 알고리즘보다 검색 자료를 먼저 보자

검색이 실패하면 점수와 fuzzy 규칙부터 의심하기 쉽다. 하지만 색인에 단어가 없다면 아무리 좋은 알고리즘도 찾을 수 없다.

검색 실패 확인 순서
1. 원본 글에 단어가 있는가
2. 생성된 search.json에 단어가 있는가
3. 검색기가 그 항목을 검사하는가
4. 관련도 점수가 알맞은가
5. 화면에 올바르게 표시되는가

소스 코드와 생성 결과는 다를 수 있다

마크다운 소스에는 ## 혼동행렬이 있었다. 하지만 Jekyll이 필터에 넘긴 것은 <h2>혼동행렬</h2>였다.

빌드 도구를 사용하는 프로젝트에서는 내가 쓴 파일뿐 아니라 도구가 최종적으로 만든 파일도 확인해야 한다.

최적화는 무엇을 버리는지 정하는 일이다

본문을 없애면 검색 자료는 빨라진다. 대신 태그와 제목 헤더가 정확해야 한다. 성능 개선은 공짜가 아니라, 필요한 정보와 버려도 되는 정보를 고르는 일이다.

작은 수정도 실제 사용자 흐름으로 확인하자

문법 검사가 통과했다고 검색이 잘되는 것은 아니다. 마지막에는 블로그를 열고 RNN, 혼동행렬, 순환신경망처럼 실제로 찾을 말을 입력해야 한다.


정리

이번 검색 개선은 한 번의 수정으로 끝나지 않았다.

날짜순 검색
→ 관련도 점수 적용
→ 큰 검색 목록과 입력 누락 발견
→ 로딩 순서 수정
→ 본문 대신 제목 헤더 중심으로 경량화
→ 띄어쓰기와 제한된 오타 검색 적용
→ 혼동행렬 검색 실패 발견
→ 마크다운과 HTML 형태 불일치 수정
→ 회귀 테스트 추가

최종 구조는 단순하다.

  • 검색 서버 없이 브라우저에서 실행
  • 제목·태그·부제목·카테고리·제목 헤더만 검색
  • 관련도 점수 우선, 같은 점수일 때 최신 글 우선
  • 띄어쓰기와 하이픈 차이 처리
  • 길이에 따라 제한된 오타 허용
  • 결과는 최대 10개
  • 로딩과 실패 상태를 사용자에게 안내

오래 운영한 기능에서 가장 위험한 순간은 “이제 됐다”고 생각한 직후였다. RNN이 잘 검색된다고 모든 검색이 잘되는 것은 아니었다. 혼동행렬이라는 한 단어가 검색 자료를 만드는 과정 전체를 다시 보여줬다.