모든 글
Android · 3분 읽기

안드로이드를 만들며 1 — 비워둔 타이포 슬롯은 남의 글꼴이 들어오는 문이었어요

Material3의 Typography 슬롯 열다섯 중 열하나만 정의하고 넷을 비워뒀어요. 그런데 비운 넷은 없는 칸이 되지 않았습니다. Roboto로 채워졌고, 화면은 그 넷을 실제로 부르고 있었어요.

#android#compose#design-system#typography

첫 앱을 만들면서 부딪힌 것들을 여덟 편으로 나눠 적어보려고 합니다.

앞의 넷은 빌드가 통과하고, 린트도 조용하고, 크래시도 안 났는데 화면이나 뜻이 조용히 틀어져 있던 것들이에요.

  1. 비워둔 타이포 슬롯 — 지금 이 글
  2. 위를 향한 삼각형은 그림자로 세울 수 없다
  3. aapt가 줄바꿈을 공백 하나로 접는다
  4. 검증은 한 번, 증명은 타입이 들고 다닌다

뒤의 넷은 그 답이 매번 같아서, 아예 구조를 바꾼 이야기입니다.

  1. 하루에 구조를 세 번 바꿨다 — Hilt · Navigation
  2. 데이터를 한 줄기로 — AppRepository와 SSOT
  3. 이벤트를 한 입구로 — MVI
  4. 생명주기를 한 자리로 — MainActivity 다이어트

첫 편은 글꼴 이야기입니다. 디자인 시스템을 만들 때 아주 자연스럽게 드는 생각이 하나 있어요.

“안 쓰는 칸은 정의하지 말자. 쓰는 것만 두면 사다리가 깨끗하니까.”

우리가 만든 사다리에서는 맞는 말이에요. 그런데 남의 사다리에서는 정반대입니다.

한 화면 안에서 글꼴이 갈려 있었어요

물타기 계산기는 Pretendard 한 벌로 그립니다. 그런데 결과 카드의 3열 대시보드와 키패드의 판독값·숫자 키가 다른 글꼴로 그려지고 있었어요.

같은 화면 안에서요. 제목은 Pretendard인데 그 밑의 숫자는 아니었습니다.

목업의 토큰 페이지에는 이렇게 적혀 있었어요.

안 서는 칸이 넷 — displaySmall · headlineSmall · bodySmall · titleSmall

사실은 서고 있었습니다. 다만 우리 글꼴이 아니었을 뿐이에요.

Typography()의 매개변수는 전부 기본값을 가집니다

Material3의 Typography는 슬롯이 열다섯이에요. 우리는 그중 열하나를 정의하고 넷을 비워뒀습니다.

// 이렇게 적으면 "넷은 없다"가 아닙니다
val TwinkleTypography = Typography(
    displayLarge = TextStyle(fontFamily = Pretendard, /* … */),
    displayMedium = TextStyle(fontFamily = Pretendard, /* … */),
    // displaySmall — 안 쓰니까 뺐습니다
    headlineLarge = TextStyle(fontFamily = Pretendard, /* … */),
    headlineMedium = TextStyle(fontFamily = Pretendard, /* … */),
    // headlineSmall — 안 쓰니까 뺐습니다
    // …
)

Typography()의 매개변수는 전부 기본값을 가진 TextStyle이에요. 안 넘기면 “비어 있음”이 넘어가는 게 아니라 Material의 값이 넘어갑니다. 그리고 Material의 기본 글꼴은 Roboto죠.

즉 위 코드는 “넷을 뺐다”가 아니라 이렇게 읽힙니다.

displaySmall = /* Material 이 알아서 채웁니다 — Roboto 로 */

우리가 “지우는 일”이라고 생각한 것이 실제로는 “남더러 채우라”고 넘기는 일이었어요.

왜 눈으로 못 잡았을까요

Roboto와 Pretendard는 둘 다 산세리프예요. 한글은 Roboto에 없어서 시스템 한글 글꼴로 떨어지는데, 그것도 산세리프입니다. 숫자와 라틴 문자에서만 차이가 나요.

그래서 숫자가 서는 자리에서만 티가 났습니다. 대시보드와 키패드죠. 그리고 그 자리가 하필 이 앱에서 가장 자주 보는 곳이었어요.

문서에 “안 서는 칸”이라고 적어둔 것도 한몫했습니다. 적어놓은 사람도, 읽은 사람도 그 문장을 확인하지 않았거든요. 문서가 틀린 게 아니라, 문서가 만든 믿음이 확인을 막은 거예요.

그래서 이렇게 정했어요

남의 프레임워크가 슬롯을 요구하면 전부 채웁니다

안 쓰는 칸도 지우지 말고 막아둡니다. 우리 값으로 채워두면 나중에 누가 그 슬롯을 부르더라도 글꼴이 갈리지 않아요.

우리 사다리의 “안 불리는 칸은 걷는다”는 법은 우리가 만든 사다리에만 맞는 말이었습니다. 남이 기본값을 채워주는 자리에서 빈칸은 “안 쓴다”가 아니라 “남더러 쓰라”는 뜻이에요.

사다리는 슬롯 개수와 따로 셉니다

슬롯이 열다섯이라고 크기 사다리가 열다섯 칸일 이유는 없어요. 우리 사다리는 크기 열 칸입니다.

11 · 12 · 14 · 16 · 20 · 24 · 28 · 32 · 36 · 40

두 슬롯이 한 칸에 같이 서도 됩니다. 키패드의 숫자 키와 이력의 결과 숫자는 둘 다 24이고 굵기로 갈려요.

슬롯은 프레임워크가 요구하는 자리 이름이고, 사다리는 우리가 정한 값입니다. 그 둘을 1:1로 맞추려고 하면 사다리에 없는 칸이 생겨요.

“이 칸은 아무 데도 안 선다”는 문장은 화면에서 확인하고 적습니다

안 서는 것과 남의 값으로 서는 것은 눈으로 보면 전혀 다릅니다. 그런데 코드만 읽으면 둘 다 “없음”으로 보여요.

이번 작업에서 정한 기준

이 버그의 성질이 계속 마음에 걸립니다. 빌드는 통과했고, 린트도 조용했고, 크래시도 없었어요. 그냥 글꼴이 달랐습니다. 그리고 그걸 알아채려면 그 화면을 그 자리에서 봐야 했어요.

기본값을 주는 API는 편합니다. 편한 만큼 “내가 안 준 것”과 “내가 준 적 없는 값이 들어간 것”이 코드에서 똑같이 생겼어요. 그 둘을 가르는 건 문서가 아니라 화면이더라고요.

다음 글도 화면에서만 보이는 이야기입니다. 이번엔 아예 눈에 안 보이던 도형 이야기예요.

👉 2 — 안드로이드의 빛은 위에서 내려옵니다 - 위를 향한 꼭지는 그늘로 설 수 없어요