Ch3 텍스트 포맷팅하기

Feb 19th, 2026

 Timothy @ Daangn Frontend Core

Software Engineer, Frontend @Daangn Frontend Core Team

ex) Senior Lead Software Engineer @NHN Dooray (2021 ~ 2023)

ex) Lead Software Engineer @ProtoPie (2016 ~ 2021)

ex) Microsoft MVP

Timothy Lee

이 웅재

3.1 폰트(서체)란?

금속활자에서 유래

  • 글자마다 별도의 상자(케이스)에 모았음

  • 대문자용 활자 케이스는 upper-case, 소문자용 활자 케이스는 lower-case

  • 이러한 케이스들의 집합을 폰트라고 함

  • 여러 크기의 폰트를 모아 타입이라 함

  • 글자의 굵기나 기울기 변형은 타입의 페이스라 함

오늘날의 폰트

  • 스타일과 크기를 의미함

    • ​bold, normal, weight, font-family

tkinter.Font.Font

  • Tk 의 폰트 객체는 고정된 크기, 스타일, 글자 두께 같은 정보를 담고 있습니다.

import tkinter
import tkinter.font

if __name__ == "__main__":
    window = tkinter.Tk()
    bi_times = tkinter.font.Font(
        family="Times",
        size=16,
        weight="bold",
        slant="italic"
    )
    canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
    canvas.pack()
    canvas.create_text(200, 100, text="Hello, World!", font=bi_times)
    tkinter.mainloop()

3.2 텍스트 측정하기

.metrics

  • .metrics: 세로축에 대한 정보

  • ​linespace: 텍스트의 높이

  • ascent: 기준선 윗부분

  • descent: 기준선 아래부분

  • 크기가 다른 글자가 같은 줄에 있을때 기준선으로 정렬해야 함

  • fixed: 글자마다의 가로 너비가 항상 같은지 여부

  • font-size 는 픽셀이 아니라 포인트 (약 1/72 인치)

print(bi_times.metrics())
{'ascent': 15, 'descent': 4, 'linespace': 19, 'fixed': 0}

.measure("Hi!")

  • .measure: 가로축에 대한 정보​

  • 전체 픽셀값을 반올림해서 보여주기 때문에 합이 다를 수 있음

  • 일부 폰트는 kerning 를 이용해서 조합의 가로 값이 달라질 수 있음

print(bi_times.measure("Hi!"))
24
print(bi_times.measure("H"))
13
print(bi_times.measure("i"))
5
print(bi_times.measure("!"))
7

Hello, (normal) + World!(italic)

if __name__ == "__main__":
    window = tkinter.Tk()
    canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
    canvas.pack()
    
    font1 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )
    font2 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )

    x, y = 200, 200
    canvas.create_text(x, y, text="Hello, ", font=font1)
    x += font1.measure("Hello, ")
    canvas.create_text(x, y, text="World!", font=font2)

    tkinter.mainloop()

Hello, (normal) + Overlapping!(italic)

if __name__ == "__main__":
    window = tkinter.Tk()
    canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
    canvas.pack()
    
    font1 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )
    font2 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )

    x, y = 200, 200
    canvas.create_text(x, y, text="Hello, ", font=font1)
    x += font1.measure("Hello, ")
    canvas.create_text(x, y, text="Overlapping!", font=font2)

    tkinter.mainloop()

anchor="nw"

if __name__ == "__main__":
    window = tkinter.Tk()
    canvas = tkinter.Canvas(window, width=WIDTH, height=HEIGHT)
    canvas.pack()
    
    font1 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )
    font2 = tkinter.font.Font(
        family="Times",
        size=16,
        slant="italic"
    )

    x, y = 200, 200
    canvas.create_text(x, y, text="Hello, ", font=font1, anchor="nw")
    x += font1.measure("Hello, ")
    canvas.create_text(x, y, text="Overlapping!", font=font2, anchor="nw")

    tkinter.mainloop()

3.3 한 단어씩 처리하기

한 글자씩 to 한 단어씩

def layout(text):
    font = tkinter.font.Font() #
    display_list = []
    cursor_x, cursor_y = HSTEP, VSTEP
    for word in text.split(): #
        w = font.measure(word) #
        display_list.append(
            (cursor_x, cursor_y, word) #
        )
        cursor_x += w + font.measure(" ") #
        if cursor_x + w > WIDTH - HSTEP: #
            cursor_y += font.metrics("linespace") * 1.25 #
            cursor_x = HSTEP
    return display_list

3.4 텍스트에 스타일 주기

Refactoring...

  • 토큰으로 변경

  • 토큰은 텍스트거나 태그

class Text:
    def __init__(self, text):
        self.text = text

class Tag:
    def __init__(self, tag):
        self.tag = tag

lex (Refactoring...)

def lex(body):
    out = [] #
    buffer = "" #
    in_tag = False
    for c in body:
        if c == "<":
            in_tag = True
            if buffer: #
                out.append(Text(buffer)) #
            buffer = "" #
        elif c == ">":
            in_tag = False
            out.append(Tag(buffer)) #
            buffer = "" #
        else:
            buffer += c #
    
    if not in_tag and buffer: #
        out.append(Text(buffer)) #

    return out #

layout (Refactoring...)

def layout(tokens):
    font = tkinter.font.Font()
    display_list = []
    cursor_x, cursor_y = HSTEP, VSTEP
    weight = "normal"
    style = "roman"
    for tok in tokens:
        if isinstance(tok, Text):
            for word in tok.text.split():
                font = tkinter.font.Font(
                    size=16,
                    weight=weight,
                    slant=style
                )
                w = font.measure(word)
                display_list.append(
                    (cursor_x, cursor_y, word, font)
                )
                cursor_x += w + font.measure(" ")
                if cursor_x + w > WIDTH - HSTEP:
                    cursor_y += font.metrics("linespace") * 1.25
                    cursor_x = HSTEP
        elif tok.tag == "i":
            style = "italic"
        elif tok.tag == "/i":
            style = "roman"
        elif tok.tag == "b":
            weight = "bold"
        elif tok.tag == "/b":
            weight = "normal"

    return display_list

draw (Refactoring...)

    def draw(self):
        for x, y, c, font in self.display_list:
            if y > self.scroll + HEIGHT:
                continue
            if y + VSTEP < self.scroll:
                continue
            self.canvas.create_text(x, y - self.scroll, text=c, font=font, anchor="nw")

3.5 레이아웃 객체

layout to Layout (Refactoring...)

class Layout:
    def __init__(self, tokens):
        self.display_list = []
        self.cursor_x = HSTEP
        self.cursor_y = VSTEP
        self.weight = "normal"
        self.style = "roman"
        
        for tok in tokens:
            self.token(tok)

layout to Layout (Refactoring...)

class Layout:
    def __init__(self, tokens):

    def token(self, tok):
        if isinstance(tok, Text):
            for word in tok.text.split():
                self.word(word)
        elif tok.tag == "i":
            self.style = "italic"
        elif tok.tag == "/i":
            self.style = "roman"
        elif tok.tag == "b":
            self.weight = "bold"
        elif tok.tag == "/b":
            self.weight = "normal"

layout to Layout (Refactoring...)

class Layout:
    def __init__(self, tokens):

    def token(self, tok):

    def word(self, word):
        font = tkinter.font.Font(
            size=16,
            weight=self.weight,
            slant=self.style
        )
        w = font.measure(word)
        self.display_list.append(
            (self.cursor_x, self.cursor_y, word, font)
        )
        self.cursor_x += w + font.measure(" ")
        if self.cursor_x + w > WIDTH - HSTEP:
            self.cursor_y += font.metrics("linespace") * 1.25
            self.cursor_x = HSTEP

Browser.load (Refactoring...)

class Browser:
    #

    def load(self, url):
        body = url.request()
        tokens = lex(body)
        self.display_list = Layout(tokens).display_list
        self.draw()

self.size (Refactoring...)

class Layout:
    def __init__(self, tokens):
        self.display_list = []
        self.cursor_x = HSTEP
        self.cursor_y = VSTEP
        self.weight = "normal"
        self.style = "roman"
        self.size = 12        #
        for tok in tokens:
            self.token(tok)

self.size (Refactoring...)

class Layout:
    def token(self, tok):
        if isinstance(tok, Text):
            for word in tok.text.split():
                self.word(word)
        elif tok.tag == "i":
            self.style = "italic"
        elif tok.tag == "/i":
            self.style = "roman"
        elif tok.tag == "b":
            self.weight = "bold"
        elif tok.tag == "/b":
            self.weight = "normal"
        elif tok.tag == "small":  #
            self.size -= 2        #
        elif tok.tag == "/small": #
            self.size += 2        #
        elif tok.tag == "big":    #
            self.size += 4        #
        elif tok.tag == "/big":   #
            self.size -= 4        #

self.size (Refactoring...)

class Layout:
    def word(self, word):
        font = tkinter.font.Font(
            size=self.size,          #
            weight=self.weight,
            slant=self.style
        )
        w = font.measure(word)
        self.display_list.append(
            (self.cursor_x, self.cursor_y, word, font)
        )
        self.cursor_x += w + font.measure(" ")
        if self.cursor_x + w > WIDTH - HSTEP:
            self.cursor_y += font.metrics("linespace") * 1.25
            self.cursor_x = HSTEP

3.6 다양한 크기의 텍스트

self.line

class Layout:
    def __init__(self, tokens):
        self.display_list = []
        self.cursor_x = HSTEP
        self.cursor_y = VSTEP
        self.weight = "normal"
        self.style = "roman"
        self.size = 12
        self.line = []         #
        for tok in tokens:
            self.token(tok)
        self.flush()

self.line

class Layout:
    def __init__(self, tokens):
        self.display_list = []
        self.cursor_x = HSTEP
        self.cursor_y = VSTEP
        self.weight = "normal"
        self.style = "roman"
        self.size = 12
        self.line = []         #
        for tok in tokens:
            self.token(tok)
        self.flush()

self.line

class Layout:
    def word(self, word):
        font = tkinter.font.Font(
            size=self.size,
            weight=self.weight,
            slant=self.style
        )
        w = font.measure(word)
        if self.cursor_x + w > WIDTH - HSTEP:            #
            self.flush()                                 #
        self.line.append((self.cursor_x, word, font))    #
        self.cursor_x += w + font.measure(" ")

def flush

  • 기준선을 따라 단어들을 정렬

  • 디스플레이 리스트에 모든 단어들을 추가

  • cursor_x 와 cursor_y 필드를 업데이트

def flush

    def flush(self):
        if not self.line: return
        metrics = [font.metrics() for x, word, font in self.line]
        max_ascent = max([metric["ascent"] for metric in metrics])
        baseline = self.cursor_y + 1.25 * max_ascent
        for x, word, font in self.line:
            y = baseline - font.metrics("ascent")
            self.display_list.append((x, y, word, font))
        max_descent = max([metric["descent"] for metric in metrics])
        self.cursor_y = baseline + 1.25 * max_descent
        self.cursor_x = HSTEP
        self.line = []

def token

    def token(self, tok):
        #
        elif tok.tag == "br":
            self.flush()
        elif tok.tag == "/p":
            self.flush()
            self.cursor_y += VSTEP

3.7 폰트 캐싱

get_font

  • 폰트를 글로벌 캐싱

FONTS = {}

def get_font(size, weight, style):
    key = (size, weight, style)
    if key not in FONTS:
        font = tkinter.font.Font(size=size, weight=weight,
            slant=style)
        label = tkinter.Label(font=font)
        FONTS[key] = (font, label)
    return FONTS[key][0]

using get_font

    def word(self, word):
        font = get_font(self.size, self.weight, self.style)    #
        w = font.measure(word)
        if self.cursor_x + w > WIDTH - HSTEP:
            self.flush()
        self.line.append((self.cursor_x, word, font))
        self.cursor_x += w + font.measure(" ")

챕터3 텍스트 포맷팅하기

By Woongjae Lee

챕터3 텍스트 포맷팅하기

  • 28