모바일에서 넓은 테이블 고정하기: 레이아웃 이동 없이 가로 스크롤 추가 방법
요약: 뷰포트보다 넓은 테이블이 있을 때, 페이지가 측면으로 밀리는 현상을 해결했습니다. 이 수정 사항은 overflow-x:auto를 가진 반응형 래퍼(wrapper)와 CSS만 사용한 스크롤바를 추가하여, 데스크톱 동작을 유지하면서 레이아웃 안정성을 확보합니다.
문제점
저희 SaaS 대시보드는 데이터가 많은 테이블(예: 계약 목록, 월별 보고서)을 렌더링합니다. 최근 iPhone 13으로 QA를 진행했을 때, 테이블이 화면 너비를 초과할 때마다 페이지가 수평으로 스크롤되는 문제가 발생했습니다:
LayoutError: viewport overflow – element .table-wrapper width 1240px > 375px (mobile)
사용자 경험(UX) 측면에서 이 문제는 심각했습니다. 사용자는 전체 페이지를 스와이프해야 했고, 고정된 헤더는 화면에서 사라졌습니다. 이 문제는 PR #24에서 새로운 컬럼(tenantId)이 추가되어 테이블 너비가 두 배로 늘어나면서 발생했습니다.
처음 시도했던 방법들
제 첫 번째 생각은 table-layout: fixed와 width: 100%를 사용하여 테이블을 강제로 축소하는 것이었습니다. 저는 다음 CSS를 Table.jsx에 직접 추가했습니다:
.table {
table-layout: fixed;
width: 100%;
...
데스크톱에서는 괜찮아 보였지만, 모바일에서는 컬럼들이 읽을 수 없는 타원형으로 축소되었습니다. 실제 데이터(ID, 금액)가 잘려나가 테이블을 쓸모없게 만들었습니다. 또한, 저는 테이블을 overflow-x: scroll이 적용된 `<div class=
- 테이블의 자연 너비가 뷰포트를 초과하는지 감지합니다.
- 화면 크기가 ≤ 768 px인 경우에만 수평 스크롤을 적용합니다.
- 데스크톱에서는 원본 테이블을 그대로 유지하여 레이아웃 오류를 방지합니다.
Wrapper는 src/components/ResponsiveTable.jsx에 위치하며, 렌더링 후 ref를 사용하여 테이블의 스크롤 너비를 측정하고 그에 따라 CSS 클래스를 토글합니다.
2. 새 파일
| 파일 | 목적 |
|---|
src/components/ResponsiveTable.jsx | 리사이즈 옵저버(ResizeObserver)를 사용한 Wrapper 컴포넌트 |
| ... | |
3. 코드 변경 사항
src/components/ResponsiveTable.jsx
import React, { useEffect, useRef, useState } from 'react';
import Table from './Table';
import './responsive-table.css';
...
주요 사항:
- ResizeObserver – 방향 변경이나 사용자가 개발자 도구를 토글할 때도 검사가 실행되도록 보장합니다.
needsScroll 상태 – 필요한 경우에만 scrollable 클래스를 토글하여 데스크톱에서 불필요한 스크롤바가 생기는 것을 방지합니다.
ref 포워딩(forwarding) – Table.jsx는 이제 forwardRef 컴포넌트를 내보냅니다.
Table.jsx 업데이트 (몇 줄만)
import React, { forwardRef } from 'react';
const Table = forwardRef<HTMLTableElement, TableProps>(({ data, columns }, ref) => (
...
시각적인 변경은 없으며, 측정만을 위해 내부 <table> 태그를 노출할 뿐입니다.
src/styles/responsive-table.css
.responsive-table-wrapper {
width: 100%;
/* 모바일에서 수직 오버플로우 방지 */
...
4. 통합(Integration)
이전에 Table을 직접 가져왔던 페이지들(예: src/pages/Contracts.jsx)에서는 import 구문을 변경했습니다:
// 이전 코드
import Table from '../components/Table';
...
그리고 JSX를 다음과 같이 업데이트했습니다:
<ResponsiveTable data={contracts} columns={contractColumns} />
다른 모든 props는 변경되지 않았기 때문에 추가적인 리팩토링은 필요하지 않았습니다.
5. 수정 사항 테스트(Testing the Fix)
저는 Cypress 테스트(cypress/integration/table_responsive.spec.js)를 추가하여 뷰포트 너비를 375px로 강제하고, 테이블이 8개 이상의 열을 포함할 경우 래퍼에 scrollable 클래스가 있는지 확인합니다.
describe('Responsive Table', () => {
it('adds horizontal scroll on mobile', () => {
cy.viewport(375, 667);
...
모든 CI 파이프라인이 이제 통과하고, iOS Safari에서의 수동 테스트를 통해 페이지가 더 이상 옆으로 이동하지 않음을 확인했습니다.
핵심 요약 (Key Takeaway)
반응형 레이아웃에서 넓은 테이블을 다룰 때는, 정적인 CSS 브레이크포인트에 의존하기보다는 **측정(measures)**하여 테이블의 고유 너비를 감지하는 전용 래퍼에 스크롤링 로직을 격리해야 합니다. 이 패턴은 데스크톱 경험을 건드리지 않으면서 정말 필요할 때만 스크롤을 추가하므로, 레이아웃 이동을 방지하고 접근성을 유지합니다.
다음 계획 (What's Next)
- 열 데이터 지연 로딩(Lazy‑load column data) – 50개 이상의 열을 가진 테이블의 경우, 초기 페이로드를 줄이기 위해 열 정의를 온디맨드(on demand)로 가져옵니다.
- 키보드 탐색(Keyboard navigation) – 스크롤 가능한 래퍼가 포커스 가능하도록(`tabindex=