FrontEnd Develop/Project : AI TUTOR

Set 자료구조로 사이드바 UX 개선하기

Frisbeen 2025. 7. 9. 07:00

예전 프로젝트에서 사용자 친화적인 사이드바(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 개선 과정은 그 대표적인 사례가 될 수 있다.