CSS 스크롤바 커스텀과 시맨틱 HTML, 그리고 에이전트 오케스트레이션 관찰

26년 08월 05일

스크롤바 하나에 접근성 이중 레이어를 입히는 일

전 매일 쓰는 도구의 스크롤바가 거슬렸다. OS 기본 스크롤바가 다크 글래스모피즘 디자인과 안 어울렸기 때문이다.

가장 먼저 확인한 건 스크롤바 문제가 사실 두 개라는 점이었다. 레이아웃 시프트는 scrollbar-gutter: stable로 예약하고 모양·색상은 표준 속성과 웹킷 폴백으로 나누는 구조다.

처음에는 표준 속성인 scrollbar-widthscrollbar-color면 끝날 줄 알았다. 그런데 Safari가 핵심 함정이었다. scrollbar-width는 18.2부터 지원하면서 scrollbar-color는 26.2까지 미지원이다. 폭만 얇아지고 색상은 OS 기본으로 남는, 어색한 중간 상태가 만들어진다.

그래서 표준 속성과 ::-webkit-scrollbar를 겹치는 이중 레이어로 갔다. Firefox·Chrome 121+·Safari 18.2+는 표준 속성으로 width와 color를 잡고 Chromium·WebKit은 웹킷 pseudo-element로 라운드 thumb 모양을 덮는 식이다. 의존성 없이 CSS 약 15 줄로 끝난다.

색상은 디자인 토큰인 --muted-foreground를 그대로 가져다 썼다. 원래 배경 위 가독 텍스트용으로 튜닝된 토큰이라 다크·라이트 양쪽에서 대비가 안정적이다. border: 2px solid transparentbackground-clip: padding-box를 붙이면 thumb가 track 양끝에 안 닿고 뜨는 floating thumb 기법이 된다.

여기서 WCAG 1.4.11이 걸려 나왔다. 비텍스트 구성요소는 인접 배경 대비 3

이상이 권장이다. alpha 0.6은 라이트 테마에서 3
경계선이라 빌드 후 DevTools로 두 테마를 검증하고 필요하면 0.7로 올리기로 했다. scrollbar-width: none으로 스크롤바를 숨기는 선택지는 처음부터 배제했다. 휠 없는 마우스 사용자가 스크롤을 못 찾는 접근성 함정이기 때문이다.

정리하면 스크롤바 커스텀은 JS 라이브러리를 달 problem이 아니라 표준 속성과 웹킷 폴백을 겹치는 CSS 문제다. JS로 스크롤바를 만들면 키보드·ARIA·포커스를 직접 구현해야 하고 forced-colors 모드와 충돌한다. 전 연령대가 쓰는 금융 도구에서 그 위험은 감수할 이유가 없다.

label/value 섹션은 div+span 말고 dl/dt/dd로

같은 세션에서 상세페이지의 label/value 섹션을 손봤다. 원래 구조는 <div className="flex justify-between"> 안에 좌측 <span> 라벨, 우측 <span> 값이 들어가는 형태였다.

문제는 값 길이가 제각각이었던 것이다. 짧은 값은 좌-우 간극이 벌어지고 긴 값은 줄바꿈이 어색했다. 시각 리듬이 들쭉날쭉했고 스크린리더도 “라벨-값 짝”으로 읽지 못했다.

<dl>/<dt>/<dd>와 CSS Grid로 바꿨다. 라벨은 <dt>로 위에, 값은 <dd>로 아래에 쌓는다. 2열 그리드에서 짧은 항목 두 개가 한 행을 공유하고 긴 항목은 wide 플래그로 sm:col-span-2를 줘서 행 전체를 차지하게 했다. 배열 순서를 짧은 값에서 긴 값 순으로 두면 그리드 흐름이 자연스럽다. 긴 항목이 중간에 끼면 그 앞행에 빈 칸이 생긴다.

빈 값 처리도 토큰 기반으로 통일했다. 값이 없으면 “정보 없음”을 보여주고 text-muted-foreground로 회색을 준다. 값이 있으면 text-foreground로 진하게 내보낸다. 결측을 색으로 구분하면 한눈에 데이터 유무가 들어온다.

좌-우 분산 구조에서 위-아래 stacked로 바꾸니 눈이 지그재그로 움직일 필요가 없어졌다. 라벨은 회색, 값은 진하게라 컬러 계층까지 겹쳐 스캔이 빨라진다. 반응형도 자연스럽다. grid-cols-1 sm:grid-cols-2로 모바일은 1열, 데스크톱은 2열이 된다. flex justify-between은 줄 수가 고정이 아니라 반응형 짝짓기가 어려웠다.

에이전트 오케스트레이션을 관찰만 한 하루

같은 날 AI 코딩 에이전트의 서브에이전트 오케스트레이션을 관찰할 기회가 있었다. 코드 수정 없이 시스템 메시지가 그대로 노출된 세션이라 평소 보기 힘든 내부 구조를 들여다볼 수 있었다.

핵심은 조사·기획·실행·검증을 분리하는 계약 우선 웨이브였다. 조사는 read-only로 코드를 읽고 보고서를 쓰고, 기획은 그 보고서를 입력으로 구현 명세를 쓰고, 실행은 명세를 축자대로 따른다. 검증은 별도 서브에이전트가 독립적으로 돈다. 구현 에이전트가 “다 했다”고 낙관 보고하는 걸 막는 장치다.

검증자 프롬프트에 인상적인 문구가 있었다. “메인 에이전트의 주장을 절대 믿지 마라”는 핵심 문장이었다. 검증자는 원본 사용자 요구를 기준으로 코드를 직접 읽고 판정을 내린다. 실제로 이 세션에서 “테스트 미수행”, “폴백이 성공으로 카운트됨” 같은 거짓 성공 주장이 검증자에 의해 적발됐다.

한 가지 더 흥미로운 건 검증 루프의 구조적 약점이었다. fork 타이밍이 만드는 stale FAIL이라는 현상인데, 검증자가 dispatch 시점 스냅샷에서 fork하면 이후 수정이 반영되지 않는다. 이미 고친 코드를 두고도 “여전히 실패” 판정이 떨어지는 문제다. 마일스톤 게이팅이 만드는 false PASS도 있었다. 구현자가 검증자의 올바른 FAIL을 범위 밖으로 번복하는 식이다.

오케스트레이션도 도구고, 검증 루프도 도구다. 도구가 늘어날수록 그 도구 자체의 약점도 보인다. 어떤 날은 코드를 쓰는 것보다 도구를 관찰하는 게 더 많은 걸 알려준다.