Astro에서 블로그 검색/필터를 가장 ‘Astro답게’ 구현하는 방법
2026년 1월 6일 오전 1:26
오늘은 Astro를 쓰면서 생겼던 질문들 —
“프레임워크 불가지론은 어떻게 가능한가?”, “섬(Islands) 방식에서 번들은
어떻게 줄어드는가?”,
그리고 최종적으로 **“블로그 글 검색/필터를 어떻게 구현하면 Astro의 의미가
유지되는가?”**를 한 흐름으로 정리했다.
결론은 간단하다:
리스트는 Astro로 정적으로 렌더링하고(HTML-first), 검색/필터는 바닐라 JS로 DOM을 숨김/표시만 하자.
글이 1000개 이하라면 이 방식은 충분히 빠르고, Astro의 장점을 극대화한다.
1) Astro의 “프레임워크 불가지론”은 어떤 의미인가?
Astro가 프레임워크 불가지론(framework-agnostic)이라는 말은:
- Astro의 기본 렌더링 모델이 React/Vue/Svelte 같은 특정 프레임워크에 종속되지 않고,
- 필요할 때만 특정 프레임워크 컴포넌트를 섬(island) 형태로 끼워 넣을 수 있다는 의미다.
예를 들어 아래처럼 한 프로젝트에서 다양한 컴포넌트를 “섬”으로 사용할 수 있다:
src/components/Button.tsx(React)src/components/Widget.svelte(Svelte)
그런데 이렇게 섞으려면 의존성이 늘지 않나?
맞다. .tsx를 쓰면 react, react-dom과 통합 패키지(@astrojs/react)가
필요하고,
.svelte를 쓰면 svelte와 통합 패키지(@astrojs/svelte)가 필요하다.
하지만 핵심은:
- 의존성이 늘어나는 것(node_modules)
- 클라이언트 번들이 늘어나는 것(브라우저로 전달되는 JS)
이 둘은 다르다.
Astro는 기본적으로 HTML을 먼저 만들고, 클라이언트 JS는 필요한 섬에만
붙인다.
즉, 프로젝트 안에 React/Svelte가 공존할 수는 있어도, 브라우저로는 실제로
hydrate 되는 섬에 필요한 런타임만 나갈 수 있다.
2) React 섬은 “전체 DOM”을 diff하나?
질문 요지:
작은 버튼 하나를 React로 섬(hydration)하면
React가 페이지 전체 DOM을 비교(diff)하나?
아니면 그 버튼 subtree만 비교하나?
정답:
- React는 페이지 전체 DOM을 기준으로 비교하지 않는다.
- React가 hydrate한 “섬의 루트 컨테이너” 아래 subtree에서만 리렌더/리컨실리에이션이 일어난다.
즉, 버튼 하나면 사실상 버튼 subtree에 한정되어 작동한다.
다만 Svelte vs React의 체감 차이는 보통 diff 방식보다:
- 초기 로드(JS 런타임 크기)
- hydration 초기 비용
에서 더 크게 나타나는 경우가 많다.
3) Astro에서 JS 인터랙션은 보통 어떻게 구현하나?
블로그/문서/뉴스 같은 정적 콘텐츠 중심 프로젝트에서는 흔히 이렇게 간다:
✅ 작은 인터랙션
- 다크모드 토글
- 코드 Copy 버튼
- TOC 접기/펼치기
- 간단한 탭/아코디언
👉 이런 건 바닐라 JS로 처리하는 경우가 많다.
✅ 조금 복잡한 위젯(상태가 얽힘)
- 검색바 + 결과 리스트
- 태그 필터 + 정렬
- 자동완성
👉 이건 섬(island)으로 프레임워크를 올리는 것도 흔한 선택이다.
4) 라우팅: pages/about.astro면 /about/로 가는 이유
Astro 자체가 강제로 /about/를 강제한다기보단,
정적 배포에서 결과물이 보통 이렇게 나오기 때문이다:
dist/about/index.html
정적 서버는 /about를 “파일”로 보려다가, 폴더 구조가 있으면 /about/로
정규화(redirect)하는 경우가 많다.
5) 블로그 글 검색은 Astro vs React 중 뭐가 나은가?
“블로그 글 검색” (Content Collections)이라면 보통 Astro가 더 유리
- 검색은 페이지 전체가 아니라 상단 섹션 + 리스트 영역만 인터랙티브면 된다.
- Astro는 HTML-first라서 기본 페이지 성능(SEO, 초기 렌더링)이 좋다.
- React로 전체를 SPA처럼 만들면 검색은 편해져도 불필요한 JS 비용이 커질 수 있다.
6) 내가 원하는 UI: “상단 검색/필터 → 하단 포스트 리스트가 즉시 변경”
스크린샷처럼:
- 상단에 검색바/필터
- 아래엔 날짜 그룹 헤더 + 글 리스트
- 검색/태그/날짜 필터가 바뀌면 아래 리스트가 즉시 바뀌어야 함
여기서 흔한 고민:
그럼 검색 섹션 + 리스트를 통째로 React 섬으로 만들면
Astro의 의미가 퇴색되는 거 아닌가?
✅ 맞다. 그래서 “Astro답게” 하려면 리스트는 Astro로 유지한다.
권장 구조(오늘의 결론):
- 리스트(포스트 카드/날짜 그룹)는 Astro가 정적으로 렌더링
- 검색/필터는 바닐라 JS로 이벤트 처리
- 필터 결과는 DOM에서
hidden토글로 표시/숨김
이 방식이면:
- JS 없이도 리스트는 초기부터 보임 (HTML-first 유지)
- 클라이언트 JS는 필터 컨트롤만 담당
- 글 1000개 이하라면 충분히 빠름
7) 성능: 글 1000개 이하라면 DOM 필터링 괜찮나?
결론: 대부분 충분히 괜찮다.
다만 중요한 건 “DOM 업데이트 비용”을 최소화하는 것.
필수 최적화 포인트
- 검색 입력은
debounce(80~150ms) - DOM 업데이트는
el.hidden = true/false또는 class 토글로 최소화 - dataset을 매번 읽지 말고, 초기에
{date, tags[], searchText, el}형태로 메모리 캐싱 - 날짜 그룹 섹션도 “보이는 글이 0개면” 같이 숨기기
8) 최종 구현: SearchBar.astro + 정적 리스트 + 바닐라 JS 필터링
설계 개요
index.astro에서:getCollection("blog")로 글 로드- 날짜별 그룹 렌더링
- 각 글 카드에
data-date,data-tags,data-search를 심는다
SearchBar.astro에서:- 검색 입력, 태그 필터, 캘린더 UI 렌더
- 페이지의
[data-post-item]를 찾고 메모리 캐싱 - 필터 변경 시
applyFilters()실행 →hidden토글
필터 데이터 구조 예시
각 글 카드에 박는 데이터:
data-date="YYYY-MM-DD"data-tags="tag1,tag2,tag3"data-search="title description tags..."(lower-case)
이걸로:
- 텍스트 검색:
includes - 태그 필터: AND/OR 조건
- 날짜 필터: YYYY-MM-DD 매칭
로직 Flow 요약
✅ 서버(Astro)
- 글 목록 로드 → 정렬/그룹핑
- HTML로 리스트 렌더링(SEO/초기 렌더링 강점)
- 각 글/그룹에
data-*주입 - 태그 목록, 글 있는 날짜 목록을
SearchBar.astroprops로 전달
✅ 클라이언트(JS)
- DOM에서 글 카드들을 찾고 캐싱
- 검색/태그/날짜 UI 이벤트 연결
- 필터 변경 시
applyFilters() - 매칭 안 되는 글은
hidden=true - 날짜 그룹 섹션도 내부에 보이는 글 없으면 숨김
- 결과 카운트 갱신
마무리
Astro로 블로그를 만들 때 검색/필터는 “무조건 프레임워크 섬”으로 만들 필요가
없다.
특히 글 1000개 이하라면:
- 리스트는 Astro가 정적으로 렌더링
- 검색/필터는 바닐라 JS로 DOM 숨김/표시
이 방식이 가장 Astro의 철학(HTML-first, 최소 JS)을 살리면서도 충분히 좋은 UX를 만든다.
다음에 개선할 수 있는 확장 아이디어
- 필터 패널을 드로어/모달로 변경
- URL 쿼리(
?q=&tags=&date=)와 동기화 (공유/뒤로가기 강화) - 본문까지 진짜 풀텍스트 검색이 필요하면 JSON 인덱스 + 클라이언트 검색(Fuse/MiniSearch/Pagefind)로 확장