1. 개요

동일한 HTML 마크업과 Tailwind CSS 코드가 구글 크롬(Chrome)이나 윈도우 엣지(Edge)에서는 자로 잰 듯이 완벽하게 흘러가는데, 유독 아이폰 사파리(Safari) 브라우저에서만 처참하게 일그러질 때가 있더군요. DoEatFit (v2.0) 개발 막바지 단계에서 바로 그 WebKit 엔진 특유의 달력 레이아웃 붕괴 결함과 조우했습니다. 식단/운동 기록 달력 UI를 띄워 확인해 보니마지막 주차가 하단 통계 패널과 겹쳐서 잘리거나, 개별 날짜 텍스트 정중앙 정렬이 무너지는 등 모바일 사용성에 상당한 손상을 입히던 상황이었습니다. 이를 웹 표준 스펙에 순수하게 귀의하여 해결한 과정을 꼼꼼히 정리합니다.


2. 핵심 내용

2-1. 🚨 문제 현상 및 근본 원인 분석

  • 현상:
    • 마지막 주차 하단 잘림 및 겹침 현상: 달력 구조가 5주에서 6주차로 길어지는 달(예: 31일이 일요일인 달)이 되면, 달력의 맨 아래 행이 하단 통계/요약 레이아웃과 거칠게 물리적으로 충돌하며 영역이 무참히 덮어씌워지거나 잘렸습니다.
    • 날짜 숫자의 수직 쏠림: 개별 날짜를 가리키는 둥근 원형 배경 내부에서 숫자가 정중앙에 수평/수직 정렬되지 못하고, 아래쪽이나 측면으로 2~3px 가량 삐딱하게 치우쳐 렌더링되었습니다.
  • 원인:
    1. WebKit 엔진의 엄격한 테이블 규칙과 Flex의 불협화음: 달력 핵심 라이브러리인 react-day-picker는 전통적이고 규칙적인 HTML table 요소 구조(table, tbody, tr, td)를 뼈대로 삼으며 개별 테이블 행(tr) 및 셀(td)에 인라인으로 display: flex 관련 Tailwind 스타일을 끼워 넣고 있었습니다. 크롬(Blink)은 테이블 내 display: flex가 강제 삽입되더라도 일반 플렉스박스인 것처럼 영역을 적절히 도출해 내는 반면, 사파리(WebKit) 엔진은 테이블 요소에 억지로 Flexbox 속성을 구겨 넣을 경우 내부 요소들의 정확한 픽셀 단위 높이 계산을 완전히 포기해 버려 최종 높이가 0px에 수렴하고 하단 패널이 위로 마구 치고 올라오는 대참사를 냈습니다.
    2. leading-none과 폰트 Baseline 기준선 오차: 날짜 텍스트 중앙 정렬을 위해 지정해 둔 leading-none (line-height: 1) 속성이 사파리 폰트 메트릭 계산기에서 베이스라인(Baseline, 글꼴의 하단 정렬 기준선)을 고집스럽게 우선하여 하단 마진을 극도로 좁혀버렸기에 날짜 숫자가 둥근 배경 내부에서 하단으로 쏠렸던 것입니다.

2-2. 💡 우아한 해결책 및 구현 코드

  • 해결 방안: 사파리 호환을 위해 억지로 미디어 쿼리나 사파리 전용 CSS 핵(Hack)을 덕지덕지 추가하여 코드 부채를 늘리는 안티패턴을 배제하고, 웹 표준 스펙에 순수하게 귀의하는 “CSS Grid 및 display: contents 레이아웃” 방안을 도출했습니다. 테이블 행(tr) 태그 자체를 마치 돔 트리 상에 존재하지 않는 투명한 래퍼(가상화)처럼 처리해 주는 display: contents 기법을 적용함으로써 사파리 테이블 버그를 완전히 극복했습니다.
  • 코드 명세:
// calendar.tsx
classNames={{
  root: cn("w-full", defaultClassNames.root),
  // display: contents를 통해 tr의 왜곡을 막고 grid-cols-7의 균등 7열 정렬 확보
  month_grid: cn(
    "grid grid-cols-7 w-full [&_thead]:contents [&_thead>tr]:contents [&_tbody]:contents", 
    classNames?.month_grid
  ),
  weekdays: cn("", classNames?.weekdays),
  weekday: cn(
    "text-muted-foreground select-none text-[0.8rem] font-normal text-center py-1", 
    defaultClassNames.weekday, 
    classNames?.weekday
  ),
  weeks: cn("", classNames?.weeks),
  week: cn("contents", defaultClassNames.week, classNames?.week),
  day: cn("group/day relative select-none p-0 text-center", defaultClassNames.day, classNames?.day),
  ...
}}
  • 날짜 가시성 및 둥근 사각형 UI 변경:
className={cn(
  "relative flex size-[--cell-size] flex-col items-center justify-center gap-1 font-normal leading-tight transition-all",
  // 둥근 사각형(rounded-xl)을 채택하여 심미적 개선과 클릭 타겟 시각화 안정성 확보
  "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:rounded-xl",
  ...
)}

2-3. ✅ 결과 검증 및 모던 오픈소스 라이브러리 검토

  • 최종 검증:
    • iOS 17 사파리(아이폰 15 프로): 달력 주차가 6주차까지 풀 렌더링되더라도 하단의 통계 및 분석 카드 레이아웃과 1px의 오차도 없이 온전한 레이아웃 여백을 보존하며 깔끔하게 스크롤됨을 입증했습니다.
    • macOS 사파리 17: 날짜 숫자가 둥근 사각형 배경의 정확한 기하학적 정중앙에 수평/수직 밀착 정렬되어 단정하고 고급스러운 타이포그래피 정합성을 실현했습니다.
    • 클릭 면적 스케일링: 선택 영역을 rounded-xl로 변경하면서 모바일 지문 터치 면적이 약 25% 가량 여유롭게 넓어져 오작동이 눈에 띄게 감소했습니다.
  • 추천 오픈소스 라이브러리 검토:
    1. react-day-picker (v9):
      • 평가: 타입스크립트 기반의 강력한 날짜 선택 유틸리티 라이브러리로서, classNames 커스텀 오버라이딩을 극도로 정교하게 지원하여 크로스 브라우징 CSS 가상화(Contents API) 연동에 대단히 훌륭한 선택지입니다.
    2. display: contents (CSS 표준):
      • 평가: 조잡한 wrapper div나 마크업 트리 왜곡 없이도 하위 요소를 부모 Grid/Flex 컨테이너로 직통 바인딩해주는 모던 CSS의 매우 유용한 가상화 기술로 강력 추천합니다.