화면 크기마다 다르게 보여야 할 때 방법이 셋 있다.
안 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 } } 데스크톱에서 카드 숨김
요금 데이터는 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>
))))
수백 개의 셀이 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 가 꺼지면 아무것도 안 뜬다.
AI 코드 어시스턴트에 바로 적용하기
Claude Code · Codex — .claude/skills/screen-variant-standard/SKILL.md · .agents/skills/screen-variant-standard/SKILL.md
두 파일은 바이트까지 같아야 한다. .agents 쪽은 tools/agents-skills.rb 가 뽑는 생성물이다.
---
name: screen-variant-standard
description: 화면 크기마다 다르게 보여야 할 때 마크업을 하나로 둘지 둘로 나눌지 가르는 기준. 내용이 같은지로 판정하는 법, 숨기는 방법, JS 로 화면을 재면 안 되는 이유를 담는다. 반응형 화면을 만들 때, 모바일 전용 컴포넌트를 만들려 할 때, 같은 데이터를 다르게 배치할 때 적용한다.
---
# 화면별 대응 표준
**크기가 아니라 「내용이 같은가」로 먼저 가른다.**
크기는 데이터가 늘면 바뀌지만, 「이 두 화면이 같은 것을 보여주나」는 지금 답할 수 있고 나중에도 안 바뀐다.
## 판정
| | 어떻게 |
|---|---|
| **같은 것을 다르게 배치한다** | **마크업 하나.** CSS 가 배치를 바꾼다 |
| **문구나 내용이 실제로 다르다** | 둘 다 그리고 `display:none` 으로 하나를 숨긴다 |
- **두 벌의 진짜 비용은 HTML 크기가 아니라 고칠 곳이 둘인 것이다.** 한쪽만 고치면 두 화면이 다른 값을 보여준다.
- 「내용이 다르다」로 가기 전에 **문구만 다른 것은 아닌지** 다시 본다. 데이터로 뺄 수 있으면 마크업은 하나다.
- **구조가 바뀌는 것은 예외다.** 표를 카드로 바꾸는 CSS 는 `<table>` 의 헤더-셀 연결을 끊을 수 있다. 두 벌로 가되 **그 이유를 파일에 적는다.**
## 숨길 때는 `display:none`
```css
.mobile-only { display: none }
@media (max-width: 767px) { .mobile-only { display: block } }
```
- **`visibility` 나 `opacity` 로 숨기지 않는다.** 눈에는 안 보이지만 **화면 읽기 도구는 둘 다 읽는다.**
- 눈으로 확인하면 멀쩡해 보여서 **알아채는 사람이 없다.**
## JS 로 화면을 재지 않는다
```javascript
// 이렇게 하지 않는다
const isMobile = window.innerWidth < 768;
return isMobile ? <A /> : <B />;
```
- 정적 export 면 **첫 HTML 에 답이 없다.** 브라우저가 JS 를 돌린 뒤에야 정해져서 **첫 화면이 깜빡인다.**
- **JS 가 꺼진 환경에서는 아무것도 안 뜬다.**
- 서버가 화면 크기를 모른다는 것이 전제다. 그 전제가 바뀌지 않는 한 CSS 로 가른다.
## 하지 않는 것
- **같은 내용을 두 벌 그리지 않는다.** 고칠 곳이 둘이 되고 한쪽만 고치면 어긋난다.
- **크기를 기준으로 방법을 고르지 않는다.** 오늘 작던 것이 데이터가 늘면 커진다.
- **`visibility`·`opacity` 로 화면별 요소를 숨기지 않는다.**
- **JS 로 화면 크기를 재어 조건부로 그리지 않는다.**
- **구조가 바뀌어 두 벌로 갈 때 이유를 안 적지 않는다.**
GitHub Copilot — .github/instructions/screen-variant-standard.instructions.md
---
description: 화면 크기마다 다르게 보여야 할 때 마크업을 하나로 둘지 둘로 나눌지
applyTo: "**"
---
# 화면별 대응
- **크기가 아니라 「내용이 같은가」로 먼저 가른다.** 크기는 데이터가 늘면 바뀌지만 「이 두 화면이 같은 것을 보여주나」는 지금 답할 수 있고 나중에도 안 바뀐다.
- **같은 것을 다르게 배치하는 것이면 마크업 하나로 두고 CSS 가 배치를 바꾼다.** 같은 데이터를 표와 카드로 두 벌 그리지 않는다.
- **두 벌의 진짜 비용은 HTML 크기가 아니라 고칠 곳이 둘인 것이다.** 한쪽만 고치면 두 화면이 다른 값을 보여주고, 그리는 코드가 둘이면 언젠가 한쪽에만 새 열이 붙는다.
- 「내용이 다르다」로 가기 전에 **문구만 다른 것은 아닌지** 다시 본다. 데이터로 뺄 수 있으면 마크업은 하나다.
- **구조가 바뀌는 것은 예외로 인정한다.** 표를 카드로 바꾸는 CSS 는 `<table>` 의 헤더-셀 연결을 끊을 수 있다. 두 벌로 가되 **그 이유를 파일에 적는다.**
- **숨길 때는 `display:none` 을 쓴다.** `visibility` 나 `opacity` 로 숨기면 눈에는 안 보이지만 **화면 읽기 도구는 둘 다 읽는다** — 눈으로 확인하면 멀쩡해 보여서 알아채는 사람이 없다.
- **JS 로 화면 크기를 재어 조건부로 그리지 않는다.** 정적 export 면 첫 HTML 에 답이 없어 **깜빡이고 JS 가 꺼지면 아무것도 안 뜬다.**
자신만의 철학을 만들어가는 중입니다.
댓글남기기