디자인 시스템에 Dialog 가 있는데 CallConfirmationDialog 를 직접 만들었다. Badge 가 있는데 InquiryStatusBadge 를 직접 만들었다.
안 A 있는 건 무조건 가져다 쓴다
안 B 필요하면 직접 만든다. 기준은 그때그때 판단한다
둘 다 답이 아니다. 하나는 10줄짜리 뱃지에도 4,000줄 번들을 끌어오게 하고, 다른 하나는 판단을 매번 새로 하게 만든다. 그리고 매번 새로 하다가 이번에 하나를 빠뜨렸다.
먼저: 50개 중 3개만 쓰고 있다
이건 원칙이 아니라 이 표준을 만든 실측이다.
design-system 이 주는 것 50 개
실제로 import 하는 것 3 개
Button 13 회
Icon 10 회
IconButton 1 회
────────────────────
나머지 47개 0 회
Dialog · Badge · Card · Carousel · TextField · DatePicker 가 전부 안 쓰인다. 대신 비슷한 것을 직접 만들었다.
CallConfirmationDialog · InquiryPasswordDialog ← Dialog 가 있다
InquiryStatusBadge ← Badge 가 있다
ReviewCard ← Card 가 있다
ReviewImageSlider ← Carousel 이 있다
왜 그랬는지 어디에도 안 적혀 있다. 몰랐는지, 안 맞았는지, 판단이 있었는지 알 수 없다.
먼저: 그러다 하나를 빠뜨렸다
문의 비밀번호를 묻는 팝업을 직접 만들면서 이것들을 손으로 처리했다.
ESC 로 닫기 ✓
뒤 페이지 스크롤 잠그기 ✓
열리면 입력칸에 커서 두기 ✓
role="dialog" · aria-modal 붙이기 ✓
정리(cleanup) ✓
────────────────────────────────
Tab 이 팝업 밖으로 못 나가게 가두기 ✗
aria-modal="true" 는 화면 읽기 도구에만 「이 밖은 없는 셈 쳐라」라고 알린다. 키보드 Tab 이동은 안 막는다 — 그건 JS 로 가둬야 한다.
그래서 팝업이 떠 있는데 Tab 을 누르면 뒤 페이지의 링크로 커서가 옮겨간다. 키보드로만 쓰는 사람은 팝업에서 빠져나와 길을 잃는다.
마우스로 쓰면 아무 문제가 없다. 화면도 멀쩡하다. 그래서 빠뜨렸는지도 모른다.
그리고 같은 코드가 두 파일에 복제돼 있다. ESC · 첫 포커스 · cleanup 이 CallConfirmationDialog(48줄)와 InquiryPasswordDialog(76줄)에 각각 있고, 비밀번호 팝업이 call-modal 클래스를 빌려 쓴다 — 이름이 안 맞는데 스타일이 필요해서다.
원칙 1. 「직접 만들면 무엇을 다시 만들어야 하나」로 가른다
무엇을 기준으로 고르는가.
안 A — 디자인 시스템에 있으면 무조건 쓴다. 판단이 필요 없다. 버린 이유는 10줄짜리에도 그게 적용된다는 것이다. InquiryStatusBadge 는 아이콘 하나에 글자 하나가 전부다.
<span className={...}>
{answered ? <CheckIcon /> : <ClockIcon />}
{status}
</span>
여기엔 빠뜨릴 것이 없다. 키보드로 누를 것도 포커스가 갈 곳도 없다. 4,429줄 번들에서 Badge 를 끌어오는 것이 오히려 비싸다.
안 B — 필요할 때 직접 만든다. 기준은 그때 판단한다. 지금 방식이다. 버린 이유는 판단이 매번 새로 이뤄진다는 것이고, 실제로 그러다 focus trap 을 빠뜨렸다.
골랐다 — 안 C, 「직접 만들면 무엇을 다시 만들어야 하는가」로 묻는다.
다시 만들 게 없다 → 직접 만든다 뱃지 · 카드 · 구분선처럼 글자와 색만 있는 것
다시 만들 게 있다 → 가져다 쓴다 팝업 · 선택 상자 · 날짜 고르기처럼 키보드와 포커스가 얽힌 것
안 A 와 안 B 가 못 지켜주는 것은 판단이 매번 같은 답을 내는 것이다. 「이거 복잡한가」는 사람마다 다르지만 「Tab 을 가둬야 하나」는 예/아니오로 답한다.
원칙 2. 「다시 만들 것」의 목록을 못 박는다
원칙 1이 서려면 「다시 만들 것」이 무엇인지 알아야 한다. 그런데 그것들은 눈에 안 보여서 그 자리에서 생각이 안 난다.
ESC 로 닫기
Tab 을 그 안에 가두기 (focus trap)
열릴 때 어디에 커서를 둘지
닫힐 때 원래 자리로 커서 되돌리기
바깥을 눌렀을 때 닫기
화면 읽기 도구가 읽을 역할과 이름
목록에서 위아래 키로 이동하기
열려 있는 동안 뒤 페이지 스크롤 잠그기
이 중 하나라도 해당하면 가져다 쓴다.
목록이 있으면 「이 중에 해당하는 게 있나」로 물을 수 있다. 없으면 매번 처음부터 떠올려야 하고, 그러다 이번처럼 하나가 빠진다. 빠뜨려도 화면은 멀쩡해서 아무도 안 알려준다.
대가는 목록이 낡을 수 있다는 것이다. 새로운 접근성 요구가 생기면 여기에 더해야 한다. 그래도 목록이 없는 것보다는 낫다 — 없으면 아예 안 떠오른다.
원칙 3. 직접 만들었으면 왜 그랬는지 그 파일에 적는다
원칙 1로 골랐어도 다음 사람은 그 판단을 못 본다.
지금 CallConfirmationDialog 에는 왜 직접 만들었는지가 한 줄도 없다. 그래서 다음에 비슷한 팝업을 만들 때 같은 고민을 처음부터 한다.
반대로 admin 의 AdminSelect 에는 적혀 있다.
기본
select를 쓰지 않는 이유는 하나입니다 — 펼쳐진 목록은 CSS 로 바꿀 수 없습니다. 그 목록은 브라우저가 페이지 밖에서 OS 위젯으로 그리고,option에 준 색·모서리·글꼴은 대부분 무시됩니다. … 대신 기본select가 공짜로 주던 것들을 여기서 다시 만들어야 합니다 — 키보드 이동, 바깥을 눌러 닫기, 화면 읽기 도구가 읽을 역할 이름입니다. 그 셋이 이 파일 분량의 대부분입니다.
이 문장이 이 표준의 기준을 준 것이다. 같은 판단을 이미 했는데 한 파일의 주석으로만 남아 규칙이 되지 못했다.
적을 것은 둘이다.
왜 가져다 쓰지 않았나 예: 펼친 목록을 CSS 로 못 바꾼다
무엇을 다시 만들어야 했나 예: 키보드 이동 · 바깥 클릭 · 역할 이름
둘째가 특히 중요하다. 나중에 그 컴포넌트를 고칠 때 무엇을 안 깨뜨려야 하는지가 거기 적혀 있다.
판단 기준 정리
| 질문 | 답 | 결론 |
|---|---|---|
| 무엇으로 가르나 | 직접 만들면 무엇을 다시 만들어야 하나 | 「복잡한가」는 사람마다 다르지만 「Tab 을 가둬야 하나」는 예/아니오다 |
| 글자와 색만 있다 | 직접 만든다 | 10줄짜리 뱃지에 4,000줄 번들을 끌어오지 않는다 |
| 키보드·포커스가 얽힌다 | 가져다 쓴다 | 빠뜨려도 화면은 멀쩡해서 아무도 안 알려준다 |
| 목록에 없는 새 요구가 있다 | 목록에 더한다 | 낡는 것보다 없는 게 나쁘다 |
| 직접 만들기로 했다 | 그 파일에 이유를 적는다 | 왜 안 썼는지 + 무엇을 다시 만들었는지 |
| 이유를 안 적으면 | 다음 사람이 같은 고민을 처음부터 한다 | 실제로 CallConfirmationDialog 가 그 상태다 |
이 표준을 정하기까지
세어 보니 50개 중 3개만 쓰고 있었다. Button · Icon · IconButton 이 전부고 나머지 47개는 한 번도 안 쓴다. 그런데 직접 만든 것들 중에 Dialog · Badge · Card · Carousel 과 겹치는 것이 여럿이었다. 있는데 안 쓰고 만든 것이다.
왜 그랬는지 찾으러 파일을 열었는데 이유가 없었다. 이 프로젝트는 주석에 근거를 잘 적는 편인데, CallConfirmationDialog 에도 InquiryStatusBadge 에도 왜 직접 만들었는지가 한 줄도 없었다. 몰랐는지 판단이 있었는지 알 수가 없었다.
대신 두 파일의 성격이 정반대라는 게 보였다. InquiryStatusBadge 는 10줄이고 아이콘 하나에 글자 하나가 전부다. CallConfirmationDialog 는 포커스와 스크롤 잠금과 ESC 를 손으로 짰다. 하나는 직접 만드는 게 맞고 하나는 아니었다.
그래서 무엇이 빠졌나 보다가 focus trap 을 찾았다. ESC 도 있고 첫 포커스도 있고 aria-modal 도 있는데 Tab 을 가두는 것만 없었다. aria-modal 은 화면 읽기 도구에만 알리지 키보드를 막지 않는다. 마우스로 쓰면 아무 문제가 없어서 빠뜨렸는지도 모르는 상태였다.
기준은 이미 코드에 적혀 있었다. admin 의 AdminSelect 주석이 「기본 select 가 공짜로 주던 것들을 여기서 다시 만들어야 합니다」라고 적고 그 목록까지 나열해 뒀다. 같은 판단을 이미 한 것이고, 한 파일의 주석으로만 남아 규칙이 되지 못했을 뿐이다. 이 저장소의 표준이 대부분 그렇게 나온다 — 이미 하고 있는데 말로 꺼내본 적이 없는 것.
목록을 못 박은 것은 「눈에 안 보인다」가 이유다. 「복잡한 컴포넌트는 가져다 쓴다」로 적으면 복잡의 기준에서 또 갈린다. 그런데 「Tab 을 가둬야 하나」 · 「닫을 때 커서를 되돌려야 하나」는 예/아니오로 답한다. 목록이 판단을 검문으로 바꾼다.
정리
- 「직접 만들면 무엇을 다시 만들어야 하나」로 가른다. 「복잡한가」는 사람마다 다르지만 이건 예/아니오다.
- 글자와 색만 있으면 직접 만든다. 10줄짜리 뱃지에 4,000줄 번들을 끌어오지 않는다.
- 키보드·포커스가 얽히면 가져다 쓴다. 빠뜨려도 화면은 멀쩡해서 아무도 안 알려준다.
- 「다시 만들 것」 목록을 못 박는다. 눈에 안 보이는 것들이라 그 자리에서는 생각이 안 난다.
- 직접 만들었으면 왜 그랬는지와 무엇을 다시 만들었는지를 그 파일에 적는다. 안 적으면 다음 사람이 같은 고민을 처음부터 한다.
AI 코드 어시스턴트에 바로 적용하기
Claude Code · Codex — .claude/skills/component-build-or-borrow-standard/SKILL.md · .agents/skills/component-build-or-borrow-standard/SKILL.md
두 파일은 바이트까지 같아야 한다. .agents 쪽은 tools/agents-skills.rb 가 뽑는 생성물이다.
---
name: component-build-or-borrow-standard
description: UI 컴포넌트를 디자인 시스템·라이브러리에서 가져다 쓸지 직접 만들지 가르는 기준. 다시 만들어야 하는 것의 목록, 직접 만들 때 파일에 적을 것, 기본 HTML 요소를 대체할 때의 판단을 담는다. 팝업·선택 상자·날짜 고르기처럼 키보드와 포커스가 얽힌 것을 만들 때, 있는 컴포넌트를 안 쓰기로 할 때 적용한다.
---
# 만들 것인가 가져다 쓸 것인가
**「직접 만들면 무엇을 다시 만들어야 하나」로 가른다.**
「복잡한가」는 사람마다 답이 다르지만, 아래 목록에 해당하는지는 예/아니오로 답한다.
## 다시 만들 것 목록
하나라도 해당하면 **가져다 쓴다.**
```
ESC 로 닫기
Tab 을 그 안에 가두기 (focus trap)
열릴 때 어디에 커서를 둘지
닫힐 때 원래 자리로 커서 되돌리기
바깥을 눌렀을 때 닫기
화면 읽기 도구가 읽을 역할과 이름
목록에서 위아래 키로 이동하기
열려 있는 동안 뒤 페이지 스크롤 잠그기
```
- **해당하는 게 없으면 직접 만든다.** 글자와 색만 있는 것(뱃지·카드·구분선)에 큰 번들을 끌어오지 않는다.
- **목록에 없는 새 요구가 생기면 목록에 더한다.** 낡는 것보다 없는 것이 나쁘다.
- 이것들은 **눈에 안 보인다.** 빠뜨려도 화면은 멀쩡하고 마우스로 쓰면 문제가 없다. 그래서 목록이 필요하다.
## `aria-modal` 은 키보드를 막지 않는다
```jsx
<div role="dialog" aria-modal="true"> {/* 화면 읽기 도구에만 알린다 */}
```
**Tab 이동은 그대로 밖으로 나간다.** 가두려면 JS 로 포커스 순환을 직접 짜야 한다. `aria-modal` 을 붙였다고 focus trap 이 된 것이 아니다.
## 직접 만들기로 했으면 파일에 적는다
```javascript
/**
* 기본 select 를 쓰지 않는 이유는 하나입니다 — 펼쳐진 목록은 CSS 로 바꿀 수 없습니다.
*
* 대신 기본 select 가 공짜로 주던 것들을 여기서 다시 만들어야 합니다 —
* 키보드 이동, 바깥을 눌러 닫기, 화면 읽기 도구가 읽을 역할 이름입니다.
*/
```
- **왜 가져다 쓰지 않았나**를 적는다. 안 적으면 다음 사람이 같은 고민을 처음부터 한다.
- **무엇을 다시 만들었나**를 적는다. 나중에 그 파일을 고칠 때 **무엇을 안 깨뜨려야 하는지**가 거기 있다.
## 하지 않는 것
- **있는 컴포넌트를 이유 없이 안 쓰지 않는다.** 안 쓰기로 했으면 그 이유를 파일에 남긴다.
- **글자와 색만 있는 것에 큰 번들을 끌어오지 않는다.**
- **팝업·선택 상자·날짜 고르기를 이유 없이 직접 만들지 않는다.**
- **`aria-modal` 만 붙이고 focus trap 을 만들었다고 여기지 않는다.**
- 같은 접근성 코드를 **여러 파일에 복제하지 않는다.** 두 번째가 나오면 가져다 쓸 때다.
GitHub Copilot — .github/instructions/component-build-or-borrow-standard.instructions.md
---
description: UI 컴포넌트를 가져다 쓸지 직접 만들지 가르는 기준
applyTo: "**"
---
# 만들 것인가 가져다 쓸 것인가
- **「직접 만들면 무엇을 다시 만들어야 하나」로 가른다.** 「복잡한가」는 사람마다 답이 다르지만 아래 목록에 해당하는지는 예/아니오로 답한다.
- **다시 만들 것 목록** — `ESC 로 닫기` · `Tab 을 그 안에 가두기(focus trap)` · `열릴 때 커서 둘 곳` · `닫힐 때 커서 되돌리기` · `바깥 클릭으로 닫기` · `화면 읽기 도구가 읽을 역할과 이름` · `목록에서 위아래 키 이동` · `열려 있는 동안 뒤 페이지 스크롤 잠그기`. **하나라도 해당하면 가져다 쓴다.**
- **해당하는 게 없으면 직접 만든다.** 글자와 색만 있는 것(뱃지·카드·구분선)에 큰 번들을 끌어오지 않는다.
- 이 항목들은 **눈에 안 보인다.** 빠뜨려도 화면은 멀쩡하고 마우스로 쓰면 문제가 없다 — 그래서 목록으로 못 박는다. 목록에 없는 새 요구가 생기면 **목록에 더한다.**
- **`aria-modal="true"` 는 화면 읽기 도구에만 알리고 키보드 Tab 이동은 안 막는다.** 가두려면 JS 로 포커스 순환을 직접 짜야 한다 — `aria-modal` 을 붙였다고 focus trap 이 된 것이 아니다.
- **직접 만들기로 했으면 그 파일에 두 가지를 적는다** — **왜 가져다 쓰지 않았나**, 그리고 **무엇을 다시 만들었나**. 둘째가 특히 중요하다: 나중에 그 파일을 고칠 때 **무엇을 안 깨뜨려야 하는지**가 거기 있다.
- **같은 접근성 코드를 여러 파일에 복제하지 않는다.** 두 번째가 나오면 가져다 쓸 때다.
자신만의 철학을 만들어가는 중입니다.
댓글남기기