WSL 한글 폰트 sudo 없이 설치하고 Pillow로 썸네일 만들기

2026. 9. 17. 13:00·IT
728x90
반응형

WSL 우분투에서 Pillow로 블로그 썸네일을 그리려는데 fc-list :lang=ko가 0줄을 돌려줬다. 한글 폰트가 하나도 없다는 뜻이다. sudo apt install fonts-noto-cjk가 정석이지만, 자동화 세션에서는 sudo가 비밀번호를 받을 터미널이 없어 막혔다. 이 글은 sudo 없이 사용자 홈에 Noto Sans KR을 설치하고, Pillow가 그 폰트를 찾게 하고, 제목 길이에 따라 글자 크기를 맞추는 썸네일 생성기를 만든 과정을 다룬다.

sudo 가 필요 없는 설치 경로가 있다

fontconfig는 시스템 폰트 디렉터리 말고도 사용자별 디렉터리를 읽는다. ~/.local/share/fonts다. 여기에 파일을 넣고 캐시만 갱신하면 그 사용자의 모든 프로그램이 폰트를 본다. 관리자 권한이 필요 없다.

폰트 파일은 Noto CJK 저장소에서 받았다. 이 저장소는 언어별 서브셋 OTF를 Sans/SubsetOTF/KR/ 아래에 두고 있어, 한국어만 필요하면 전체 CJK 묶음 대신 파일 두 개면 된다.

mkdir -p ~/.local/share/fonts
cd ~/.local/share/fonts
curl -sLO https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/SubsetOTF/KR/NotoSansKR-Regular.otf
curl -sLO https://raw.githubusercontent.com/notofonts/noto-cjk/main/Sans/SubsetOTF/KR/NotoSansKR-Bold.otf
fc-cache -f
fc-list :lang=ko
# NotoSansKR-Bold.otf: Noto Sans KR:style=Bold
# NotoSansKR-Regular.otf: Noto Sans KR:style=Regular

파일은 각각 4.6MB, 4.8MB였다. 처음에 Google Fonts의 다운로드 URL을 시도했는데 zip 대신 HTML 페이지가 내려와서 버렸다. GitHub raw 경로가 스크립트에서 다루기 확실하다. 되돌리고 싶으면 파일 두 개를 지우고 fc-cache -f를 다시 돌리면 끝이다. 시스템은 건드리지 않았다.

Pillow 는 사용자 폰트 디렉터리를 알아서 뒤진다

Pillow 문서의 ImageFont.truetype(font, size) 설명에는 이런 대목이 있다. 파일을 지정한 이름으로 못 찾으면 "리눅스에서는 ~/.local/share/fonts, /usr/local/share/fonts, /usr/share/fonts" 같은 디렉터리도 찾아본다. 즉 파일명만 줘도 된다.

from PIL import ImageFont
font = ImageFont.truetype("NotoSansKR-Bold.otf", 64)
print(font.getbbox("티스토리 발행 자동화"))
# (0, 21, 559, 82)

다만 나는 경로를 명시하는 쪽을 택했다. 폰트가 없으면 Pillow가 OSError를 내는데, 그 시점에는 어디를 뒤졌는지가 메시지에 없다. 후보 경로 목록을 코드에 두고 차례로 존재 여부를 확인한 뒤, 하나도 없으면 "Noto Sans KR을 설치하라"는 메시지로 실패하게 했다. 설정 파일에서 다른 폰트 경로를 줄 수도 있게 했다.

def font_path(weight):                      # "bold" | "regular"
    candidates = [
        Path.home() / ".local/share/fonts" / f"NotoSansKR-{weight.title()}.otf",
        Path(f"/usr/share/fonts/opentype/noto/NotoSansCJK-{weight.title()}.ttc"),
    ]
    for p in candidates:
        if p.exists():
            return p
    raise RuntimeError(f"한글 폰트를 찾지 못했습니다 ({weight})")

제목 길이에 맞춰 글자 크기를 줄이는 쪽이 자르는 것보다 낫다

썸네일은 1200×630, 어두운 배경에 제목을 흰색 굵은 글씨로 놓고 아래에 블로그 이름을 둔다. 문제는 제목 길이다. 12자짜리도 있고 38자짜리도 있다. 72px로 고정하면 긴 제목은 네 줄을 넘어 잘린다.

접근은 "3줄 안에 들어올 때까지 글자 크기를 4px씩 줄인다"다. 한글은 띄어쓰기가 적어서 textwrap.wrap의 단어 기준 줄바꿈이 잘 안 맞는다. 그래서 글자 폭을 크기의 0.95배로 어림잡아 한 줄 글자 수를 정하고, 실제 폭은 draw.textlength()로 다시 재서 넘치면 다음 크기로 넘어간다.

def fit_title(title, font_path, size, max_width, draw):
    for s in range(size, 34, -4):
        font = ImageFont.truetype(str(font_path), s)
        per_line = max(6, int(max_width / (s * 0.95)))
        lines = textwrap.wrap(title, width=per_line) or [title]
        if len(lines) <= 3 and all(draw.textlength(l, font=font) <= max_width for l in lines):
            return lines, font
    return textwrap.wrap(title, width=24)[:3], ImageFont.truetype(str(font_path), 34)

38자 제목은 60px 세 줄로, 12자 제목은 72px 한 줄로 나왔다. 글자를 자르는 방식(…)과 비교하면, 검색 결과나 SNS 카드에서 제목이 통째로 읽힌다는 점이 다르다. 썸네일의 목적이 클릭이라면 제목을 잘라서는 안 된다.

하나 더: 설정값이 빈 문자열이면 기본값으로 안 넘어간다

첫 결과물에서 하단의 블로그 이름이 안 찍혔다. 코드는 cfg.get("thumbnail.footer", cfg.get("blog.name"))였다. 설정 파일에 footer: ""가 있었고, 빈 문자열은 "키가 존재한다"이므로 get의 기본값이 쓰이지 않았다. cfg.get("thumbnail.footer") or cfg.get("blog.name") or ""로 바꿔 해결했다. 사소하지만 설정 파일에 예시로 빈 값을 적어두는 습관과 dict.get의 의미가 충돌하는 전형적인 경우다.

반론. 시스템에 폰트를 설치하는 것이 더 깔끔하지 않은가. 다른 사용자도 쓰고, 패키지 관리자가 갱신도 해준다. 맞다. 사람이 직접 터미널을 잡고 있다면 apt가 낫다. 이 글의 방법은 sudo를 쓸 수 없는 상황, 즉 자동화 세션이나 권한이 제한된 컨테이너에서 쓰는 우회로다. 다만 결과 파일이 홈 디렉터리 안에 있어서 무엇을 어떻게 바꿨는지 추적하기 쉽다는 이점은 우회로 쪽에 있다.

참고 자료

  • PIL.ImageFont — Pillow 문서 — truetype이 탐색하는 디렉터리, getbbox
  • notofonts/noto-cjk — GitHub — 언어별 서브셋 OTF 위치와 라이선스
  • Using HTTP cookies — MDN — 같은 파이프라인의 로그인 세션 문제를 다룬 글에서 함께 참고
728x90
반응형

'IT' 카테고리의 다른 글

설계 문서를 테스트로 강제하기, spec.yaml과 spec-check  (0) 2026.09.16
AI 에이전트 보안, 권한을 어디까지 줄 것인가  (0) 2026.09.16
Playwright 고정 대기 대신 상태 대기를 써야 하는 이유  (0) 2026.09.15
오픈소스 라이선스 변경, 기업이 자꾸 바꾸는 이유와 개발자의 대응  (1) 2026.09.15
Playwright intercepts pointer events 해결  (0) 2026.09.14
'IT' 카테고리의 다른 글
  • 설계 문서를 테스트로 강제하기, spec.yaml과 spec-check
  • AI 에이전트 보안, 권한을 어디까지 줄 것인가
  • Playwright 고정 대기 대신 상태 대기를 써야 하는 이유
  • 오픈소스 라이선스 변경, 기업이 자꾸 바꾸는 이유와 개발자의 대응
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (416) N
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (19) N
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자바 배열
    오류
    리눅스 설치
    servlet class
    spring java
    SSL 인증서
    자바 객체 지향
    러스트
    vue 설치
    proxy pass
    클론코딩
    자바 exception
    Rust lang
    ubuntu
    ssl 인증서 발급
    vue cli
    에디터 팁
    gitlab 설치
    Extension Bisect
    Node
    lang rust
    VS Code 팁
    브라우저 자동화
    자바 생성자
    Java Array
    svelte
    css transition
    rust linux
    css perspective
    자바 for문
    vscode
    자바 클래스
    css list
    mysql db
    프런트엔드
    jsp parameter
    React Compiler
    nginx ssl 적용
    docker
    css block
    티스토리 자동화
    nginx ssl 설정
    css float
    java casting
    css tb
    API 설계
    nginx
    css table
    AI 코딩 에이전트
    jsp include
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
WSL 한글 폰트 sudo 없이 설치하고 Pillow로 썸네일 만들기
상단으로

티스토리툴바