사이드바 UX 개선기: 가독성, 페이지네이션, 그리고 검색까지
1. 고민과 문제점
프로젝트가 커지고 블로그 포스트가 하나둘 쌓이다 보니 사이드바(Explorer) 영역에서 몇 가지 문제점이 눈에 띄기 시작했습니다.- 가독성 저하: 파일명이 길어질 경우 사이드바 영역을 넘어가거나 잘려서 무슨 파일인지 한눈에 알기 어려웠습니다.
- 확장성 부족: 포스트가 수백 개로 늘어난다면 스크롤이 끝도 없이 길어질 것이 뻔했습니다. VSCode처럼 트리 구조를 가지고 있지만, 수많은 파일이 한 번에 나열되는 것은 사용자 경험(UX)에 좋지 않아 보였습니다.
2. 선택지 분석
가독성 문제
- 옵션 A: 사이드바 너비를 늘린다. -> 공간 효율성이 떨어짐.
- 옵션 B: 줄바꿈 처리한다. -> 파일 목록의 통일성이 깨짐.
- 옵션 C: 말줄임(...) 처리하고 툴팁(Tooltip)을 제공한다. -> 가장 깔끔하고 VSCode스러운 방식.
확장성 문제 (많은 파일)
- 옵션 A: 가상 스크롤(Virtual Scrolling) 도입. -> 구현 복잡도 증가.
- 옵션 B: 페이지네이션(Pagination) 도입. -> '더 보기' 링크를 통해 대시보드로 이동시키는 방식이 깔끔하다고 판단. 탐색기에서는 최신 글 10개만 보여주고 나머지는 대시보드에서 관리하도록 유도.
3. 해결 과정
Step 1: 가독성 및 디자인 개선
우선ExplorerItem 컴포넌트를 손봤습니다. 긴 파일명은 CSS truncate 속성으로 말줄임 처리하고, shadcn/ui의 Tooltip 컴포넌트를 활용해 마우스를 올렸을 때 전체 이름이 보이도록 했습니다. 또한 아이콘 사이즈가 제각각이던 문제를 해결하기 위해 shrink-0와 고정 사이즈(size-4)를 적용하여 정렬을 맞췄습니다.Step 2: 재귀적 페이지네이션 (Recursive Pagination)
가장 고민했던 부분입니다. 단순히 최상위 폴더만 제한하는 것이 아니라,blog/dev나 blog/journal 같은 하위 폴더에도 동일한 규칙(10개 제한)이 적용되어야 했습니다.use-explorer 훅 내부에 재귀 함수(limitItemsRecursively)를 구현하여 트리의 모든 깊이에서 10개 이상의 항목이 있으면 잘라내고, 마지막에 ... (더 보기) 링크를 추가하는 로직을 완성했습니다.// use-explorer.tsx (Simplified)
const limitItemsRecursively = (items: ITree[]): ITree[] => {
// ... 정렬 로직 ...
if (sorted.length > 10) {
const sliced = sorted.slice(0, 10);
sliced.push({
id: "(more)",
label: t("more"), // 다국어 지원
path: "/blog/dashboard",
icon: "none", // 아이콘 제거
});
result = sliced;
}
// ... 재귀 호출 ...
};icon: "none")하고, 다국어(messages)를 적용해 완성도를 높였습니다.4. 블로그 대시보드 검색 기능 추가
페이지네이션으로 '더 보기' 눌렀을 때 이동하는 대시보드 페이지에서 포스트가 많아지면 원하는 글을 찾기 어렵겠다는 생각이 들어 검색 기능을 추가하기로 했습니다.요구사항
- 제목 기반 검색: 포스트 제목으로 필터링
- Debounce 적용: 타이핑할 때마다 검색하지 않고 입력이 멈춘 후 검색
- 자동완성(Autocomplete): 검색어에 맞는 포스트 제목을 드롭다운으로 제안
삽질 기록: Debounce 라이브러리 선택
처음엔@toss/react 라이브러리의 useDebounce 훅을 사용하려 했습니다. 토스에서 만든 라이브러리라 믿음직스러웠거든요.npm install @toss/reactnpm error ERESOLVE could not resolve
npm error peer react@"^17.0.1 || ^18.0.0" from @toss/react@1.5.2--legacy-peer-deps로 강제 설치는 가능했지만, 더 큰 문제가 있었습니다.// 예상했던 사용법
const debouncedQuery = useDebounce(query, 300);
// 실제 @toss/react의 useDebounce 시그니처
useDebounce(callback: (...args: any[]) => any, wait: number);@toss/react의 useDebounce는 값이 아니라 함수를 debounce하는 용도였습니다. 타입 에러가 발생했습니다:Argument of type 'string' is not assignable to parameter of type '(...args: any[]) => any'해결: lodash.debounce로 전환
결국 가장 안정적이고 널리 사용되는lodash의 debounce를 선택했습니다.npm install lodash @types/lodash// dashboard-search.tsx
import { debounce } from "lodash";
const updateDebouncedQuery = useCallback(
debounce((value: string) => {
setDebouncedQuery(value);
}, 300),
[],
);또 다른 버그: 자동완성 드롭다운이 안 닫힘
구현 후 테스트해보니 자동완성 드롭다운에서 항목을 선택해도 드롭다운이 닫히지 않는 버그가 있었습니다.원인을 분석해보니:handleSelect에서setSuggestions([])를 호출- 하지만
setDebouncedQuery(title)호출 useEffect가debouncedQuery변경 감지- 다시
setSuggestions를 채워버림
useBoolean 훅을 활용하여 드롭다운 표시를 명시적으로 제어했습니다:import { useBoolean } from "@/hooks/use-boolean";
// useBoolean 훅으로 드롭다운 상태 관리
const dropdown = useBoolean(false);
const handleSelect = (title: string) => {
setQuery(title);
setDebouncedQuery(title);
dropdown.onFalse(); // 명시적으로 닫기
};
// JSX
{dropdown.value && suggestions.length > 0 && query && (
<ul>...</ul>
)}useBoolean은 onTrue(), onFalse(), onToggle() 같은 명시적인 메서드를 제공해서 setIsOpen(true) 같은 코드보다 의도가 더 명확하게 드러납니다.5. 최종 결과
- 깔끔해진 사이드바: 파일명이 길어도 레이아웃이 깨지지 않으며, 툴팁으로 내용을 확인할 수 있습니다.
- 부담 없는 탐색기: 파일이 아무리 많아져도 폴더 당 10개로 유지되어 시각적 피로도가 줄었습니다.
- 편리한 검색: 대시보드에서 제목으로 빠르게 원하는 포스트를 찾을 수 있고, 자동완성으로 더 빠른 탐색이 가능합니다.
6. 후기
단순한 기능 구현을 넘어 "사용자가(그리고 개발자인 내가) 어떻게 하면 더 편하게 쓸 수 있을까?"를 고민했던 작업이었습니다.특히 이번 작업에서 느낀 점:- 라이브러리 선택 시 React 버전 호환성 확인은 필수입니다. 최신 React 19를 쓰고 있다면 더욱 그렇습니다.
- API 시그니처를 꼭 확인해야 합니다. 같은 이름의 훅이라도 "값을 debounce"하는지 "함수를 debounce"하는지가 다를 수 있습니다.
- 커스텀 훅을 적극 활용하자.
useBoolean처럼 단순하지만 코드 가독성을 높여주는 훅은 작은 기능에서도 빛을 발합니다.