한글 디스플레이 폰트 + -webkit-text-stroke 에서 없던 점이 생긴다

Google Fonts Gugi 로 바꾸자 글자 사이에 작은 점이 렌더링됐다. stroke 를 빼면 사라진다. 글리프의 분리된 서브패스에 외곽선이 독립적으로 그려지기 때문으로 보인다.

큰 타이틀에 흰색 외곽선을 3~4px 얹는 건 흔한 강조 패턴이다.

-webkit-text-stroke: 4px #fff;

폰트를 Jua 에서 Gugi 로 바꿨더니 특정 글자 사이에 원래 없어야 할 작은 점이 렌더링됐다.

의도:  사랑의 특급배송
실제:  사·랑의 특급배송

“사”와 “랑” 사이에 점이 하나 더 보인다. 오타를 의심할 만큼 자연스러워 보이는 게 고약하다.

같은 폰트, 같은 크기라도 -webkit-text-stroke 가 없는 텍스트에서는 안 나타난다. stroke 유무에 정확히 대응해서 나타나고 사라졌다.

원인 (추정)

Gugi 는 버블·라운드 스타일로 그려진 한글 디스플레이 폰트다. 일부 음절의 외곽선 경로에 겹치거나 분리된 서브패스가 들어 있는 것으로 보인다. 자소 사이 여백을 표현하는 작은 도형 같은 것들이다.

-webkit-text-stroke 는 글리프의 채우기가 아니라 외곽선 경로 자체를 따라 선을 그린다. 그래서 숨어 있던 서브패스에도 독립적으로 stroke 가 그려지고, 채우기만 할 때는 안 보이던 것이 화면에 점이나 획으로 드러난다.

폰트 파일 자체의 글리프 설계 문제로 보인다. 웹폰트가 완전히 로드된 뒤에도 재현된다. document.fonts.check() 로 로드 완료를 확인하고 다시 검증했으니 FOUT 이나 레이스 컨디션은 아니다.

확인 방법 — 같은 페이지에서 폰트만 갈아끼운다

폰트 후보를 비교할 때 각각 코드를 고쳐 빌드하면 느리다. 실행 중인 페이지에 CSS 를 주입해서 A/B 하면 몇 초면 된다.

await page.evaluate((family) => {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = `https://fonts.googleapis.com/css2?family=${family}&display=swap`;
  document.head.appendChild(link);

  const style = document.createElement('style');
  style.textContent = `* { font-family: '${family.replace('+', ' ')}', sans-serif !important; }`;
  document.head.appendChild(style);
}, 'Gugi');

await page.evaluate(() => document.fonts.ready);  // FOUT 배제
await page.screenshot({ path: 'out.png' });

document.fonts.ready 를 기다리는 줄이 중요하다. 이게 없으면 폴백 폰트가 찍힌 스크린샷을 보고 “괜찮네” 하게 된다.

DPI 를 올려야 보인다

deviceScaleFactor: 2 정도로 찍어야 눈에 들어온다. 1x 스크린샷에서는 압축 노이즈인지 실제 점인지 구분이 안 된다.

나도 처음엔 압축 아티팩트로 착각하고 넘어갔다. 확대해서 다시 보고서야 폰트를 바꾼 시점과 정확히 일치한다는 걸 알았다.

해결 — 다른 폰트로 교체

같은 계열(라운드·카툰 스타일에 두꺼운 weight)의 다른 후보인 Do Hyeon 으로 바꿨다. 동일 텍스트·동일 stroke 설정으로 재현 테스트했을 때 아티팩트가 없었다.

실제로 쓸 문구 6개 전부로 확인했다. 한 문구만 보고 판단하면 안 된다. 문제가 되는 글리프에 걸려야 나타나기 때문에, 안 걸리는 문구만 골라 본 셈이 될 수 있다.

교훈

폰트 미리보기로는 이 문제를 못 잡는다. Google Fonts 사이트의 미리보기는 stroke 없이 보여준다. 디스플레이 계열 한글 폰트를 -webkit-text-stroke 와 함께 쓸 계획이면, 교체 전에 실제 사용할 문구 전부로 stroke 를 적용한 상태를 스크린샷으로 확인해야 한다.

그리고 폰트 비교는 머릿속에서 결론 내지 않는 게 낫다. “Gugi 는 라운드니까 stroke 랑 잘 맞겠지” 같은 추론은 이 경우 정확히 반대였다. 라운드 스타일이라서 서브패스가 복잡했고, 그래서 stroke 와 안 맞았다.

dev 서버에 후보를 순차 주입해 같은 텍스트로 스크린샷을 비교하는 게 훨씬 빠르고 확실하다. 위 스크립트면 폰트 하나당 몇 초다.