화면 크기마다 다르게 보여야 할 때 방법이 셋 있다.
안 A 둘 다 그려놓고 CSS 로 하나를 숨긴다
안 B 화면별 전용 컴포넌트를 따로 만든다
안 C 같은 것이 CSS 로 모양만 바뀐다
지금 셋이 다 쓰이고 있다. 그게 문제가 아니라 무엇을 언제 쓸지가 없는 것이 문제다.
먼저: 레이아웃 조각은 이미 정해졌다
컴포넌트 자리 표준 원칙 5가 레이아웃 조각을 닫았다 — 정적 export 라 서버가 화면 크기를 모르므로 layout 이 전부 그리고 CSS 가 가른다.
그리고 거기에 단서를 하나 달아뒀다.
레이아웃 조각은 몇 개 안 되므로 그 무게를 감수한다 — 대신 목록이 긴 것(후기 수십 건 같은)을 이렇게 양쪽 다 그리지는 않는다.
이 글은 그 「대신」의 경계를 정한다.
먼저: 같은 요금표를 두 벌 그리고 있다
이건 원칙이 아니라 이 표준을 만든 실측이다.
요금 페이지가 같은 데이터를 두 가지로 그린다.
데스크톱 app/fee/page.js 의 <div className="fee-table-wrap"> 표
모바일 app/fee/FeeVoucherMobile.js 카드
.fee-table-wrap { display: none }
@media (min-width: 768px) { .fee-table-wrap { display: block } } 데스크톱에서만 표
@media (min-width: 768px) { .fee-mobile { display: none } } 데스크톱에서 카드 숨김
한 줄씩 따라가면 이렇다. .fee-table-wrap { display: none }은 표를 감싼 요소를 기본값으로 화면에서 안 보이게 한다 — 그래서 아무 조건이 없으면 표는 항상 숨어 있다. @media (min-width: 768px) { ... }는 미디어 쿼리다. 화면 너비가 768px 이상일 때만 중괄호 안의 CSS를 적용하라는 뜻이다. 첫 번째 미디어 쿼리는 화면이 넓어지면 .fee-table-wrap의 display를 다시 block으로 바꿔 표를 보이게 한다 — 그래서 표는 데스크톱(넓은 화면)에서만 나타난다. 두 번째 미디어 쿼리는 반대로 .fee-mobile(카드)을 화면이 넓을 때 숨긴다. 결과적으로 좁은 화면에서는 표가 숨고 카드만 남고, 넓은 화면에서는 카드가 숨고 표만 남는다 — 표와 카드 둘 다 항상 그려져 있고, CSS가 둘 중 하나를 안 보이게 할 뿐이다.
요금 데이터는 52개 항목, 170줄이다. 표 쪽은 중첩 map 네 겹으로 그려진다.
voucher.groups.map(group =>
group.subs.map(sub =>
sub.rows.map(row => (
<tr>
{row.support.map(...)} {/* 가격 */}
{row.support.map(...)} {/* 지원금 */}
{row.selfPay.map(...)} {/* 본인부담 */}
</tr>
))))
한 줄씩 따라가면 이렇다. <tr>처럼 자바스크립트 코드 안에 HTML 비슷한 태그가 섞인 문법이 JSX다. map은 배열의 항목 하나하나를 다른 값으로 바꿔 새 배열을 만드는 자바스크립트 메서드인데, 여기서는 그 바뀐 값이 화면 조각(<tr>, <td> 같은 것)이다. 코드는 네 겹으로 겹쳐 있다 — 바깥쪽 voucher.groups.map이 그룹마다 한 번씩 돌고, 그 안의 group.subs.map이 그룹 안의 서브그룹마다 한 번씩 돌고, 또 그 안의 sub.rows.map이 서브그룹 안의 행마다 한 번씩 돌면서 <tr>(표의 한 행)을 하나씩 만든다. 그리고 그 <tr> 안에서 row.support.map 같은 호출이 세 번 더 돌면서 가격·지원금·본인부담 칸(<td>)을 채운다. 겹이 깊을수록 “그룹 수 × 서브그룹 수 × 행 수 × 칸 수”만큼 화면 조각이 늘어난다 — 그래서 아래 설명처럼 셀이 수백 개가 된다.
수백 개의 셀이 HTML 에 들어가고, 카드 쪽이 같은 데이터를 또 그린다. 그리고 어느 쪽이든 절반은 display:none 이다.
원칙 1. 크기가 아니라 「내용이 같은가」를 먼저 묻는다
무엇을 기준으로 방법을 고르는가.
안 A — 크기로 가른다. 작으면 둘 다 그리고 크면 하나만 그린다. 직관적이다. 버린 이유는 얼마가 큰지를 매번 판단해야 한다는 것이다. 링크 4개는 작고 셀 수백 개는 크지만, 그 사이 어딘가가 늘 나온다. 그리고 크기는 데이터가 늘면 바뀐다 — 오늘 작던 것이 내년에 크다.
골랐다 — 안 B, 「내용이 같은가」를 먼저 묻는다.
내용이 같다 → 마크업 하나. CSS 가 배치를 바꾼다
내용이 다르다 → 둘 다 그린다
안 A 가 못 지켜주는 것은 답이 데이터에 안 달려 있다는 것이다. 「이 두 화면이 같은 것을 보여주나」는 지금 보고 답할 수 있고, 데이터가 늘어도 안 바뀐다.
실제로 두 사례가 정확히 갈린다.
Hero 의 CTA 버튼 모바일 "서비스 알아보기" (고정)
데스크톱 heroSlides[index].ctaPrimary.label (슬라이드마다 다름)
→ 내용이 다르다
요금표 모바일 카드 · 데스크톱 표
→ 같은 요금을 다르게 배치한 것뿐이다
크기로 갈랐으면 둘 다 「크지 않으니 둘 다 그린다」로 갔을 것이다. 요금표는 크니까 걸렸겠지만, 그건 크기가 아니라 애초에 두 벌 그릴 이유가 없었다는 것이 진짜 이유다.
원칙 2. 내용이 같으면 마크업 하나로 두고 CSS 가 배치를 바꾼다
같은 것을 두 벌 그리지 않는다. 하나만 그리고 CSS 가 화면마다 다르게 배치한다.
/* 좁을 때는 세로로 쌓고 */
.item { display: block }
/* 넓을 때는 가로로 편다 */
@media (min-width: 768px) {
.item { display: table-row }
}
두 벌을 안 그려서 얻는 것이 셋이다.
HTML 이 절반이 된다
고칠 곳이 하나다 — 지금은 요금이 바뀌면 표와 카드를 둘 다 고쳐야 한다
어긋날 수가 없다 — 한쪽만 고치면 두 화면이 다른 값을 보여준다
둘째와 셋째가 크기보다 중요하다. 요금표는 데이터가 site-data.js 한 곳에 있어 아직 어긋나지 않았지만, 그리는 코드가 둘이면 한쪽에만 새 열을 더하는 일이 언젠가 생긴다.
다만 구조가 바뀌는 것은 예외로 인정한다. 표를 카드로 바꾸는 CSS 는 <table> 의 행·열 관계를 흐트러뜨려 헤더와 셀의 연결(접근성)이 끊길 수 있다. 그때는 두 벌을 그리되 그 이유를 파일에 적는다 — 만들 것인가 가져다 쓸 것인가 표준이 요구하는 것과 같은 기록이다.
이 예외가 실제로 필요한지는 확인하지 못했다. 요금표를 마크업 하나로 만들 수 있는지는 실제로 짜봐야 안다.
원칙 3. 내용이 다르면 둘 다 그리되, 그때만 그런다
Hero 의 CTA 는 모바일과 데스크톱의 문구가 다르다.
<Link className="mobile-only-cta">서비스 알아보기</Link>
<Link className="desktop-only-cta">{heroSlides[index].ctaPrimary.label}</Link>
서버가 화면 크기를 모르므로 어느 쪽을 그릴지 빌드 때 못 정한다. 그래서 둘 다 그리고 CSS 가 하나를 숨긴다.
이건 어쩔 수 없는 경우이지 편해서 고르는 방법이 아니다. 그래서 조건을 붙인다.
- 정말 내용이 다른지 다시 본다. 문구만 다르면 데이터로 뺄 수 있는지 먼저 확인한다.
- JS 로 화면을 재서 하나만 그리지 않는다. 정적 export 라 첫 HTML 에 답이 없어 깜빡이고, JS 가 꺼지면 아무것도 안 뜬다.
- 화면에 안 보이는 쪽도 화면 읽기 도구는 읽는다.
display:none은 읽기 도구에서도 빠지지만,visibility나opacity로 숨기면 양쪽이 다 읽힌다. 숨길 때는display:none을 쓴다.
마지막이 놓치기 쉽다. 눈으로는 하나만 보이는데 화면 읽기 도구는 둘 다 읽는 상태가 되면, 그걸 알아채는 사람이 없다.
판단 기준 정리
| 질문 | 답 | 결론 |
|---|---|---|
| 무엇으로 가르나 | 내용이 같은가 | 크기는 데이터가 늘면 바뀌지만 이건 안 바뀐다 |
| 같은 것을 다르게 배치한다 | 마크업 하나 | CSS 가 배치를 바꾼다 |
| 표를 카드로 바꿔야 한다 | 예외로 인정한다 | 헤더-셀 연결이 끊길 수 있다. 이유를 파일에 적는다 |
| 문구나 내용이 실제로 다르다 | 둘 다 그린다 | 서버가 화면을 모르니 빌드 때 못 정한다 |
| 그때 숨기는 방법 | display:none |
visibility·opacity 는 읽기 도구가 둘 다 읽는다 |
| JS 로 화면을 재면 | 안 된다 | 깜빡이고 JS 가 꺼지면 아무것도 안 뜬다 |
| 레이아웃 조각은 | 이미 정해졌다 | 전부 그리고 CSS 가 가른다 |
이 표준을 정하기까지
남은 빈틈이 「방식이 셋 섞여 있다」였다. 둘 다 그리고 숨기기, 전용 컴포넌트, 모양만 바꾸기. 처음엔 셋 중 하나를 고르는 문제로 봤다.
그런데 실제 코드를 보니 셋이 다 필요한 자리가 있었다. Hero 는 문구가 달라서 둘 다 그려야 하고, .hero-slider 는 비율만 바뀌면 되고, 요금표는… 왜 두 벌인지 이유를 찾을 수 없었다.
요금표를 재보고 방향이 정해졌다. 52개 항목 170줄짜리 데이터를 표로 한 번, 카드로 한 번 그린다. 표 쪽은 중첩 map 네 겹이라 셀이 수백 개다. 그런데 둘이 보여주는 내용은 같다 — 같은 요금을 배치만 다르게 한 것이다.
그래서 기준이 「크기」에서 「내용이 같은가」로 옮겨갔다. 크기로 가르면 얼마가 큰지를 매번 판단해야 하고, 오늘 작던 것이 데이터가 늘면 커진다. 반면 「이 두 화면이 같은 것을 보여주나」는 지금 답할 수 있고 데이터가 늘어도 답이 안 바뀐다.
그리고 두 벌의 진짜 비용이 크기가 아니라는 게 보였다. HTML 이 두 배가 되는 것보다 고칠 곳이 둘이라는 것이 크다. 요금이 바뀌면 표와 카드를 둘 다 고쳐야 하고, 한쪽만 고치면 두 화면이 다른 값을 보여준다. 지금은 데이터가 site-data.js 한 곳이라 안 어긋났지만, 그리는 코드가 둘이면 한쪽에만 새 열을 더하는 일이 언젠가 생긴다.
마지막으로 숨기는 방법에 함정이 하나 있었다. display:none 은 화면 읽기 도구에서도 빠지지만 visibility 나 opacity 로 숨기면 양쪽이 다 읽힌다. 눈으로는 하나만 보이는데 읽기 도구는 둘 다 읽는 상태가 되고, 그걸 알아채는 사람이 없다. 앞 표준에서 focus trap 을 빠뜨린 것과 같은 종류라 규칙에 박았다.
정리
- 크기가 아니라 「내용이 같은가」로 먼저 가른다. 크기는 데이터가 늘면 바뀌지만 이건 안 바뀐다.
- 내용이 같으면 마크업 하나로 두고 CSS 가 배치를 바꾼다. 두 벌의 진짜 비용은 HTML 크기가 아니라 고칠 곳이 둘인 것이다.
- 표를 카드로 바꾸는 것처럼 구조가 바뀌면 예외로 인정하되 이유를 파일에 적는다. 헤더와 셀의 연결이 끊길 수 있다.
- 내용이 실제로 다를 때만 둘 다 그린다. 어쩔 수 없는 경우이지 편해서 고르는 방법이 아니다.
- 숨길 때는
display:none을 쓴다.visibility·opacity는 화면 읽기 도구가 둘 다 읽는다. - JS 로 화면 크기를 재어 하나만 그리지 않는다. 정적 export 라 깜빡이고 JS 가 꺼지면 아무것도 안 뜬다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기