화면 크기마다 다르게 보여야 할 때 방법이 셋 있다.

안 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 은 읽기 도구에서도 빠지지만, visibilityopacity 로 숨기면 양쪽이 다 읽힌다. 숨길 때는 display:none 을 쓴다.

마지막이 놓치기 쉽다. 눈으로는 하나만 보이는데 화면 읽기 도구는 둘 다 읽는 상태가 되면, 그걸 알아채는 사람이 없다.


판단 기준 정리

질문 결론
무엇으로 가르나 내용이 같은가 크기는 데이터가 늘면 바뀌지만 이건 안 바뀐다
같은 것을 다르게 배치한다 마크업 하나 CSS 가 배치를 바꾼다
표를 카드로 바꿔야 한다 예외로 인정한다 헤더-셀 연결이 끊길 수 있다. 이유를 파일에 적는다
문구나 내용이 실제로 다르다 둘 다 그린다 서버가 화면을 모르니 빌드 때 못 정한다
그때 숨기는 방법 display:none visibility·opacity 는 읽기 도구가 둘 다 읽는다
JS 로 화면을 재면 안 된다 깜빡이고 JS 가 꺼지면 아무것도 안 뜬다
레이아웃 조각은 이미 정해졌다 전부 그리고 CSS 가 가른다

이 표준을 정하기까지

남은 빈틈이 「방식이 셋 섞여 있다」였다. 둘 다 그리고 숨기기, 전용 컴포넌트, 모양만 바꾸기. 처음엔 셋 중 하나를 고르는 문제로 봤다.

그런데 실제 코드를 보니 셋이 다 필요한 자리가 있었다. Hero 는 문구가 달라서 둘 다 그려야 하고, .hero-slider 는 비율만 바뀌면 되고, 요금표는… 왜 두 벌인지 이유를 찾을 수 없었다.

요금표를 재보고 방향이 정해졌다. 52개 항목 170줄짜리 데이터를 표로 한 번, 카드로 한 번 그린다. 표 쪽은 중첩 map 네 겹이라 셀이 수백 개다. 그런데 둘이 보여주는 내용은 같다 — 같은 요금을 배치만 다르게 한 것이다.

그래서 기준이 「크기」에서 「내용이 같은가」로 옮겨갔다. 크기로 가르면 얼마가 큰지를 매번 판단해야 하고, 오늘 작던 것이 데이터가 늘면 커진다. 반면 「이 두 화면이 같은 것을 보여주나」는 지금 답할 수 있고 데이터가 늘어도 답이 안 바뀐다.

그리고 두 벌의 진짜 비용이 크기가 아니라는 게 보였다. HTML 이 두 배가 되는 것보다 고칠 곳이 둘이라는 것이 크다. 요금이 바뀌면 표와 카드를 둘 다 고쳐야 하고, 한쪽만 고치면 두 화면이 다른 값을 보여준다. 지금은 데이터가 site-data.js 한 곳이라 안 어긋났지만, 그리는 코드가 둘이면 한쪽에만 새 열을 더하는 일이 언젠가 생긴다.

마지막으로 숨기는 방법에 함정이 하나 있었다. display:none 은 화면 읽기 도구에서도 빠지지만 visibilityopacity 로 숨기면 양쪽이 다 읽힌다. 눈으로는 하나만 보이는데 읽기 도구는 둘 다 읽는 상태가 되고, 그걸 알아채는 사람이 없다. 앞 표준에서 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 가 꺼지면 아무것도 안 뜬다.**

자신만의 철학을 만들어가는 중입니다.
최상단으로 이동했습니다!
확대 이미지

댓글남기기