01 THINK어떻게 생각하는가

사이드바가 한 화면에서만 사라졌다

가설 다섯 개를 세워 전부 반증하고 나서야 원인을 찾았다. CSS 특이도와 라우트 청크 로드 순서의 문제였다.

민감정보 처리 — 이 볼트는 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개인데 하나만 터졌다. 조건이 셋 동시에 성립할 때만 난다.

  1. 라우트의 page.tsx서버 컴포넌트
  2. 슬라이스 배럴이 그 패키지 소비 파일을 재노출한다 ← 9개 중 유일
  3. 그 소비 파일에 클라이언트 지시어가 없다

이러면 모듈이 서버 컴포넌트 그래프에 들어가고, 그 CSS 가 라우트 전용 청크로 분리된다. 라우트 청크는 공용 청크보다 뒤에 로드된다. 나머지 8개는 클라이언트 컴포넌트를 거쳐서만 도달해 CSS 가 앞쪽 공용 청크로 접힌다.

실측이 그대로 보여줬다 — 문제 라우트만 스타일시트가 1개 많고, 마지막 .hidden 선언이 그 추가 시트에 있었다.

그리고 조건 2는 내가 만들었다. 며칠 전 리팩터에서 형제 슬라이스가 쓸 수 있게 배럴에 재노출한 것이 서버 그래프 유입 경로를 만들었다. 아키텍처 규칙(슬라이스 내부 파일 직접 import 금지)을 지키려 한 변경이 다른 층의 문제를 만든 사례.

수정 2단

  • 원인 제거 — 해당 파일에 클라이언트 지시어 추가. 클라이언트 그래프로 들어가 CSS 가 공용 청크로 접힌다. 동형 파일 9개도 함께 정리
  • 방어선 — 래퍼를 hidden md:blockmax-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 환경에서는 통과하는 것으로 보인다
  • 인증·메뉴 스토어가 실패를 조용히 삼키고 성공처럼 못박는다. 새로고침 전까지 복구되지 않는다. 이번 사고의 원인은 아니었지만 사이드바가 사라지는 또 다른 경로로 남아 있다
debuggingcssnextjsrsc

기록

2026.08.26에 처음 커밋됐고, 이후 2번 더 손댔습니다.

  • e1f77d9Content: 영어판 4차 — crm-sidebar-vanished · bb-mcp-boundaries
  • 029cadaTopics: a controlled axis to browse by, and a filter that costs no JavaScript
  • 411ca64Content: six entries carried over from the vault

전체 빌드 기록 →