Skip to main content
← 블로그

같은 파랑, 두 가지 뜻 — 템플릿 표시를 다시 그리다

VauDium·

템플릿에서 온 글자를 파란 배경으로 표시했는데, 텍스트 선택도 파란 배경이었습니다. 색을 바꾸는 일인 줄 알았다가 브라우저와 두 모바일 엔진의 사정을 차례로 배운 하루의 기록입니다.

같은 파랑, 두 가지 뜻 — 템플릿 표시를 다시 그리다

페칫에는 템플릿 부분 보기·수정이라는 모드가 있습니다. 템플릿에서 발화된 할 일의 본문에서, 어디까지가 템플릿이 물려준 글자이고 어디부터가 이번에 내가 쓴 글자인지를 갈라 보여주는 기능이에요. 글자 하나하나에 계보를 새겨 두었기 때문에 가능한 일이고, 그 이야기는 글자에 새긴 계보에 적어 뒀습니다.

오늘 이야기는 그 뒤입니다. 계보는 정확히 기록되고 있었는데, 그걸 보여주는 방식이 틀렸습니다.

지적은 한 줄이었다

“템플릿 표시가 선택 표시랑 좀 헷갈리는 것 같은데?”

켜 보면 바로 압니다. 템플릿 몫 글자에는 브랜드 블루 배경이 옅게 깔립니다. 그리고 마우스로 글자를 드래그하면 — 파란 배경이 깔립니다. 같은 화면에서 같은 표현이 두 가지 뜻을 갖고 있었어요.

처음엔 색이 문제라고 생각했습니다. 다시 보니 겹친 건 색만이 아니었습니다. 표현 방식이 같았습니다. 둘 다 “글자 뒤를 칠하는” 방식이었고, 하필 색상축까지 같았습니다. 둘 중 하나만 어긋내도 구분이 서는 상황이었죠.

밑줄로 바꾸는 안을 먼저 검토했습니다. 선택은 절대 밑줄로 그려지지 않으니 확실합니다. 그런데 밑줄 서식과 링크가 이미 밑줄을 쓰고 있었고, 무엇보다 모바일 쪽은 텍스트 배경을 속성 하나로 칠하고 있어서 밑줄로 갈아타려면 안드로이드에 점선 밑줄을 직접 그려 넣어야 했습니다. 값이 너무 비쌌습니다.

그래서 색상축만 갈랐습니다. 선택이 파랑이니 템플릿은 앰버로. 라이트에서 #EAB308 20%, 다크에서 #FDE047 22%. 형광펜으로 그은 자리처럼 읽힙니다. 브랜드색을 쓰고 싶었던 게 애초에 화근이었습니다 — 브랜드색이 곧 시스템 강조색과 같은 계열이었으니까요.

조금 더 넓게, 그런데 padding 은 안 된다

“조금 더 넓게 할 수 있어?”

칠하는 면적을 키우자는 얘기였습니다. 인라인 요소니까 좌우로 여백을 주고 같은 크기의 음수 마진으로 상쇄하면 레이아웃은 그대로 두고 색만 넓어집니다. 흔한 수법이에요.

여기선 안 됐습니다. 이 규칙에는 box-decoration-break: clone 이 걸려 있습니다. 여러 줄에 걸친 배경이 줄마다 온전한 모양으로 그려지게 하는 속성인데, 여백뿐 아니라 마진까지 조각마다 복제합니다. 음수 마진으로 상쇄하려 들면 줄바꿈된 두 번째 줄부터 글이 왼쪽으로 밀립니다.

box-shadow 의 spread 를 썼습니다. 그림자를 2px 퍼뜨리면 요소 바깥으로 같은 색이 번지는데, 레이아웃에는 아무 영향이 없습니다. 배경과 그림자를 같은 색으로 맞추면 여백을 준 것과 똑같이 보입니다.

불릿에는 왜 색이 안 들어갔나

“체크리스트랑 불릿 리스트가 있는데 불릿은 색이 안 들어가 있거든?”

당연한 결과였습니다. 계보 마크는 글자에 붙는 표식인데, 리스트의 점과 번호는 글자가 아니거든요. 브라우저가 문단 바깥에 따로 그리는 장식입니다. 마크 범위 밖이니 배경이 닿을 리가 없죠.

칠하려면 마커를 따로 지목해야 하는데, 여기서 두 번째 벽을 만났습니다. ::marker 는 배경색을 받지 않습니다. 색과 글꼴 계열만 통합니다. 그래서 마커는 배경 워시가 아니라 진한 앰버 글자색으로 갔습니다. 본문은 옅게 깔리고 점은 진하게 찍히는데, 나란히 보면 같은 계열이라 한 덩어리로 읽힙니다.

모바일은 사정이 또 달랐습니다. iOS 와 안드로이드는 불릿을 텍스트 첨부물로 직접 그리고 있었고, 그 색이 뷰 전체에 하나였습니다. 줄마다 다른 색을 주려면 줄을 만드는 자리에서 “이 줄에 템플릿 몫 글자가 있나”를 물어봐야 했어요. 다행히 줄 데이터에 이미 그 정보가 있어서, 판정 함수 하나를 양쪽에 같은 모양으로 넣는 것으로 끝났습니다.

반전 채움은 왜 실패했나

툴바의 모드 토글 버튼도 앰버로 맞추기로 했습니다. 이왕이면 반전색으로 꽉 채워서 “모드가 켜졌다”를 크게 외치게 하자는 생각이었죠.

만들어 놓고 나란히 보니 어긋났습니다. 버튼은 진한 머스터드인데 본문은 연한 크림이었습니다. 같은 hex 를 쓰는데도요.

당연한 일이었습니다. 본문 워시는 글자가 그 위에 얹히니까 옅어야 합니다. 20% 알파를 흰 배경에 얹으면 대략 #FBF1D5 가 나옵니다. 반면 버튼은 불투명 #EAB308 이고요. 그리고 이건 알파를 조절해서 좁힐 수 있는 간극이 아닙니다 — 워시를 진하게 만들수록 그 위의 글자가 읽기 어려워지니까요. 반투명 워시와 불투명 채움은 애초에 같은 색이 될 수 없습니다.

그래서 반전을 접고, 버튼도 본문 하이라이트를 축소한 모양으로 갔습니다. 옅은 앰버 배경 + 진한 앰버 아이콘. 마침 이건 툴바의 다른 서식 토글들이 이미 쓰던 활성 문법이라, 색만 다른 같은 모양이 됐습니다. 한때 테두리 링을 둘러 조금 더 튀게 해 볼까 했지만 그것도 뺐습니다. 툴바에 혼자만 다른 문법을 쓰는 버튼을 두는 것보다, 컨벤션 안에 있는 편이 낫습니다.

다크 모드가 걸었던 발

버튼 색을 팔레트 토큰(SECONDARY500)으로 잡았습니다. 토큰을 썼으니 다크 모드는 알아서 되겠거니 했죠.

안 됐습니다. 페칫의 다크 테마는 노랑 계열 램프를 거울처럼 뒤집어 정의합니다. 라이트에서 50이 가장 옅고 900이 가장 진한데, 다크에서는 그 반대예요. 그리고 하필 500은 뒤집어도 자기 자신입니다. 양 테마에서 똑같은 값이 나옵니다.

토큰을 썼는데 테마를 아예 안 타는, 눈에 잘 안 띄는 함정이었습니다. 더 성가신 건 그 다음이었어요. 밝은 앰버 위에 얹을 어두운 잉크가 필요했는데, 램프가 뒤집혀 있으니 “양 테마에서 계속 어두운” 토큰이 아예 존재하지 않습니다. 고정값을 박는 수밖에 없었습니다.

결국 이건 반전 채움을 접어야 할 이유가 하나 더 있었다는 뜻이기도 했습니다. 워시 배경으로 돌아오자 필요한 잉크가 “라이트에선 진한 앰버 / 다크에선 밝은 앰버”로 바뀌었고, 뒤집힌 램프가 정확히 그 짝을 갖고 있었거든요. 고정값이 사라졌습니다.

체크박스는 상자를 직접 그려야 했다

체크리스트에도 표시를 넣으려는데, 여기가 제일 깊었습니다.

데스크톱의 체크박스는 브라우저가 그리는 진짜 컨트롤입니다. CSS 로 건드릴 수 있는 건 accent-color 하나뿐이고, 그건 체크된 상태의 채움색만 바꿉니다. 빈 상자의 테두리는 운영체제가 그려서 손이 닿지 않아요.

그런데 계보 표시는 체크 여부와 상관없어야 합니다. 이건 상태 표시가 아니라 출처 표시니까요. 완료하지 않은 항목도 템플릿에서 왔으면 그렇게 보여야 합니다.

방법은 하나뿐이었습니다. 네이티브 컨트롤을 끄고 상자를 직접 그리는 것. appearance: none 으로 브라우저 그림을 지우고, 테두리와 채움과 체크 표시를 CSS 로 다시 만들었습니다. 종전 상자의 치수와 색을 그대로 맞춰서 겉보기 변화는 없지만, 이제 데스크톱의 모든 체크리스트 상자는 브라우저가 아니라 우리가 그립니다.

여기서 재밌는 걸 깨달았습니다. 모바일은 원래부터 직접 그리고 있었습니다. 같은 개념이 한쪽에서는 OS 컨트롤이고 다른 쪽에서는 우리 코드였던 거죠. 이번에 데스크톱이 모바일을 따라간 셈입니다.

한 가지 곁가지 — 앰버로 채운 상자 위의 체크 표시는 흰색이면 안 됩니다. 앰버는 휘도가 높아서 흰 표시가 묻혀요. 어두운 갈색으로 바꿨습니다. 파란 상자에 흰 체크는 5:1 넘게 나오는데 앰버에 흰 체크는 3:1도 안 나옵니다.

모드는 포커스와 함께 끝난다

마지막은 색이 아니라 수명에 관한 것이었습니다. “포커스를 잃으면 모드도 꺼지게 하자.”

코드를 열어 보니 이미 그렇게 돼 있었습니다 — 제목 편집기에도, 모바일에도. 데스크톱 본문 편집기 한 군데만 빠져 있었어요. 예전에 “모드는 편집기 수명 동안 유지”라고 정해 뒀던 결정이 다른 곳들에서는 이미 뒤집혔는데, 여기만 남아 있었던 겁니다. 세 줄을 더하는 걸로 끝났습니다.

그런데 하나가 더 있었습니다. “템플릿에 반영” 버튼을 눌러 창을 띄우면 모드가 그대로 켜져 있더군요. 포커스가 안 옮겨간 겁니다. 툴바 버튼들은 눌러도 편집기 포커스를 뺏지 않도록 일부러 막아 뒀고(미저장 편집을 먼저 저장해야 해서요), 떠오른 창도 포커스를 가져가지 않았습니다. 기댈 blur 자체가 없었습니다. 그 자리에서는 명시적으로 껐습니다.

오늘의 교훈

하나, 겹치는 건 색이 아니라 표현 방식일 때가 많습니다. “파란 하이라이트”와 “파란 선택”이 헷갈린 이유의 절반은 파랑이었지만 나머지 절반은 “뒤를 칠한다”였습니다. 색만 보고 원인을 좁히면 다음에 또 겹칩니다.

둘, 반투명과 불투명은 형제가 아닙니다. 같은 색값을 써도 알파가 다르면 다른 색입니다. 글자 뒤에 깔리는 것과 버튼을 채우는 것은 애초에 같아질 수 없고, 억지로 맞추려 하면 둘 중 하나가 망가집니다.

셋, 토큰을 썼다고 다크 모드가 된 게 아닙니다. 스케일이 뒤집혀 정의된 램프에서는 가운데 값이 테마를 안 탑니다. 토큰 이름이 주는 안심은 실제 값 앞에서 무력합니다.

넷, 같은 개념을 플랫폼마다 다른 엔진이 그립니다. 리스트 마커, 체크박스, 텍스트 배경 — 셋 다 브라우저와 iOS 와 안드로이드에서 그리는 주체가 다릅니다. “이 색을 저기에도”가 한 줄로 끝나는 경우는 거의 없고, 어디까지 맞출지는 매번 값을 재 보고 정해야 합니다. 이번엔 워시 넓히기를 데스크톱에만 두기로 했습니다. 모바일은 배경이 줄 높이 전체를 칠해서 이미 넉넉하거든요.

다섯, 문법이 이미 있는데 한 군데만 빠져 있는 경우가 잦습니다. 포커스 해제도 그랬습니다. 새로 정할 일이 아니라 이미 정해 둔 걸 마저 적용하는 일이었어요. 새 규칙을 만들기 전에 먼저 뒤져 볼 이유입니다.

기능이 늘지는 않았습니다. 계보는 어제도 정확히 기록되고 있었어요. 다만 이제 그걸 보고 있는 사람이 자기가 뭘 보고 있는지 헷갈리지 않습니다.