민감정보 처리 — 이 볼트는 iCloud 동기화 경로다. 계정·비밀번호·토큰, 운영 호스트 주소, 저장소 URL, 회원 실명·이메일, 그리고 보안 점검 결과 상세는 의도적으로 제외했다. 호스트는
<운영 CRM>/<운영 API>, 저장소는<모노레포>, 사내 디자인시스템 패키지는<사내 UI 패키지>로 대치했다. 상세는 사내 저장소의docs/문서에만 있다.
작업 대상: 내부 운영 어드민, Next 15 App Router.
증상
증상: 운영 CRM 에서 특정 관리 화면에 들어가면 사이드 메뉴가 통째로 사라진다. 콘솔 에러 없음. 그 화면에서만.
반증한 가설 5개
증상 정보가 적어서 가설을 세우고 깨는 과정을 반복했다. 전부 틀렸다.
| 가설 | 반증 방법 |
|---|---|
| 클라이언트 지시어 누락으로 컴파일 에러 | mutation 테스트 — 지시어 유무 무관하게 응답 동일 |
| 메뉴 트리가 빈 배열 | 실제 API 응답에 정상 포함 |
| flex 오버플로로 사이드바가 0폭 짜부 | 여러 폭에서 가로 오버플로 없음 |
| 권한 판정으로 숨겨짐 | 권한 가드는 본문 영역 안, 사이드바는 그 형제 — 구조적으로 불가 |
| 배포 환경이 구 코드 | 계정에 필요한 권한 레코드가 모두 있어 구 코드에서도 보였을 상황 |
실제 원인 — CSS 특이도 충돌
사이드바 래퍼 클래스가 hidden md:block 이었다. .hidden 과 .md\:block 은 특이도가 같아 문서 순서로 승자가 갈린다.
<사내 UI 패키지> 스타일시트가 Tailwind 기본 유틸을 재출력한다(.hidden·.block·.absolute 등). 그런데 반응형 변형(md:block)은 없다. 이 시트가 앱 시트보다 뒤에 로드되는 라우트에서는 .hidden{display:none} 이 이겨 사이드바가 display:none 이 된다.
왜 그 한 화면만이었나 — 이게 핵심이었다
같은 패키지를 쓰는 파일이 9개인데 하나만 터졌다. 조건이 셋 동시에 성립할 때만 난다.
- 라우트의
page.tsx가 서버 컴포넌트다 - 슬라이스 배럴이 그 패키지 소비 파일을 재노출한다 ← 9개 중 유일
- 그 소비 파일에 클라이언트 지시어가 없다
이러면 모듈이 서버 컴포넌트 그래프에 들어가고, 그 CSS 가 라우트 전용 청크로 분리된다. 라우트 청크는 공용 청크보다 뒤에 로드된다. 나머지 8개는 클라이언트 컴포넌트를 거쳐서만 도달해 CSS 가 앞쪽 공용 청크로 접힌다.
실측이 그대로 보여줬다 — 문제 라우트만 스타일시트가 1개 많고, 마지막 .hidden 선언이 그 추가 시트에 있었다.
그리고 조건 2는 내가 만들었다. 며칠 전 리팩터에서 형제 슬라이스가 쓸 수 있게 배럴에 재노출한 것이 서버 그래프 유입 경로를 만들었다. 아키텍처 규칙(슬라이스 내부 파일 직접 import 금지)을 지키려 한 변경이 다른 층의 문제를 만든 사례.
수정 2단
- 원인 제거 — 해당 파일에 클라이언트 지시어 추가. 클라이언트 그래프로 들어가 CSS 가 공용 청크로 접힌다. 동형 파일 9개도 함께 정리
- 방어선 — 래퍼를
hidden md:block→max-md:hidden md:block. 이건 미디어 쿼리 안으로 들어가 엘리먼트에서 맨hidden클래스를 없앤다. 외부.hidden이 매칭할 대상 자체가 사라져 로드 순서와 무관해진다
특이도 경쟁을 이기려 하지 말고 경쟁 자체를 없애는 쪽이 맞았다.
배포 후 재측정으로 확인 완료 — 시트 수가 줄고, 마지막 .hidden 이 앱 시트로 돌아오고, 사이드바가 정상 렌더된다.
오늘 배운 것
1. 프로덕션에서만 나는 버그가 있다. dev 서버는 CSS 를 한 덩이로 서빙하고 프로덕션만 라우트별로 청크를 쪼갠다. 로컬에서 몇 라운드를 돌렸는데 애초에 재현될 수 없는 환경이었다.
2. 재현은 사용자가 실제로 쓰는 URL 에서. "개발 환경"을 로컬로 가정하고 여러 번 검증했는데 대상 자체가 달랐다. URL 을 먼저 물어야 했다.
3. mutation 테스트는 지표를 먼저 정한다. 첫 가설의 위치는 맞았다. 그런데 "컴파일 에러가 나는지"만 보고 응답이 같다는 이유로 폐기했다. 실제 영향은 에러가 아니라 CSS 청크 배치였다 — 그때 스타일시트 목록을 봤어야 했다. 무엇이 달라져야 하는지를 정하지 않고 mutation 을 돌리면 맞는 가설도 버린다.
4. 타입이 거짓말하는 패턴이 반복된다. 응답에 오지 않는 필드를 non-optional 로, null 이 오는 필드를 non-null 로 선언한 게 이 API 하나에서 3건 나왔다. 새 필드를 non-optional 로 두기 전에 실호출로 확인. 타입을 사실대로 고치면 컴파일러가 나머지 누락 지점을 잡아준다(실제로 그렇게 1건 더 찾았다).
5. 목록을 자르고 판단하지 않는다. head -2 때문에 잘못된 원격에 push 했다.
6. 편의를 위한 자동 매핑이 적용 범위를 조용히 넓힐 수 있다. 레거시 행에 기본값을 채우는 건 UX 개선처럼 보이지만 의미가 달라진다. 사람이 명시적으로 고르게 두는 게 안전하다.
남은 것
- 사내 UI 패키지 빌드가 파손 상태 — 로컬에서 프로덕션 빌드 검증이 불가능하다. 배포는 되고 있으니 CI 환경에서는 통과하는 것으로 보인다
- 인증·메뉴 스토어가 실패를 조용히 삼키고 성공처럼 못박는다. 새로고침 전까지 복구되지 않는다. 이번 사고의 원인은 아니었지만 사이드바가 사라지는 또 다른 경로로 남아 있다