Skip to content

Conversation

@rtttr1
Copy link
Collaborator

@rtttr1 rtttr1 commented Jan 1, 2026

📌 Related Issues

✅ 체크 리스트

  • PR 제목의 형식을 잘 작성했나요? e.g. [Feat] PR 템플릿 작성
  • 빌드가 성공했나요? (pnpm build)
  • 리뷰어와 라벨을 지정했나요?

📄 Tasks

  • ModalLayout 컴포넌트 추가로 모달 레이아웃 및 이벤트 처리 분리
  • FocusTrap 컴포넌트 추가로 접근성 개선 (키보드 포커스 관리)
  • url 변경시 모달 store reset

⭐ PR Point

FocusTrap 컴포넌트로 접근성 개선 (키보드 포커스 관리)

기존에는 모달을 열고 Tab으로 모달내 버튼에 접근하려고해도 background에 있는 요소들로 먼저 접근해 모달의 버튼을 누르기 쉽지 않았어요.
시각장애를 가지고 계신 분들은 접근하기가 매우 불편한 구조였죠. 물론 시각장애가 없는 사용자의 사용성도 좋지 못해요.

2026-01-01.9.35.10.mov

이런 문제를 해결하고자 FocusTrap 컴포넌트를 개발했어요.
FocusTrap자식요소 내부로 포커스를 가두기가 관심사에요.
원리는 아래와 같아요.

  1. 자식 요소 내부의 인터렉티브한 요소들 탐색
  2. 랜더링시 탐색한 인터렉티브 요소들중 첫번째 요소에 focus 지정
  3. 첫요소에서 tab+shift, 마지막 요소에서 tab 키이벤트 발생시 각각 마지막 요소, 첫 요소로 focus 지정해 밖으로 포커스 이동 방지
// 자식 요소 내부의 포커스 가능한 요소들로 포커스 가두는 컴포넌트 
const FocusTrap = ({ children }: { children: ReactNode }) => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const container = containerRef.current;
    // 자식 요소 내부의 인터렉티브한 요소들 탐색
    const focusableElements = container.querySelectorAll<HTMLElement>(
      'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]'
    );

    const firstFocusableElement = focusableElements[0];
    const lastFocusableElement = focusableElements[focusableElements.length - 1];

    // 랜더링시 첫 요소에 포커스 줘서 포커스 가두기
    firstFocusableElement.focus();

    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.shiftKey) {
        // 첫번째 요소에서 shift + Tab 키이벤트 발생시 마지막 요소로 foucs 이동
        if (document.activeElement === firstFocusableElement) {
          lastFocusableElement.focus();
        }
      } else {
        // 마지막 요소에서 Tab 키이벤트 발생시 첫 요소로 focus 이동
        if (document.activeElement === lastFocusableElement) {
          firstFocusableElement.focus();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, []);

  return <div ref={containerRef}>{children}</div>;
};

export default FocusTrap;

이제 모달을 FocusTrap으로 감싸서 랜더링해주면 다음과 같이 모달 내부로 포커스가 갇혀서 키보드 접근성을 개선할 수 있어요.
모달 뿐만 아니라 드로어, 드롭다운 등 포커스가 내부에 머물러야하는 요소들에 다 사용할 수 있으니 필요할때 적절히 사용해봐요

2026-01-01.9.24.12.mov

ModalLayout 컴포넌트 추가로 모달 레이아웃 및 이벤트 처리 분리

ModalLayout 컴포넌트의 책임은 다음과 같아요.

  1. esc키로 최상단 모달 닫는 이벤트 관리
  2. 모달 외부(ModalLayout 부분) 클릭시 모달 닫는 이벤트 관리
  3. 모달이 띄어져있으면 backdrop 역할 (배경색 어둡게)

기존에는 Modal 컴포넌트와 ModalLayout을 같이 랜더링해주는 구조였어요.

// Modal 내부에서 ModalLayout 랜더링
const Modal = ({}: DialogProps) => {
  return (
    <ModalLayout onClose={onClose}>
      <div className={description ? containerNoGapStyle : containerStyle}>
        // ...
      </div>
    </ModalLayout>
  );
};

export default Modal;

이때 3번 책임 때문에 1 모달 1 ModalLayout 이라 모달이 2개이상 랜더링 되면 배경색이 더 어두워지는 버그가 있었어요. (ModalLayout이 여러개 랜더링되어)

2026-01-01.9.34.08.mov

이를 해결하고자 Modal내부에서 ModalLayout을 분리해 ModalLayout은 여러개의 모달을 띄워도 한개만 유지되게 구조를 변경했어요.
사실 관심사와 확장성을 생각하면 애초에 분리하는게 맞는 구조라 생각해요.

이때 문제가 되는 지점이 2 번 책임이었어요.
현재 모달은 id값을 통해서 close 로직을 관리하고 있어요.
즉, 모달 랜더링 함수를 store에 저장할때 지정되는 id값을 ModalLayout이 알아야 모달을 닫을 수 있는거죠.
기존에는 모달에서 ModalLayout을 랜더링하기 때문에 ModalLayout에 id를 알려줄수 있지만 분리하게 되면 각 모달에서 id를 알려주지 못해요.

이를 modalStore에서 최상단 모달을 지우는 closeLastModal액션을 추가해서 문제를 해결했어요.
어차피 여러개의 모달을 띄우고 esc, 모달 외부 클릭시 최상단 모달만 닫으면 되기 때문이에요.

closeLastModal: () => set((state) => ({ modalStore: state.modalStore.slice(0, -1) })),
2026-01-01.9.23.49.mov

url 변경시 모달 store reset

  // ModalProvier.tsx
  const location = useLocation();

  useEffect(() => {
    resetStore();
  }, [location.pathname, resetStore]);

ModalProvier 내부에서 useLocation 훅을 활용해 url 변경시 모달 store를 reset해주는 로직을 추가했어요.
건휘가 고생하던게 어쩌면 이걸로 해결되지 않을까? 하는 희망을 품어봐요.

📷 Screenshot

🔔 ETC

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 1, 2026

Deploying dash-client-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8dd6981
Status: ✅  Deploy successful!
Preview URL: https://8b4552cc.dash-client-dev.pages.dev
Branch Preview URL: https://refactor--599-modal-renderin.dash-client-dev.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

Deploying dash-client with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8dd6981
Status: ✅  Deploy successful!
Preview URL: https://1b0edfc3.dash-client.pages.dev
Branch Preview URL: https://refactor--599-modal-renderin.dash-client.pages.dev

View logs

@rtttr1 rtttr1 changed the title [Refactor] 포커스트랩 추가, 모달 2개 이상 랜더링시 배경 어두워지는 버그 해결 [Refactor] FocusTrap 추가, 모달 2개 이상 랜더링시 배경 어두워지는 버그 해결 Jan 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] 모달 로직 개선

2 participants