Skip to content

Comments

Refactor/#247: GNB와 SideDrawer의 HTML구조, 컨벤션, 접근성 리팩토링#187

Merged
juyesu merged 16 commits intodevelopfrom
refactor/#247-gnb-sidedrawer
Jul 6, 2025
Merged

Refactor/#247: GNB와 SideDrawer의 HTML구조, 컨벤션, 접근성 리팩토링#187
juyesu merged 16 commits intodevelopfrom
refactor/#247-gnb-sidedrawer

Conversation

@juyesu
Copy link
Collaborator

@juyesu juyesu commented Jun 30, 2025

변경 사항

여러 종류의 리팩토링을 진행한만큼
코드 변경 사항을 확인해주실 때, files changed보다는 commit이력을 따라가면서 확인해주시면 어떠한 목적으로 변경한 코드인지
확인하기에 용이할 것 같습니다.

  1. GNB를 서버 컴포넌트로 변경하고, 내부의 jsx를 자식 컴포넌트들에서 그리도록 변경하였습니다.
  • GNB를 서버 컴포넌트로 관리함으로써 얻는 이점을 기대하기 보다는 GNB 상위 컴포넌트의 jsx를 관심사에 따라 분리함으로써 컴포넌트의 책임을 명확하게 하고 가독성을 높이기 위한 목적으로 변경하였습니다.
  1. SideDrawer가 호출되는 위치가 기존 GNB 컴포넌트 내부에서 전역 layout의 main 외부로 변경하였습니다.
  • 레이아웃 계층상 header -> GNB -> SideDrawer 로 이어지는 구조보다는, 다른 header, main 요소들에 종속되지 않도록 SideDrawer를 위치시키는 것이 더 적절하기에 이와 같이 리팩토링 진행하였습니다.
  1. SideDrawer의 호출 상태를 관리하는 상태를 useSideDrawerStore 로 변경했습니다.
  • SideDrawer를 여는 버튼은 GNB 내부에, SideDrawer가 호출되는 위치는 GNB 외부에 있기 때문에 props로 상태를 전달하기엔 어렵고, 전역으로 SideDrawer의 상태를 관리할 필요가 있다고 느껴져 useSideDrawerStore을 통해 관리하도록 변경하였습니다.
  • Context와 비교하면 Context는 단 하나의 상태 관리만을 위해서 Provider를 선언해야 하고, 상태 변경 시 관련된 모든 컴포넌트가 리렌더링된다는 점에서 zustand store를 사용하는 방식이 더 직관적이고 적합하다고 판단하였습니다.
  1. HTML구조를 개선하였습니다. 불필요한 div 중첩 수를 줄이고, GNB로고에 사용된 h1을 제거, 리스트가 필요한 경우 nav, ul, li를 도입하는 등 전반적인 구조를 점검하였습니다.
  2. GNB에서 프로필 이미지를 클릭하면 열리는 드롭다운을 DropDown공통 컴포넌트로 변경하였습니다.
  • 변경 전
<div className="mt-2.5 w-40 overflow-hidden rounded-xl bg-gray-50 shadow-lg">
  <button
    onClick={() => gotoMyPage()}
    className="block w-full px-4 py-4 text-left text-sm hover:bg-gray-100"
  >
    마이페이지
  </button>
  • 변경 후
<Dropdown.Container
  id="profile-dropdown"
  role="menu"
  onKeyDown={handleMenuKeyDown}
  className="absolute top-full right-0 z-10 w-40 rounded-xl bg-gray-50 shadow-lg"
>
  <Dropdown.Content
    contentItem={
      <Link
        ref={(el) => {
          menuItemRef.current[0] = el;
        }}
        href={`${APP_ROUTES.mypage}`}
        tabIndex={0}
        role="menuitem"
        onClick={() => closeDropdown()}
        className="block w-full rounded-xl px-4 py-4 text-left text-sm font-medium hover:bg-gray-100"
      >
        마이페이지
      </Link>
    }
  />
  1. export const를 export default 방식으로 변경하고, 하드 코딩된 라우트명을 상수로 대체하는 등 기존에 컨벤션에 맞지 않는 부분들을 수정하였습니다.
  2. GNB, UserProfileDropdown, SideDrawer의 접근성을 개선하였습니다.
  • 드롭다운 또는 사이드 드로어가 열릴 때 자동으로 포커스가 가도록 설정하였습니다.
  • 시맨틱한 html 태그를 사용하여 의미를 파악하기에 더 용이하도록 하였습니다.
  • tabIndex를 적용하고, esc↑(Arrow up), ↓(Arrow down)방향키 조작을 지원하는 등 키보드 접근성을 향상시켰습니다.
  • 다양한 aria속성을 사용하여 필요한 label텍스트를 제공하고, 팝업의 열림 상태를 명시적으로 전달하는 등 스크린 리더 사용자에 대한 접근성 개선을 진행하였습니다.
// 사이드 드로어 컨테이너 개선 전
<div
  className={`fixed top-0 right-0 z-50 flex h-full w-2/3 transform flex-col bg-white p-4 transition-transform duration-300 ease-in-out md:hidden ${
    isOpen ? 'translate-x-0' : 'translate-x-full'
  }`}
>

// 사이드 드로어 컨테이너 개선 후
<div
  ref={drawerRef}
  role="dialog"
  aria-modal="true"
  aria-labelledby="side-drawer-title"
  tabIndex={-1}
  aria-hidden={!isDrawerOpen}
  className={`fixed top-0 right-0 z-[60] flex h-full w-2/3 transform flex-col bg-white p-4 transition-transform duration-300 ease-in-out md:hidden ${
    isDrawerOpen ? 'translate-x-0' : 'translate-x-full'
  }`}
>
  <h2 id="side-drawer-title" className="sr-only">
    메뉴 사이드 드로어
  </h2>
// 사이드 드로어 트리거 버튼 개선 전
<button
  onClick={openDropdown}
  className="items-center justify-center"
  aria-label="유저 메뉴 열기"
>
  {myInfo.image ? (
    <div className="h-10 w-10 overflow-hidden rounded-full bg-gray-300">
      <Image
        src={myInfo.image}
        alt="프로필 이미지"
        width={40}
        height={40}
      />
    </div>
  ) : (
    <DefaultProfileImage width={40} height={40} />
  )}
</button>

// 사이드 드로어 트리거 버튼 개선 후
<Dropdown
  isOpen={isDropdownOpen}
  onClose={closeDropdown}
  className="flex items-center"
  trigger={
    <button
      type="button"
      className="h-10 w-10 overflow-hidden rounded-full bg-gray-300"
      aria-label={`${profileImage && userName ? `${userName}님의 프로필 이미지.` : '기본 프로필 이미지.'}클릭하면 사용자 메뉴가 열립니다`}
      aria-expanded={isDropdownOpen}
      aria-controls="profile-dropdown"
      aria-haspopup="menu"
      onClick={toggleDropDown}
    >
      {profileImage && userName ? (
        <Image src={profileImage} alt="" width={40} height={40} />
      ) : (
        <DefaultProfileImage width={40} height={40} aria-hidden="true" />
      )}
    </button>
  }
>

juyesu added 13 commits June 29, 2025 17:35
- LoginSection 컴포넌트를 제거하고 해당 코드를 MenuGroups 컴포넌트 내부로 이동
- div 중첩을 줄이고 의미에 더 적합한 태그들로 변경 (로고에 사용된 h1 제거)
- 가독성을 위한 개행 적용
- 레이아웃을 더 명확하게 잡기 위해서 비효율적으로 작성된 css 수정
- useSideDrawerStore를 통해 상태 관리가 이루어지도록 변경
- SideDrawer 컴포넌트의 선언 위치를 레이아웃 최상위(main태그 바깥)로 변경
- 햄버거 메뉴 jsx 호출 위치를 MenuGroups 컴포넌트 내부로 이동
@juyesu juyesu self-assigned this Jun 30, 2025
@netlify
Copy link

netlify bot commented Jun 30, 2025

Deploy Preview for we-write ready!

Name Link
🔨 Latest commit d35cf7f
🔍 Latest deploy log https://app.netlify.com/projects/we-write/deploys/68665081feea6700089b8ecd
😎 Deploy Preview https://deploy-preview-187--we-write.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Collaborator

@hoxey2Front hoxey2Front left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~! 디자인 상으로는 많이 안바뀐거 같은데 코드상에는 엄청 많이 바뀌었네요 🙌


useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isDrawerOpen) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape는 처음 보는데 혹시 esc키 인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다 esc 입력을 통해 Drawer가 닫힐 수 있게 하는 이벤트입니다.

Copy link
Collaborator

@SwimmingRiver SwimmingRiver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~
브랜치 단위를 더 작게 해도 좋을 거 같아요!
어떤 방식으로 리팩토링이 필요한 부분을 찾아서 진행하시나요?

import { useAuth } from '@/providers/auth-provider/AuthProvider.client';
import useReferer from '@/hooks/useReferer';
import UserProfileDropdown from '@/components/layout/GNB/UserProfileDropdown';
import { Hamburger } from '@public/assets/icons';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lucide 아이콘으로 통일하는건 어떨까요?

Copy link
Collaborator Author

@juyesu juyesu Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 lucide 아이콘으로 변경할지 생각해보았는데 아직 기존에 사용된 SVGR을 lucide로 대체해자는 논의가 나오지 않은 상황에서 lucide 로 변경하는건 리팩토링보단 취향적인 부분이 될 것 같아서 이번 작업에는 제외됐습니다.

lucide 로 대체 가능한 svg는 lucide로 변경하는 것도 괜찮고, 이미 프로젝트에 사용된 svgr은 변경하지 않는 방법도 있을 것 같아서 위클리 스크럼 때 논의해볼까요?

Comment on lines 26 to 43
useEffect(() => {
if (isDropdownOpen) {
setFocusedIndex(0);
setTimeout(() => {
menuItemRef.current[0]?.focus();
}, 0);
}
}, [isDropdownOpen]);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isDropdownOpen) {
closeDropdown();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isDropdownOpen]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같은 의존성 배열을 갖는데 따로 분리된 이유가 있을까요?
구상하신 구조의 순서 때문일까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

첫 번째는 관심사를 분리(하나는 포커스 제어, 하나는 리스너 관리)하기 위함이고,
두 번째는 코드 가독성의 향상, 세 번째는 유지보수의 용이함 때문입니다.

두 로직의 연관 관계가 생기거나, 성능 개선의 필요가 생긴다면 useEffect 통합을 고민해볼 수 있을 것 같습니다

}
};

const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 좋은거 같네요
접근성을 위해서 전역적으로 설정할 필요도 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 기준에 따르면 Arrow Up / Down 키보드 조작은 "선택 가능한 UI 구성요소"에서 필수적으로 지원해야 한다고 합니다.
페이지내 모든 인터렉션까진 아니더라도 li태그나, role='menuitem' 등에는 공통적으로 적용되어도 좋을 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리팩토링에서 zustand 추가까지 넘어가게된 아이디어가 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 설명에 적혀져 있는 부분을 첨부하겠습니다!
혹시 아래의 설명에 더해서 추가적으로 궁금하신 점이 있으시다면 질문주세요!

  1. SideDrawer가 호출되는 위치가 기존 GNB 컴포넌트 내부에서 전역 layout의 main 외부로 변경하였습니다.
  • 레이아웃 계층상 header -> GNB -> SideDrawer 로 이어지는 구조보다는, 다른 header, main 요소들에 종속되지 않도록 SideDrawer를 위치시키는 것이 더 적절하기에 이와 같이 리팩토링 진행하였습니다.
  1. SideDrawer의 호출 상태를 관리하는 상태를 useSideDrawerStore 로 변경했습니다.
  • SideDrawer를 여는 버튼은 GNB 내부에, SideDrawer가 호출되는 위치는 GNB 외부에 있기 때문에 props로 상태를 전달하기엔 어렵고, 전역으로 SideDrawer의 상태를 관리할 필요가 있다고 느껴져 useSideDrawerStore을 통해 관리하도록 변경하였습니다.
  • Context와 비교하면 Context는 단 하나의 상태 관리만을 위해서 Provider를 선언해야 하고, 상태 변경 시 관련된 모든 컴포넌트가 리렌더링된다는 점에서 zustand store를 사용하는 방식이 더 직관적이고 적합하다고 판단하였습니다.

@juyesu juyesu added the Review-Applied 모든 리뷰를 반영완료하였음을 의미합니다. label Jul 3, 2025
@juyesu juyesu merged commit 6c20f9a into develop Jul 6, 2025
5 checks passed
@juyesu juyesu deleted the refactor/#247-gnb-sidedrawer branch July 6, 2025 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review-Applied 모든 리뷰를 반영완료하였음을 의미합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants