React 모달/드로어의 스크롤 잠금이 해제되지 않는 문제 해결하기
카운터 기반 scrollLockManager로 overflow 관리 중앙화하기
2025년 12월 21일
React 모달/드로어의 스크롤 잠금이 해제되지 않는 문제 해결하기
모달과 드로어를 닫고 페이지를 이동했는데, 스크롤이 작동하지 않았습니다. 왜 이런 일이 생겼고 어떻게 해결했는지 정리해봤어요 🎄
문제 상황
제가 만든 서비스는 이런 흐름이었어요.
Drawer 열림 → 모달로 확인 → API 요청 → 모달 닫힘 → Drawer 닫힘 → 페이지 이동
이 과정에서 Drawer와 모달은 열려있는 동안 배경 스크롤을 막기 위해 overflow: hidden 처리를 해두었습니다.
그런데 페이지 이동 후, 새 페이지에서 스크롤이 되지 않는 문제가 발생했습니다.
원인 분석
1. cleanup 실행 순서의 불일치
페이지 A에서 페이지 B로 이동하면, React는 컴포넌트 트리를 순회하며 cleanup을 실행합니다. 이 과정에서 Drawer와 Modal의 cleanup 실행 순서가 의도와 달랐어요.
- Modal 입장:
originalOverflow는hidden(Drawer가 먼저 열려서) - Drawer 입장:
originalOverflow는auto(원래 상태)
둘의 cleanup 순서가 바뀌면, 최종적으로 overflow: hidden이 남게 됩니다.
2. 컴포넌트 생명주기의 차이
제가 놓쳤던 점은 모달과 Drawer를 다르게 관리하고 있었다는 거였어요.
| 컴포넌트 | 관리 방식 | cleanup 타이밍 |
|---|---|---|
| Drawer | 페이지 내 useState | 페이지 언마운트 시 즉시 |
| Modal | OverlayProvider 레벨 | 페이지와 독립적 |
Drawer는 페이지의 일부라 먼저 cleanup 되어 스크롤을 풀었지만, Modal은 나중에 cleanup 되면서 다시 hidden으로 덮어쓴 거죠.
3. 근본 원인: 분산된 상태 관리
Drawer를 useOverlay 훅으로 바꾸면 해결은 되지만, 이 방식은 여전히 컴포넌트 트리 구조와 추가 순서에 의존합니다. 코드가 방대해지고 여러 개발자가 함께 작업하면 또다시 같은 문제가 발생할 수 있어요.
결국 문제의 본질은 각 컴포넌트가 스스로 overflow를 관리한다는 점입니다. 모달은 열릴 때 hidden, 닫힐 때 원래값 복원. Drawer도 똑같이 하니까 서로의 상태를 덮어쓰게 됩니다.
해결책: 중앙 집중식 스크롤 잠금 매니저
설계 원칙
이걸 해결하려면 **지금 스크롤을 잠가야 하는 컴포넌트가 몇 개인가?**를 중앙에서 관리해야 합니다.
MUI의 ModalManager를 참고했을 때, 카운팅을 통해 스크롤 잠금을 관리하고 있었어요.
| 상태 변화 | 동작 |
|---|---|
| count: 0 → 1 | overflow: hidden 적용 |
| count: 1+ → 1+ | 변경 없음 |
| count: 1 → 0 | 원래 스타일로 복원 |
scrollLockManager.ts
타입 정의와 전역 상태
모노레포나 별도 패키지 환경에서는 각 패키지가 자체 모듈 스코프를 가지기 때문에 let lockCount = 0이 패키지마다 별도로 존재하게 됩니다.
반면 window 객체는 브라우저 탭당 하나만 존재하므로 패키지 경계를 넘어 상태 공유가 가능합니다.
전역 상태 조회 (Singleton)
스크롤 잠금 (lock)
스크롤 잠금 해제 (unlock)
useBodyScrollLock.ts
수정을 진행하면서 두 가지 UX 문제도 함께 해결해볼 수 있었습니다.
- Layout Shift 방지: 스크롤바가 사라질 때 컨텐츠가 밀리는 현상 → 스크롤바 너비만큼
padding-right추가 - 깜빡임 방지:
useLayoutEffect를 사용해 브라우저가 화면을 그리기 전에 DOM 조작
마치며
이번 문제 해결을 통해 useEffect와 useLayoutEffect의 차이, React 렌더링 방식, 변수의 스코프, 컴포넌트 생명주기 등을 다시 공부할 수 있는 기회가 됐습니다.
빠른 출시로 인해 더 많이 고민해보지는 못했지만, 화면 만들기에서 벗어나 오랜만에 깊이 고민해볼 수 있어서 재밌었어요.
앞으로도 즐거운 트러블슈팅을 겪기를 바라며... 미리 메리 크리스마스 🎄