Ch5 페이지 레이아웃

Feb 26th, 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

이 웅재

5.1 레이아웃 트리

layout 객체를 Browser 의 필드로

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

class Browser:
    def load(self, url):
        body = url.request()
        nodes = HTMLParser(body).parse()
        self.display_list = Layout(nodes).display_list
        self.draw()
class Browser:
    def load(self, url):
        body = url.request()
        nodes = HTMLParser(body).parse()
        self.document = Layout(nodes)                     #
        self.document.layout()                            #
        self.display_list = self.document.display_list    #
        self.draw()

Layout 생성자와 layout 으로 분리

  • 레이아웃 객체를 생성하는 것과 실제로 레이아웃을 수행하는 것은 별개의 과정

class Layout:
    def __init__(self, node):
        self.node = node

Layout 에 자식 포인터와 부모 포인터 추가

  • 트리로 만들려면, 자식과 부모 이전 형제 포인터를 저장

  • 나중에 크기와 위치를 계산하는데 유용하게 사용

class Layout:
    def __init__(self, node, parent, previous):
        self.node = node
        self.parent = parent
        self.previous = previous
        self.children = []

DocumentLayout

  • 레이아웃 트리의 루트 역할을 할 두번째 레이아웃 객체

class DocumentLayout:
    def __init__(self, node):
        self.node = node
        self.parent = None
        self.children = []

    def layout(self):
        child = Layout(self.node, self, None)
        self.children.append(child)
        child.layout()

Layout to BlockLayout

  • 모호함을 줄이기

class BlockLayout:
    def __init__(self, node, parent, previous):
        self.node = node
        self.parent = parent
        self.previous = previous
        self.children = []

    def layout(self):
        self.display_list = []
        self.cursor_x = HSTEP
        self.cursor_y = VSTEP
        self.weight = "normal"
        self.style = "roman"
        self.size = 12
        self.line = []
        self.recurse(self.node)
        self.flush()

Using BlockLayout in DocumentLayout

class DocumentLayout:
    def __init__(self, node):
        self.node = node
        self.parent = None
        self.children = []

    def layout(self):
        child = BlockLayout(self.node, self, None)
        self.children.append(child)
        child.layout()

Using DocumentLayout in Browser.load

class Browser:
    def load(self, url):
        body = url.request()
        nodes = HTMLParser(body).parse()
        self.document = DocumentLayout(nodes)
        self.document.layout()
        self.display_list = self.document.display_list
        self.draw()

5.2 블록 레이아웃

BlockLayout

BlockLayout.layout_mode()

  • 텍스트나 <b> 같은 텍스트 관련 태그인지

  • <p> 나 <h1> 같은 블록인지

BLOCK_ELEMENTS = [
    "html", "body", "article", "section", "nav", "aside",
    "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
    "footer", "address", "p", "hr", "pre", "blockquote",
    "ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
    "figcaption", "main", "div", "table", "form", "fieldset",
    "legend", "details", "summary"
]

class BlockLayout:
    def layout_mode(self):
        if isinstance(self.node, Text):
            return "inline"
        elif any([isinstance(child, Element) and child.tag in BLOCK_ELEMENTS for child in self.node.children]):
            return "block"
        elif self.node.children:
            return "inline"
        else:
            return "block"

BlockLayout.layout

class BlockLayout:
    def layout(self):
        mode = self.layout_mode()
        if mode == "block":
            previous = None
            for child in self.node.children:
                next = BlockLayout(child, self, previous)
                self.children.append(next)
                previous = next
        else:
            self.cursor_x = 0
            self.cursor_y = 0
            self.weight = "normal"
            self.style = "roman"
            self.size = 12

            self.line = []
            self.recurse(self.node)
            self.flush()
class BlockLayout:
    def layout(self):
        # ...
            
        for child in self.children:
            child.layout()

BlockLayout.layout_mode()

  • 자식도 재귀적으로 layout 수행

5.3 크기와 위치

x, y, width, height

  • 각 레이아웃 객체마다 크기와 위치를 독립적으로 계산

class BlockLayout:
    def __init__(self, node, parent, previous):
        self.node = node
        self.parent = parent
        self.previous = previous
        self.children = []
        self.x = None
        self.y = None
        self.width = None
        self.height = None

cursor_x, cursor_y

  • 페이지 상의 절대 위치를 표시하는 대신 BlockLayout의 x 와 y 에 대한 상대 위치로 처리

class BlockLayout:
    def layout(self):
        mode = self.layout_mode()
        if mode == "block":
            # ...
        else:
            self.cursor_x = 0          #
            self.cursor_y = 0          #
            self.weight = "normal"
            self.style = "roman"
            self.size = 12

            self.line = []
            self.recurse(self.node)
            self.flush()

flush

  • 상대적인 값이므로 flush 에서 디스플레이 리스트를 계산할 때 블록의 x 와 y 를 더해줘야 함

class BlockLayout:
    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 rel_x, word, font in self.line:                             #
            x = self.x + rel_x                                          #
            y = self.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 = 0                                               #
        self.line = []

word

  • cursor_x 가 이 블록의 width 에 도달하면 줄바꿈을 수행

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

x, y, width

  • 각 객체는 부모의 왼쪽 가장자리에서 시작하여 부모 엘리먼트를 채워감

  • 레아아웃 객체의 세로 위치는 이전 형제 노드가 있는지에 따라 달라짐

    • 있으면 형제 객체의 바로 뒤에서 시작

    • 없으면 부모 객체의 상단

class BlockLayout:
    def layout(self):
        self.x = self.parent.x
        self.width = self.parent.width

        if self.previous:
            self.y = self.previous.y + self.previous.height
        else:
            self.y = self.parent.y

height

  • 높이는 자식들의 높이를 합한 값

  • 텍스트를 담고 있는 BlockLayout 은 자식 블록이 없음

class BlockLayout:
    def layout(self):
        # ...
        
        if mode == "block":
            self.height = sum([
                child.height for child in self.children])
        else:
            self.height = self.cursor_y

DocumentLayout.layout

  • 문서가 항상 같은 위치에서 시작하기 때문에 매우 간단함

class DocumentLayout:
    def layout(self):
        child = BlockLayout(self.node, self, None)
        self.children.append(child)

        self.width = WIDTH - 2*HSTEP
        self.x = HSTEP
        self.y = VSTEP
        child.layout()
        self.height = child.height

5.4 재귀 페인팅

paint_tree

  • 각 레이아웃 객체에 paint 함수를 추가하여 해당 객체의 디스플레이 리스트를 반환

  • 모든 레이아웃 객체에 대하여 paint 를 재귀적으로 호출

def paint_tree(layout_object, display_list):
    display_list.extend(layout_object.paint())

    for child in layout_object.children:
        paint_tree(child, display_list)

DocumentLayout.paint

  • 페인트할 것이 아무것도 없음

class DocumentLayout:
    def paint(self):
        return []

BlockLayout.paint

  • recurse 와 flush 에서 계산한 display_list 를 복사

class BlockLayout:
    def paint(self):
        return self.display_list

Browser.load

class Browser:
    def load(self, url):
        body = url.request()
        self.nodes = HTMLParser(body).parse()
        self.document = DocumentLayout(self.nodes)
        self.document.layout()
        self.display_list = []
        paint_tree(self.document, self.display_list)
        self.draw()

5.5 배경 그리기

텍스트 그리기

class DrawText:
    def __init__(self, x1, y1, text, font):
        self.top = y1
        self.left = x1
        self.text = text
        self.font = font

        self.bottom = y1 + font.metrics("linespace")

    def execute(self, scroll, canvas):
        canvas.create_text(
            self.left, self.top - scroll,
            text=self.text,
            font=self.font,
            anchor='nw'
        )

사각형 그리기

class DrawRect:
    def __init__(self, x1, y1, x2, y2, color):
        self.top = y1
        self.left = x1
        self.bottom = y2
        self.right = x2
        self.color = color

    def execute(self, scroll, canvas):
        canvas.create_rectangle(
            self.left, self.top - scroll,
            self.right, self.bottom - scroll,
            width=0,
            fill=self.color
        )

실제로 그리는 객체 추가

  • 인라인 모드일때 한하여 각 단어를 그리는 Draw 객체를 추가

  • pre 태그에 회색 배경을 추가

class BlockLayout:
    def paint(self):
        cmds = []
        if isinstance(self.node, Element) and self.node.tag == "pre":
            x2, y2 = self.x + self.width, self.y + self.height
            rect = DrawRect(self.x, self.y, x2, y2, "gray")
            cmds.append(rect)

        if self.layout_mode() == "inline":
            for x, y, word, font in self.display_list:
                cmds.append(DrawText(x, y, word, font))
        return cmds

Using excute in draw

class Browser:
    def draw(self):
        self.canvas.delete("all")
        for cmd in self.display_list:
            if cmd.top > self.scroll + HEIGHT: continue
            if cmd.bottom < self.scroll: continue
            cmd.execute(self.scroll, self.canvas)
            
     # 전체 크기를 알 수 있기 때문에, 크기를 넘으면 스크롤 방지
     def scrolldown(self, e):
        max_y = max(self.document.height + 2*VSTEP - HEIGHT, 0)
        self.scroll = min(self.scroll + SCROLL_STEP, max_y)
        self.draw()

챕터5 페이지 레이아웃

By Woongjae Lee

챕터5 페이지 레이아웃

  • 16