UI 라이브러리를 만들면서 컴포넌트의 범위부터 정해야 했다. 살펴본 daisyUI 목록에는 68개가 있었고, shadcn의 목록은 더 길었다.
위에서 아래로 읽으면 전부 필요해 보인다. Rating도 언젠가 쓸 자리가 있어 보이고 Carousel도 없으면 아쉬워 보인다. 그런데 이 사이트는 화면이 다섯 개다.
68개 중 실제로 쓰는 건 몇 개인지부터 보자.
컴포넌트 목록 대신 실제 화면에서 반복되는 패턴을 세었다. 아홉 개를 추렸고, 구현과 검증 과정에서 추가로 네 가지 문제를 발견했다.
1. 목록을 보고 고르면 왜 전부 필요해 보이나
목록은 쓸 자리가 있는지를 묻지 않고 이름만 늘어놓는다. 읽는 쪽은 그 이름을 보고 쓸 상황을 떠올린다. 떠올려지면 필요한 것이 된다.
Rating을 보면 후기 화면이 떠오르는데 이 사이트에 후기 화면은 없다. 그래도 이름은 목록에 남아 있고 지울 근거를 대려면 없는 화면을 근거로 삼아야 한다. 그래서 그냥 남는다.
그래서 반대로 갔다. 이름이 아니라 코드에서 시작해서 화면에 실제로 몇 번 나오는지 세고 그 숫자를 넘는 것만 만든다.
기준은 세 번으로 잡았다. 두 번까지는 우연히 닮았을 수 있고 세 번째가 나와야 앞으로도 늘어난다는 근거가 된다.
2. 다섯 파일에서 한 글자도 다르지 않았다
화면 코드를 grep으로 훑었다.
grep -rn 'max-w-5xl flex-1 px-5 py-10 md:px-8 md:py-16' src --include='*.tsx'
다섯 줄이 나왔고 공백 하나까지 같았다.
PageShell이 생긴 이유는 이 다섯 줄이 전부다. 링크 규칙은 여섯 자리에서 같았다. 카드 면은 두 자리뿐이라 기준에 못 미쳤는데 두 자리의 모양이 서로 달라서 맞추려고 올렸다.
이렇게 아홉 개가 나왔다. 스펙마다 파일:줄을 그대로 적어 뒀다. 반년 뒤에 이걸 왜 만들었는지는 그 줄에 적힌 위치를 열어 보면 된다.
3. 화면에 한 번도 안 나오는 것은 어떻게 하나
여기가 기준이 안 서는 자리다. 이 사이트에는 폼이 없고 모달도 표도 없으니 세면 전부 0이다.
그런데 쓸 일이 없는 것과 아직 쓴 적이 없는 것은 다르다. 문의 폼은 곧 붙일 자리였다.
열한 개를 shadcn base-nova에서 받아 왔다. 대신 스펙마다 한 줄을 박았다.
handwork 사용처: 0곳
배럴도 두 묶음으로 나눴다. 위가 화면에서 쓰던 아홉이고 아래가 받아 온 열하나다.
// ── handwork 화면에서 쓰던 것 (9종) ──
export { AppLink } from "./ui/app-link";
// …
// ── shadcn base-nova 에서 받은 것 (11종) ──
export { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
같은 목록에 있어도 근거가 다르다. 섞어 두면 반년 뒤에 어느 것이 화면에서 검증된 값인지 알 수 없다.
4. 검사 대상이 없어도 통과했던 문제
접근성 검사를 붙였다. 스토리마다 axe가 돌고 위반이 있으면 그 스토리의 테스트가 실패한다.
돌렸더니 36개 전부 통과였는데 그중 하나는 일부러 깨뜨려 둔 스토리였다.
아이콘 버튼에서 aria-label을 뺐으니 이름 없는 버튼이 되고 그러면 걸려야 맞다. 그런데 자식으로 넣어 둔 ●가 진짜 글자여서 axe는 그걸 버튼 이름으로 읽었다.
aria-hidden 인 svg만 남기고 다시 돌렸다.
"Buttons must have discernible text (button-name)"
이 줄이 나오기 전까지는 검사가 돌고 있는 상태로 보였다. 초록불만 보고 넘어갔으면 그대로 보고됐을 것이다.
실패 0건은 그 자체로 아무것도 증명하지 않는다. 결함이 있는 입력에도 0건이 나올 수 있어서다. 그 뒤로는 게이트를 붙일 때마다 한 번씩 깨뜨려 본다. 대비 검사는 토큰 hex를 한 글자 바꿔서, 버전 검사는 CHANGELOG.md 최상단만 올려서 확인했다.
5. 색이 안 바뀌는데 아무도 알려주지 않았다
컴포넌트를 라이브러리로 뽑고 문서 사이트를 따로 만들었다. 링크에 마우스를 올렸더니 색이 그대로였다.
링크 컴포넌트는 --brand-accent를 쓰고 있었는데 그 토큰은 앱이 가진 것이라 라이브러리로 옮길 때 같이 오지 않았다.
Tailwind는 없는 토큰의 클래스를 아예 만들지 않아서 빌드가 안 깨진다. 타입도 맞고 테스트도 통과하고 스토리북도 초록불이다. 화면에서 마우스를 올려 보기 전까지는 아무 데도 안 나온다.
값이 같은 --ring이 라이브러리에 있어서 그쪽으로 바꿨다. 라이트가 #0B6B63이고 다크가 #5EEAD4 다. 화면 색은 그대로다.
사이트를 안 만들었으면 못 찾았다. 앱 안에 있는 동안에는 그 토큰을 앱이 갖고 있었다.
6. 빌드는 통과하는데 dev 서버만 500이 났다
토큰 파일은 라이브러리에 두고 쓰는 쪽이 가져간다. 한 줄이면 된다.
@import "@scopulus/ui/tokens.css";
src/tokens.css에 두고 package.json의 exports에 그 경로를 적었다. 프로덕션 빌드는 통과했다. 그런데 dev 서버를 띄우면 이 한 줄에서 500이 났다.
같은 파일의 같은 한 줄인데 빌드하는 쪽은 찾고 dev 쪽은 못 찾는다. 앞 절과 달리 이건 시끄럽게 실패해서 화면을 열자마자 보였다.
tokens.css를 패키지 루트로 옮겼다. exports를 거치지 않는 경로가 되니 양쪽이 같이 찾는다. 파일을 옮겨서 푼 것이라 왜 한쪽만 못 찾는지는 확인하지 못했다.
7. 버튼을 줄인 근거가 사라졌다
shadcn add button은 variant 6개에 size 7개를 준다.
화면에서 <button>을 세어 보니 하나였고 그것도 테마 토글이다. 그래서 variant 2개에 size 2개만 남겼다. 쓰지 않는 variant는 검증되지 않은 채로 스토리만 늘린다.
그 뒤에 Dialog를 받았다. 닫기 버튼이 icon-sm을 쓰고 바닥 버튼이 outline을 쓴다. 둘 다 앞에서 뺀 값이었다.
되살려서 지금은 variant 3개에 size 3개다. 판단이 틀렸던 게 아니라 “버튼이 들어갈 자리가 둘뿐”이라는 근거가 사실이 아니게 됐다.
onClick과 children도 필수에서 선택으로 내렸다. 아무 일도 안 하는 버튼을 타입으로 막으려고 첫 스펙에서는 필수로 잡은 값이다. 그런데 Base UI는 핸들러와 내용을 바깥에서 넣어 주므로 필수로 두면 라이브러리 안에서 조합이 안 된다.
이 두 줄이 CHANGELOG.md의 Changed 절이다. 이력을 손으로 쓰기로 했으니 잊힐 자리다. scripts/check-version.mjs가 최상단 버전과 package.json을 대조한다.
8. 도입 비용과 한계
- 타입이 막아 주던 것을 잃었다.
onClick을 선택으로 내리면서 빈 버튼과 핸들러 없는
버튼이 타입에서 통과한다.aria-label이 빠진 것만 접근성 검사가 잡고 나머지는 사람이 본다 className을 열었다.Dialog가 닫기 버튼의 위치를 바깥에서 앉히려면 열어야 했다.
색과 크기도 같이 덮어쓸 수 있게 됐다.cn이 충돌을 나중 값으로 정리해서 조용히 적용된다- hover 색을 화면마다 못 바꾼다.
--ring하나로 모았다. 불편한 게 아니라 그러라고 모았다 - 받아 온 열한 개는 검증되지 않았다. 스토리와 접근성 검사는 돌지만 실제 화면에서 한 번도
안 쓰였다. 그 사실을 스펙 한 줄로만 표시해 뒀다 - 이력을 손으로 쓴다. 검사기는 최상단 버전이
package.json과 같은지만 본다. 적힌 내용이
사실인지는 판정하지 않는다
확인하지 못한 것
- 시각 회귀 검사가 없다. 픽셀 단위로 어긋나는 것은 지금 게이트에 안 잡힌다
- 테스트는 라이트에서만 돈다. 다크 대비는 스크립트가 계산한 값에 기대고 있고 화면으로
확인하지 않았다 - 화면 이관이 아직이다. 이 사이트의 26곳이 옛 코드를 그대로 쓴다. 바꿀 자리는
파일:줄로
적어 뒀다 - Figma로는 못 넘긴다. 토큰을 W3C Design Tokens JSON으로 뽑는 스크립트까지 만들었는데,
받는 쪽 Variables API가 Enterprise 요금제를 요구한다. 그래서 JSON이 맞게 생겼는지도
확인하지 못했다
9. 화면에서 찾은 아홉 개와 최종 컴포넌트 스무 개
스무 개고 그중 아홉만 화면에서 나왔다.
소스는 packages/scopulus-ui
에 있다. 사이트 주소는 배포한 뒤에 채운다.
답글 남기기