예전 프로젝트에서 사용자 친화적인 사이드바(Sidebar)를 만들기 위해 다양한 UI/UX 개선을 적용했다.
진행하고 있는 AI TUTOR 프로젝트의 SIDEBAR의 UI/UX에도 개선이 필요했다.
1. Hovering을 하고 있지도 않은데, Hovering 시 바뀌는 스타일이 불필요하게 적용됨
2. toggling 반응속도가 느림
이때 쓸 수 있는 방법은 JavaScript의 Set<number> 자료구조를 활용한 열림/접힘 상태 관리 최적화가 있었다.
이 문서는 해당 리팩토링 과정과 그 이유, 그리고 부수적인 UI 버그 해결 방법까지 총망라한 정리이다.
1. 기존 사이드바 상태 관리 방식
초기에는 각 폴더의 노트 리스트 열림 여부를 다음과 같이 관리했다:
const [isNoteOpen, setIsNoteOpen] = useState<{ [id: number]: boolean }>({});
이 객체 기반 방식은 열림/접힘을 true/false로 저장하고, 토글할 때는 다음과 같이 처리했다:
const toggleNote = (id: number) => {
setIsNoteOpen(prev => ({
...prev,
[id]: !prev[id],
}));
};
문제점
- 객체에 매번 복사 연산(...prev)이 들어가서 불변성 관리가 번거롭다
- 처음 열렸다 닫혔다 한 노트의 key는 계속 남아 있어 불필요한 상태 누적이 발생한다
- 복수의 열림 상태가 중첩되며 UI 버그로 이어지기도 한다 (예: 접힌 줄 알았는데 상태가 true로 유지)
2. 개선 전략: Set<number> 자료구조 도입
열려 있는 폴더 ID만 Set에 저장하는 방식으로 전환:
const [noteOpenSet, setNoteOpenSet] = useState<Set<number>>(new Set());
장점
항목개선 전 (Object)개선 후 (Set)
상태 구조 | { 1: true, 2: false } | Set(1, 3) |
중복 허용 | 가능 | 자동 제거됨 |
열림 판단 | isNoteOpen[id] | noteOpenSet.has(id) |
토글 처리 | 불변성 복잡 | add/delete 간단 |
불필요한 키 | 남음 | 존재하지 않음 |
토글 함수 예시
const toggleNote = (id: number) => {
setNoteOpenSet(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
이제 상태는 "열려 있는 폴더 ID만 저장"하면 되므로 매우 직관적이고 깔끔해졌다.
3. JavaScript Set 자료구조 간단 설명
Set은 JavaScript의 내장 자료구조로, 중복되지 않는 값들의 집합이다.
const s = new Set();
s.add(1);
s.add(2);
s.add(1); // 무시됨
s.has(2); // true
s.delete(2);
특징:
- 값의 존재 여부를 O(1) 시간에 확인 가능
- Array와 달리 중복이 없음
- 불필요한 상태가 누적되지 않음
TypeScript에서는 Set<number>, Set<string> 등으로 타입 지정 가능하다.
4. Hover 잔상 문제 해결
문제
노트 클릭 시 회색 배경(bg-black-90)이 남고, 다른 노트를 눌러도 이전 항목의 색상이 유지되는 문제 발생
원인
- 선택된 노트를 판별할 때 useParams()의 folderId, noteId를 사용
- router.push() 직후엔 params가 즉시 반영되지 않아 예전 값이 남아 있음
해결: usePathname() 기반 판단
const pathname = usePathname();
const notePath = `/notes/${folder.folderId}/${note.noteId}/summary`;
const isSelectedNote = pathname === notePath;
- URL이 바뀌는 즉시 pathname이 업데이트되므로, 선택 상태가 정확하게 반영됨
- hover는 hover:bg-black-90, 선택된 노트는 bg-black-90으로 시각적 충돌 없음
5. Pathname vs Params 라우터 동기화 구조 비교
라우팅 시 router.push() 이후 어떤 값이 먼저 반영되는지 이해하는 것이 중요하다.
usePathname()
- 브라우저 주소 문자열(/notes/1/2/summary)을 즉시 반환
- 내부적으로는 window.location.pathname처럼 동작
- router.push() 호출 직후 곧바로 업데이트됨 → 즉시성 있음
useParams()
- URL 동적 세그먼트([folderId], [noteId] 등)를 객체로 반환
- 단, 해당 경로에 맞는 컴포넌트가 다시 렌더링된 후에야 값이 반영됨
- 이 과정은 비동기이며, 레이아웃 구조에 따라 한 템포 늦음
반영 흐름 예시
시점 | pathname | params |
router.push() 직후 | /notes/1/2/summary (바뀜) | { folderId: '이전', noteId: '이전' } |
리렌더링 후 | /notes/1/2/summary | { folderId: '1', noteId: '2' } |
결국, 선택된 메뉴를 강조하는 UI 처리를 할 때는 usePathname()이 더 신뢰성이 높다.
6. 폴더 선택 강조 개선
기존에는 선택된 폴더의 구분이 불명확했음. 개선된 방식:
const folderPath = `/notes/${folder.folderId}`;
const isSelectedFolder = pathname.startsWith(folderPath);
<div className={isSelectedFolder ? 'bg-black-90 border-l-4 border-mju-primary' : ''}>
- 하위 노트를 보고 있어도 해당 폴더가 선택된 것으로 간주되므로 UX 향상됨
7. 전체 리팩토링 효과
항목 | 개선 전 | 개선 후 |
열림 상태 관리 | object 구조, 복잡 | Set<number>, 간결함 |
선택 항목 판별 | 느림, 지연 (params) | 즉시, 정확 (pathname) |
UI 버그 | hover 잔상 남음 | 깔끔하게 해소됨 |
유지보수성 | 복잡하고 취약 | 단순하고 명확 |
라우터 의존성 | params와 sync 필요 | pathname으로 안전하게 분리 |
8. 결론 및 확장 방향
- Set을 활용한 상태 관리는 사이드바 UI 같은 토글형 컴포넌트에 매우 유용
- pathname 기반 비교는 라우터 동작 지연 문제를 피해가는 가장 직관적인 방법
- 추후 accordion UX로 확장 시에는 Set이 아닌 단일 openedFolderId: number | null 구조로도 대체 가능
- framer-motion 등 애니메이션을 추가하면 UX를 더욱 부드럽게 만들 수 있음
요약: Set은 단순한 상태 저장 이상의 UX 안정성과 코드 명확성을 가져다준다. Sidebar 개선 과정은 그 대표적인 사례가 될 수 있다.
'FrontEnd Develop > Project : AI TUTOR' 카테고리의 다른 글
.env를 모르고 올렸을때의 대처 : Git Filter-Repo 이후 충돌 해결 및 복구 시나리오 (0) | 2025.07.04 |
---|---|
전등, 스위치, 전선으로 이해하는 Zustand 전역 UI 참조 (0) | 2025.06.28 |
STT 라우팅과 비동기 처리 분리하기: 실전 리팩토링 사례 (0) | 2025.05.05 |
튜토리얼 모달 컴포넌트 기능 개발 #2. 상세 설계서 (0) | 2025.03.11 |
튜토리얼 모달 컴포넌트 기능 개발 #1. 기본 설계서 (0) | 2025.03.09 |