플러터(Flutter) 화면에서 점수나 재화 숫자가 바뀔 때마다 그 칸의 폭이 조금씩 달라져 옆에 붙은 칸들이 흔들릴 때, 숫자 폭을 고정해 해결하는 방법입니다.
개인 게임 앱 위쪽 막대에 골드, 재료, 가방 같은 칩을 나란히 두었는데, 골드가 오를 때마다 오른쪽 칩들이 좌우로 살짝살짝 움직였습니다. 숫자가 쉬지 않고 바뀌는 화면이라 그 떨림이 계속 눈에 걸렸습니다.
- 원인은 글꼴의 숫자마다 폭이 달라서, 같은 자리 수여도 값에 따라 칩 폭이 바뀐 것이었다
- 흔히 쓰는 FontFeature.tabularFigures() 는 글꼴에 tnum 기능이 없으면 아무 효과가 없다
- 그래서 숫자 칸의 폭을 가장 넓은 숫자 기준으로 재서 고정했다
1. 숫자 폭이 글꼴마다 다르다
본문용 글꼴은 숫자 열 개의 폭을 똑같이 맞춘 경우가 많지만, 제목이나 장식용 글꼴은 1 은 좁고 4 나 0 은 넓게 그리는 가변폭 숫자가 흔합니다. 이 앱의 숫자 글꼴(주아)을 16px 로 쓰면 1 은 약 6.6px, 4 는 약 10.1px 이라 같은 세 자리라도 111 과 444 는 10px 넘게 차이가 납니다.
칩이 내용 폭만큼 늘었다 줄었다 하는 구조(Row(mainAxisSize: MainAxisSize.min))라면, 이 차이가 그대로 옆 칩의 위치를 밉니다. 숫자가 자주 바뀌는 카운터나 타이머에서 특히 눈에 띕니다.
2. 칸 넓이를 비율로 고정했더니 숫자가 작아졌다
처음에는 칩 네 개의 폭을 Expanded(flex: ...) 로 비율 고정하고, 숫자가 칸을 넘치면 FittedBox 로 줄이게 했습니다. 칩은 더 이상 움직이지 않았지만, 옆에 단추가 붙은 칩처럼 칸이 빠듯한 곳에서는 숫자가 눈에 띄게 작아졌습니다.
칸이 움직이는 문제를 칸을 묶어서 풀면, 이번엔 글자 크기가 값에 따라 바뀌는 문제가 생기는 셈이라 이 방법은 되돌렸습니다. 고정해야 할 것은 칩 전체가 아니라 숫자가 차지하는 폭이었습니다.
3. tabularFigures 를 쓰기 전에 글꼴부터 확인하기
숫자 폭을 맞추는 정석은 오픈타입의 고정폭 숫자 기능(tnum)이고, 플러터에서는 TextStyle(fontFeatures: [FontFeature.tabularFigures()]) 로 켭니다. 다만 이건 글꼴 안에 tnum 기능이 들어 있을 때만 동작하고, 없으면 에러 없이 그냥 무시됩니다.

fontTools 로 열어 보니 주아는 글꼴 기능이 kern 하나뿐이라 tnum 이 없었고, 나눔스퀘어라운드는 tnum 은 없지만 숫자 열 개가 원래 같은 폭(600)이었습니다. 그러니 앞의 글꼴은 tabularFigures 를 켜도 그대로 흔들리고, 뒤의 글꼴은 켜지 않아도 흔들리지 않습니다.
4. 가장 넓은 숫자로 폭을 재서 고정하기
글꼴이 도와주지 않으면 폭을 직접 재면 됩니다. 표시할 문자열의 숫자를 모두 가장 넓은 숫자 하나로 바꿔 TextPainter 로 잰 뒤, 그 폭의 SizedBox 안에 실제 숫자를 넣는 방식입니다.

이렇게 하면 같은 자리 수 안에서는 값이 바뀌어도 칸 폭이 그대로이고, 자리 수가 늘어날 때만 한 번 넓어집니다. 쉼표나 K, M 같은 단위 글자는 그대로 두고 숫자만 바꿔 재기 때문에 표시 형식이 섞여 있어도 됩니다.
여기서 기준 숫자를 무엇으로 잡느냐가 중요한데, 흔히 넓을 것 같은 8 은 이 글꼴에서 가장 넓지 않았습니다. 주아의 숫자 폭을 보면 8 은 581 인데 4 는 628 이고, 0, 2, 6, 7, 9 도 8 보다 넓습니다. 8 로 재면 4 가 여러 개 들어간 값에서 칸이 글자보다 좁아져 끝이 잘릴 수 있으니, 기준은 짐작하지 말고 위처럼 열 개를 재서 가장 넓은 것으로 고르는 게 안전합니다.
5. 정리
숫자가 바뀔 때마다 옆 칸이 흔들린다면 글꼴의 숫자가 가변폭인지부터 확인하고, FontFeature.tabularFigures() 를 쓰기 전에 그 글꼴에 tnum 기능이 있는지 봐야 합니다. 기능이 없으면 가장 넓은 숫자로 폭을 재서 숫자 칸만 고정하는 편이, 칸 전체를 묶어 글자를 줄이는 것보다 화면이 덜 어색했습니다.
여기까지 플러터에서 숫자가 바뀔 때 칸이 흔들리는 문제에 대해서 작성해봤습니다. 여기까지 읽어주셔서 감사합니다!