Skip to content

Conversation

@Bangdayeon
Copy link
Member

@Bangdayeon Bangdayeon commented Jul 22, 2025

관련 이슈

PR 설명

  • 사용자가 버튼 클릭 시 옵션 리스트를 확인하고 항목을 선택할 수 있는 드롭다운 컴포넌트
  • 세부 스타일링은 디자인 스펙 확정 후 반영 예정
  • 서버 연동을 통한 옵션 데이터 수신을 고려하여 별도 API 핸들링 모듈 추가 예정

useDropdown.ts

  • dropdown의 상태를 관리
반환값 타입 설명
isOpen boolean 드롭다운 메뉴의 열림 여부 상태 (true = 열림, false = 닫힘)
toggle () => void isOpen 상태를 토글 (열기/닫기 전환)하는 함수
close () => void 드롭다운을 강제로 닫는 함수 (isOpen을 false로 설정)

props | type | 설명
ref | React.RefObject<HTMLDivElement> | 드롭다운 DOM 요소 참조를 위한 ref. 외부 클릭 감지를 위해 사용

Dropdown.tsx

  • 선택형 드롭다운 컴포넌트로, 사용자 입력을 트리거로 옵션 리스트를 렌더링
  • 선택된 옵션은 버튼에 표시되며, 변경 시 콜백 함수가 실행
  • 디자인 요소로 크기(size) 및 색상(color) 옵션을 제공
  • 내부에서 useDropdown 훅으로 열림/닫힘 상태 및 외부 클릭 감지를 관리
  • 메뉴 UI는 DropdownMenu 컴포넌트로 분리되어 있음
props type description
options DropdownOption[] 드롭다운에 표시할 옵션 목록
defaultSelected DropdownOption 초기 선택값. 기본값은 options[0]
onSelect (option: DropdownOption) => void 사용자가 옵션을 선택했을 때 실행되는 콜백 함수
size "sm" | "md" | "lg" 버튼 및 메뉴 항목의 크기 조정 옵션

@Bangdayeon Bangdayeon requested a review from Seong-Myeong July 22, 2025 15:34
@Bangdayeon Bangdayeon linked an issue Jul 22, 2025 that may be closed by this pull request
@Goder-0
Copy link
Contributor

Goder-0 commented Jul 23, 2025

PR 제목을 수정해주세요

@Bangdayeon Bangdayeon changed the title Feat: add dropdown component (#85) Dropdown 컴포넌트 구현 Jul 23, 2025
@Bangdayeon
Copy link
Member Author

PR 제목 수정 완료했습니다

@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch 3 times, most recently from ecb7991 to d65f2ee Compare August 12, 2025 13:17
@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from d65f2ee to a349beb Compare August 28, 2025 10:35
@github-actions
Copy link

@Bangdayeon Bangdayeon requested a review from Goder-0 August 28, 2025 11:14
@Bangdayeon
Copy link
Member Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Sep 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Sep 21, 2025

Warning

Rate limit exceeded

@Bangdayeon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7e109 and b27e952.

📒 Files selected for processing (4)
  • src/components/basics/Dropdown/Dropdown.styles.ts
  • src/components/basics/Dropdown/Dropdown.tsx
  • src/components/basics/Dropdown/useDropdown.ts
  • src/stories/Dropdown.stories.tsx

Walkthrough

드롭다운 컴포넌트를 새로 구현했습니다. Tailwind Variants 기반 스타일링 모듈과 함께, 상태 관리, 키보드 상호작용, 접근성 속성을 지원하는 forwardRef 기반 React 컴포넌트를 추가했습니다. useDropdown 훅은 외부 포인터 이벤트와 Escape 키로 드롭다운을 닫는 기능을 관리합니다. DropdownOptionDropdownProps 인터페이스를 정의하고, sm/md/lg 크기 옵션을 지원합니다. Storybook 스토리 파일도 함께 추가되었습니다.

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 드롭다운 컴포넌트 구현이라는 핵심 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 필수 섹션인 '관련 이슈'와 'PR 설명'을 포함하고 있으며, 구현된 훅과 컴포넌트에 대한 자세한 설명이 포함되어 있습니다.
Linked Issues check ✅ Passed PR 변경사항은 #85의 요구사항을 충족합니다: 드롭다운 형식 메뉴 표시, size 옵션 지원, ReactNode/string 콘텐츠 처리 기능이 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항은 드롭다운 컴포넌트 구현(#85)과 직접적으로 관련된 것으로, 스타일, 훅, 컴포넌트, 스토리 파일 추가이며 범위를 벗어난 변경사항은 없습니다.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from a349beb to 538d649 Compare September 21, 2025 14:42
@github-actions
Copy link

@Bangdayeon
Copy link
Member Author

Bangdayeon commented Sep 21, 2025

수정 사항

  • 스토리북 meta 선언부 최신화
  • useDropdown.ts 오타 수정
  • DropdownMenu onClick 전달 props 역할 분리
  • a11y 내용 추가

@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from 538d649 to bba101e Compare September 21, 2025 14:55
@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (9)
src/components/Dropdown/useDropdown.ts (2)

10-18: 모바일 외부 클릭 누락 + ESC 키 미처리

모바일 터치(또는 펜) 입력이 닫힘 처리되지 않고, ESC 키로 닫기 동작도 없습니다. pointerdown/keydown 리스너로 보완하세요. 또한 effect 내부에서 close() 참조 대신 setIsOpen(false)를 직접 호출하면 deps 경고를 피할 수 있습니다.

-  useEffect(() => {
-    const handleClickOutside = (e: MouseEvent) => {
-      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
-        close();
-      }
-    };
-    document.addEventListener('mousedown', handleClickOutside);
-    return () => document.removeEventListener('mousedown', handleClickOutside);
-  }, []);
+  useEffect(() => {
+    const onPointerDown = (e: PointerEvent) => {
+      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
+        setIsOpen(false);
+      }
+    };
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') setIsOpen(false);
+    };
+    document.addEventListener('pointerdown', onPointerDown);
+    document.addEventListener('keydown', onKeyDown);
+    return () => {
+      document.removeEventListener('pointerdown', onPointerDown);
+      document.removeEventListener('keydown', onKeyDown);
+    };
+  }, []);

1-1: toggle/close 메모이제이션으로 핸들러 안정화

prop으로 내려보낼 가능성을 고려하면 useCallback으로 고정해두는 편이 안전합니다.

-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useRef, useState, useCallback } from 'react';

-  const toggle = () => setIsOpen(prev => !prev);
-  const close = () => setIsOpen(false);
+  const toggle = useCallback(() => setIsOpen(prev => !prev), []);
+  const close = useCallback(() => setIsOpen(false), []);

Also applies to: 7-8

src/components/Dropdown/DropdownMenu.tsx (3)

15-21: Tailwind 클래스 오타 및 충돌 수정 필요

in-w-max는 유효하지 않은 유틸리티이고, borderborder-none이 상충합니다. 메뉴가 다른 요소 아래 깔리는 문제도 방지하려면 z-index를 지정하세요.

-  const baseStyle = clsx(
-    'absolute block in-w-max text-gray-900 bg-white border my-1 rounded-md shadow-md border-none',
-    'max-h-40 overflow-y-auto',
-    'overflow-hidden whitespace-nowrap text-ellipsis'
-  );
+  const baseStyle = clsx(
+    'absolute block min-w-max text-gray-900 bg-white my-1 rounded-md shadow-md z-10',
+    'max-h-40 overflow-y-auto',
+    'ring-1 ring-gray-200'
+  );

20-25: 항목 패딩이 이중 정의됨

menuStyle에 패딩이 있고 sizes에도 패딩이 있어 중복됩니다. sizes만 사용하도록 정리하세요. 또한 항목 텍스트 말줄임은 li에 적용하는 편이 맞습니다.

-  const menuStyle = 'px-4 py-2 transition-colors hover:bg-gray-200 cursor-pointer';
+  const menuStyle = 'transition-colors hover:bg-gray-200 cursor-pointer truncate';

Also applies to: 27-27


6-6: 타입 순환 의존성 완화 제안

DropdownMenuDropdown에서 타입을 가져오면 구조가 강하게 결합됩니다. Dropdown.types.ts(또는 types.ts)로 DropdownOption을 분리해 양방향 참조를 없애는 것을 권장합니다.

src/components/Dropdown/Dropdown.stories.tsx (2)

39-43: 스토리 args 타입 간소화

ComponentProps<typeof Dropdown>로 간단히 표현하면 React 제네릭 추론 없이도 명확합니다. 이에 따라 import도 정리 가능합니다.

-import React, { useState } from 'react';
+import { useState, type ComponentProps } from 'react';
@@
-function InteractiveDropdown(
-  args: typeof Dropdown extends React.ComponentType<infer P> ? P : never
-) {
+function InteractiveDropdown(args: ComponentProps<typeof Dropdown>) {

Also applies to: 2-2


7-13: 데모 텍스트 오탈자

"Banaaaana"는 문서 품질 측면에서 오타로 보입니다.

-  { value: 'banana', label: '🍌 Banaaaana' },
+  { value: 'banana', label: '🍌 Banana' },
src/components/Dropdown/Dropdown.tsx (2)

33-39: 패딩 클래스 중복 제거

buttonStyle에 패딩이 있고 sizes에도 패딩이 있어 중복입니다. buttonStyle에서 패딩을 제거하고 sizes로만 제어하세요.

-  const buttonStyle =
-    'px-4 py-2 text-gray-900 max-w-60 rounded-md transition-colors hover:cursor-pointer';
+  const buttonStyle =
+    'text-gray-900 max-w-60 rounded-md transition-colors hover:cursor-pointer';

59-64: ARIA 연결 보완(권장)

버튼에 aria-controls를 부여해 메뉴 요소와 연결하면 보조기기 호환성이 좋아집니다. useId()로 id를 생성해 DropdownMenuul에 전달하는 방식을 추천합니다. (동시 PR로 DropdownMenuid prop 추가 필요)

예시 변경(개략):

  • Dropdown.tsx: import { useId } from 'react' 추가, const menuId = useId(); 생성, 버튼에 aria-controls={isOpen ? menuId : undefined} 설정, <DropdownMenu id={menuId} ... /> 전달
  • DropdownMenu.tsx: DropdownMenuPropsid?: string 추가, <ul id={id} role="menu">로 반영

원하시면 관련 패치까지 한 번에 만들어 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03e486b and bba101e.

📒 Files selected for processing (4)
  • src/components/Dropdown/Dropdown.stories.tsx (1 hunks)
  • src/components/Dropdown/Dropdown.tsx (1 hunks)
  • src/components/Dropdown/DropdownMenu.tsx (1 hunks)
  • src/components/Dropdown/useDropdown.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/components/Dropdown/DropdownMenu.tsx (1)
src/components/Dropdown/Dropdown.tsx (1)
  • DropdownOption (9-12)
src/components/Dropdown/Dropdown.stories.tsx (1)
src/components/Dropdown/Dropdown.tsx (2)
  • DropdownOption (9-12)
  • Dropdown (22-67)
src/components/Dropdown/Dropdown.tsx (2)
src/components/Dropdown/useDropdown.ts (1)
  • useDropdown (3-20)
src/components/Dropdown/DropdownMenu.tsx (1)
  • DropdownMenu (14-44)

@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from bba101e to 568f845 Compare September 21, 2025 16:40
@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/components/Dropdown/DropdownMenu.tsx (5)

38-41: Space 키 호환성 보강

일부 환경에서 Space 키가 "Spacebar"로 보고됩니다. 아래처럼 분기 추가를 권장합니다.

-            if (e.key === 'Enter' || e.key === ' ') {
+            if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {

16-19: 텍스트 말줄임 처리 위치 수정 (ul → li)

현재 text-ellipsisul에 적용돼 항목별 말줄임이 동작하지 않습니다. 스크롤은 컨테이너(ul), 말줄임은 항목(li)에 적용하세요.

   const baseStyle = clsx(
-    'absolute block min-w-max text-gray-900 bg-white my-1 rounded-md shadow-md border-none z-10',
-    'max-h-40 overflow-y-auto',
-    'overflow-hidden whitespace-nowrap text-ellipsis'
+    'absolute block min-w-max text-gray-900 bg-white my-1 rounded-md shadow-md border-none z-10',
+    'max-h-40 overflow-y-auto overflow-x-hidden'
   );
-  const menuStyle = 'transition-colors hover:bg-gray-200 cursor-pointer';
+  const menuStyle = 'transition-colors hover:bg-gray-200 cursor-pointer truncate';

Also applies to: 20-21, 27-27


29-49: 로빙 탭인덱스 및 화살표 키 탐색 추가 제안

현재 모든 항목이 tabIndex=0으로 탭 정지가 과도합니다. 메뉴 패턴에 맞춰 로빙 탭인덱스(하나만 0, 나머지 -1)와 ArrowUp/Down, Home/End 포커스 이동을 추가하세요. Enter/Space 활성화는 기존대로 li에서 처리 유지 권장.

 export function DropdownMenu({ options, onOptionClick, size }: DropdownMenuProps) {
   const baseStyle = clsx(
@@
-  return (
-    <ul className={baseStyle} role="menu">
-      {options.map(option => (
-        <li
+  const listRef = React.useRef<HTMLUListElement>(null);
+  const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLUListElement>) => {
+    const items = listRef.current?.querySelectorAll<HTMLLIElement>('li[role="menuitem"]');
+    if (!items || items.length === 0) return;
+    const arr = Array.from(items);
+    const currentIndex = arr.findIndex(el => el === document.activeElement);
+    let next = currentIndex;
+    if (e.key === 'ArrowDown') { e.preventDefault(); next = (currentIndex + 1 + arr.length) % arr.length; }
+    if (e.key === 'ArrowUp')   { e.preventDefault(); next = (currentIndex - 1 + arr.length) % arr.length; }
+    if (e.key === 'Home')      { e.preventDefault(); next = 0; }
+    if (e.key === 'End')       { e.preventDefault(); next = arr.length - 1; }
+    if (next !== currentIndex && next >= 0) arr[next].focus();
+  };
+
+  return (
+    <ul ref={listRef} className={baseStyle} role="menu" onKeyDown={handleMenuKeyDown}>
+      {options.map((option, index) => (
+        <li
           className={menuClasses}
           key={option.value}
           onClick={() => onOptionClick(option)}
           role="menuitem"
-          tabIndex={0}
+          tabIndex={index === 0 ? 0 : -1}
           onKeyDown={e => {
             if (e.key === 'Enter' || e.key === ' ') {
               e.preventDefault();
               onOptionClick(option);
             }
           }}
         >
           {option.label}
         </li>
       ))}
     </ul>
   );
 }

20-21: 포커스 가시성 보강

키보드 사용자용 포커스 스타일을 추가하세요.

-  const menuStyle = 'transition-colors hover:bg-gray-200 cursor-pointer';
+  const menuStyle = 'transition-colors hover:bg-gray-200 cursor-pointer focus:outline-none focus-visible:bg-gray-200 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-500';

8-12: 메뉴 레이블링 옵션 추가 제안 (a11y)

트리거와의 연결(aria-labelledby) 또는 자체 라벨(aria-label)을 prop으로 받을 수 있도록 확장하면 스크린 리더 경험이 개선됩니다.

 interface DropdownMenuProps {
   options: DropdownOption[];
   onOptionClick: (option: DropdownOption) => void;
   size: 'sm' | 'md' | 'lg';
+  ariaLabel?: string;
+  labelledById?: string;
 }
@@
-export function DropdownMenu({ options, onOptionClick, size }: DropdownMenuProps) {
+export function DropdownMenu({ options, onOptionClick, size, ariaLabel, labelledById }: DropdownMenuProps) {
@@
-    <ul className={baseStyle} role="menu">
+    <ul className={baseStyle} role="menu" aria-label={ariaLabel} aria-labelledby={labelledById}>

검증 요청:

  • 트리거 버튼 측에서 aria-haspopup="menu"aria-expanded가 설정돼 있는지 확인 부탁드립니다.

Also applies to: 30-30

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bba101e and 568f845.

📒 Files selected for processing (4)
  • src/components/Dropdown/Dropdown.stories.tsx (1 hunks)
  • src/components/Dropdown/Dropdown.tsx (1 hunks)
  • src/components/Dropdown/DropdownMenu.tsx (1 hunks)
  • src/components/Dropdown/useDropdown.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/Dropdown/useDropdown.ts
  • src/components/Dropdown/Dropdown.stories.tsx
  • src/components/Dropdown/Dropdown.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Dropdown/DropdownMenu.tsx (1)
src/components/Dropdown/Dropdown.tsx (1)
  • DropdownOption (9-12)
🔇 Additional comments (1)
src/components/Dropdown/DropdownMenu.tsx (1)

30-37: 이전 피드백 반영 LGTM: role 및 기본 키보드 활성화 정상

ul role="menu", li role="menuitem"으로 수정했고, 클릭 활성화도 적절합니다. 잘 반영되었습니다.

@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from 568f845 to 862f955 Compare November 2, 2025 14:21
@github-actions
Copy link

github-actions bot commented Nov 2, 2025

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/components/Dropdown/useDropdown.ts (1)

10-27: 드롭다운이 닫혀있을 때도 이벤트 리스너가 활성 상태입니다.

현재 구현은 드롭다운의 열림 여부와 관계없이 항상 document 레벨 이벤트 리스너를 등록합니다. 페이지에 여러 드롭다운이 있는 경우 불필요한 이벤트 처리가 누적될 수 있습니다.

isOpen 상태가 true일 때만 리스너를 등록하도록 최적화를 고려해보세요.

다음과 같이 리팩토링할 수 있습니다:

  useEffect(() => {
+   if (!isOpen) return;
+
    // 모바일 외부 클릭 처리
    const onPointerDown = (e: PointerEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    };
    // ESC 키 처리
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setIsOpen(false);
    };
    document.addEventListener('pointerdown', onPointerDown);
    document.addEventListener('keydown', onKeyDown);
    return () => {
      document.removeEventListener('pointerdown', onPointerDown);
      document.removeEventListener('keydown', onKeyDown);
    };
-  }, [ref]);
+  }, [ref, isOpen]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 568f845 and 862f955.

📒 Files selected for processing (3)
  • src/components/Dropdown/Dropdown.stories.tsx (1 hunks)
  • src/components/Dropdown/Dropdown.tsx (1 hunks)
  • src/components/Dropdown/useDropdown.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Dropdown/Dropdown.tsx
  • src/components/Dropdown/Dropdown.stories.tsx
🔇 Additional comments (2)
src/components/Dropdown/useDropdown.ts (2)

6-8: useCallback 메모이제이션 적절함.

toggle 및 close 함수를 useCallback으로 메모이제이션하여 불필요한 재생성을 방지한 것은 좋은 패턴입니다.


12-16: 리뷰 댓글은 부정확합니다. 현재 구현이 올바르게 작동하고 있습니다.

코드를 검증한 결과:

  1. ref 위치가 올바름: Dropdown.tsx 43줄의 <div ref={ref}>가 토글 버튼과 드롭다운 메뉴를 모두 감싸고 있습니다.

  2. 이벤트 처리 로직이 정상 작동: useDropdown.ts 13줄의 ref.current.contains(e.target as Node) 체크로 인해:

    • 버튼 클릭 시: 버튼이 ref 컨테이너 내부에 있으므로 contains() 반환값이 TRUE → 드롭다운이 닫히지 않음
    • 외부 클릭 시: contains() 반환값이 FALSE → 드롭다운이 닫힘
  3. 결과: 토글 버튼 클릭 시 드롭다운이 즉시 닫히지 않으며, 버튼의 onClick={toggle} 핸들러가 정상적으로 작동합니다.

Likely an incorrect or invalid review comment.

@Bangdayeon Bangdayeon self-assigned this Nov 2, 2025
@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from 862f955 to 839d8ed Compare November 2, 2025 15:17
@github-actions
Copy link

github-actions bot commented Nov 2, 2025

@Seong-Myeong
Copy link
Contributor

Seong-Myeong commented Nov 6, 2025

문서로 정리한 기본 컴포넌트 구현 방식을 따르는지 확인부탁드립니다

@Bangdayeon Bangdayeon removed their assignment Nov 7, 2025
@Bangdayeon Bangdayeon force-pushed the feature/#85-add-dropdown-component branch from 839d8ed to 8280261 Compare November 20, 2025 06:06
@github-actions
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/components/basics/Dropdown/useDropdown.ts (1)

10-27: 리스너 최적화를 고려해보세요.

현재 구현은 드롭다운이 닫혀있을 때도 전역 리스너가 계속 활성화되어 있습니다. isOpen 상태에 따라 조건부로 리스너를 등록하면 불필요한 이벤트 처리를 줄일 수 있습니다.

🔎 최적화 제안
  useEffect(() => {
+   if (!isOpen) return;
+
    // 모바일 외부 클릭 처리
    const onPointerDown = (e: PointerEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    };
    // ESC 키 처리
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setIsOpen(false);
    };
    document.addEventListener('pointerdown', onPointerDown);
    document.addEventListener('keydown', onKeyDown);
    return () => {
      document.removeEventListener('pointerdown', onPointerDown);
      document.removeEventListener('keydown', onKeyDown);
    };
-  }, [ref]);
+  }, [ref, isOpen]);
src/components/basics/Dropdown/Dropdown.tsx (2)

22-28: 중복된 기본값 처리를 단순화하세요.

defaultSelected는 이미 파라미터에서 options[0]로 기본값이 설정되어 있는데, Line 27에서 다시 ?? options[0] ?? null로 체크하고 있습니다. 이는 불필요한 중복입니다.

🔎 단순화 제안
 const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(function Dropdown(
   { options, defaultSelected = options[0], size = 'sm', onSelect, ...rest },
   ref
 ) {
   const { isOpen, toggle, close } = useDropdown(ref as React.RefObject<HTMLDivElement>);
-  const [selected, setSelected] = useState<DropdownOption | null>(
-    defaultSelected ?? options[0] ?? null
-  );
+  const [selected, setSelected] = useState<DropdownOption>(defaultSelected);

48-70: 불필요한 div 래퍼를 제거하세요.

Line 50의 빈 <div>는 아무런 역할을 하지 않으므로 제거할 수 있습니다.

🔎 수정 제안
       {isOpen && options.length > 0 && (
         <ul className={contentStyle()} role="menu">
-          <div className="">
-            {options.map(option => (
-              <li
-                className={listStyle({ size })}
-                key={option.value}
-                onClick={() => handleOptionClick(option)}
-                role="menuItem"
-                tabIndex={0}
-                onKeyDown={e => {
-                  if (e.key === 'Enter' || e.key === ' ') {
-                    e.preventDefault();
-                    handleOptionClick(option);
-                  }
-                }}
-              >
-                {option.label}
-              </li>
-            ))}
-          </div>
+          {options.map(option => (
+            <li
+              className={listStyle({ size })}
+              key={option.value}
+              onClick={() => handleOptionClick(option)}
+              role="menuItem"
+              tabIndex={0}
+              onKeyDown={e => {
+                if (e.key === 'Enter' || e.key === ' ') {
+                  e.preventDefault();
+                  handleOptionClick(option);
+                }
+              }}
+            >
+              {option.label}
+            </li>
+          ))}
         </ul>
       )}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 862f955 and 1f7e109.

📒 Files selected for processing (4)
  • src/components/basics/Dropdown/Dropdown.styles.ts
  • src/components/basics/Dropdown/Dropdown.tsx
  • src/components/basics/Dropdown/useDropdown.ts
  • src/stories/Dropdown.stories.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/stories/Dropdown.stories.tsx (1)
src/components/basics/Dropdown/Dropdown.tsx (1)
  • DropdownOption (9-12)
src/components/basics/Dropdown/Dropdown.tsx (2)
src/components/basics/Dropdown/useDropdown.ts (1)
  • useDropdown (3-29)
src/components/basics/Dropdown/Dropdown.styles.ts (2)
  • contentStyle (3-5)
  • listStyle (7-16)
🪛 GitHub Actions: CI
src/stories/Dropdown.stories.tsx

[error] 2-2: ESLint: storybook/no-renderer-packages - Do not import renderer package "@storybook/react" directly. Use a framework package instead. Command: 'pnpm exec eslint "src/**/*.{js,jsx,ts,tsx}" --max-warnings=0'

🪛 GitHub Check: ci
src/stories/Dropdown.stories.tsx

[failure] 2-2:
Do not import renderer package "@storybook/react" directly. Use a framework package instead (e.g. @storybook/nextjs, @storybook/react-vite, @storybook/nextjs-vite, @storybook/react-webpack5, @storybook/react-native-web-vite)

🔇 Additional comments (2)
src/components/basics/Dropdown/Dropdown.tsx (1)

38-47: color prop과 Button의 variant 관계를 명확히 하세요.

Line 45에서 Buttonvariant"primary"로 하드코딩되어 있습니다. 정의되어 있지만 사용되지 않는 color prop이 Button의 스타일을 제어하기 위한 것인지 확인이 필요합니다.

만약 color prop이 Button의 색상을 제어해야 한다면, 이를 variant나 다른 Button prop에 매핑해야 합니다.

src/stories/Dropdown.stories.tsx (1)

2-2: import 문은 올바릅니다. Storybook 8.x에서 Next.js를 사용할 때, 타입 MetaStoryObj@storybook/react에서 import하는 것이 표준입니다. @storybook/nextjs는 프레임워크 설정과 렌더링을 위한 것이며, 타입 정의는 @storybook/react에서 제공합니다.

Likely an incorrect or invalid review comment.

@github-actions
Copy link

github-actions bot commented Jan 4, 2026

@Bangdayeon Bangdayeon self-assigned this Jan 4, 2026
@Bangdayeon
Copy link
Member Author

다른 컴포넌트들과 같이 형식을 맞췄습니다. 접근성 등이 미완성이나, mvp에서 dropdown이 들어가지 않기 때문에 큰 문제가 없으면 머지해도 좋을 것 같습니다. @Seong-Myeong @Goder-0

Comment on lines 37 to 38
onSelect(option);
close();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
onSelect(option);
close();
onSelect(option);
on close()

현재 close()가 부모와 자식에서 모두 호출되어 동일 상태 업데이트가 두 번 일어납니다. 닫기 책임은 부모에만 두고 자식에서는 선택 이벤트만 전달하는 게 좋습니다.

import React, { useState } from 'react';

import { DropdownMenu } from './DropdownMenu';
import { useDropdown } from './usdDropdown';
Copy link
Contributor

Choose a reason for hiding this comment

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

파일명 오타로 보입니다.

Suggested change
import { useDropdown } from './usdDropdown';
import { useDropdown } from './useDropdown';

];

const meta = {
title: 'Components/Dropdown',
Copy link
Contributor

Choose a reason for hiding this comment

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

해당부분 경로 수정해야합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Bangdayeon 해당부분 진행되고 머지해주세요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dropdown 컴포넌트 구현

4 participants