Jaeyeol Lee
kodingwarrior.hackers.pub.ap.brid.gy
Jaeyeol Lee
@kodingwarrior.hackers.pub.ap.brid.gy
Neovim Super villain. 풀스택 엔지니어 내지는 프로덕트 엔지니어라고 스스로를 소개하지만 사실상 잡부를 담당하는 사람. CLI 도구를 만드는 것에 관심이 많습니다 […]

🌉 bridged from https://hackers.pub/@kodingwarrior on the fediverse by https://fed.brid.gy/
vimrc2025 행사에서 뿌릴 스티커 도착함.
November 13, 2025 at 4:27 AM
두근두근 (명목상) 수습생활 6일차...

일이 재밌게 챌린징하고 도파민 터진다....
November 11, 2025 at 6:49 AM
social.silicon.moe
November 10, 2025 at 3:05 AM
@sigridjineth 안녕하세요! 반갑습니다!
November 9, 2025 at 1:51 AM
브라우저 스터디 기록 (3)
Note 이 글은 Web Browser Engineering 을 독학하면서 시도했던 것들을 의식의 흐름대로 남긴 흔적입니다. TL;DR - Chapter 3 연습문제 풀이를 보고 싶다면 여기서 확인할 수 있다. Chapter 3는 좀 빡세다는 느낌이 들었다. 이 글을 작성하는 시점에는 Chapter 4까지 이미 끝내놓은 상태이긴 하지만, 런타임 환경마다 각자 다르게 동작하는 폰트 렌더링이라던가 후술할 **일부 연습문제** 가 굉장히 골치가 아팠던 것으로 기억한다. 그만큼 텍스트 레이아웃이라는 심연히 굉장히 골때리다는 것이고, "폰트렌더링과 씨름했던 사람들은 어떤 싸움을 해온 것인가..." 라는 생각이 들곤 했다. Chapter 2에서는 글자를 하나씩 하나씩 고정된 간격으로 렌더링했었다. 고정폭 글자 기준으로는 이렇게 해도 문제가 없긴 하지만, 가변폭 글자 기준으로는 가독성이 굉장히 떨어진다. 예를 들자면, a 이라는 글자의 폭이 다르고, l 이라는 글자의 폭이 다르다. 그리고, 단어를 구성하는 각 글자의 폭을 합친 값과 단어 자체의 폭도 값이 다르다. 그래서, 텍스트 자체를 렌더링할때는 단어 단위로 렌더링하는 편이 좀 더 정밀하다고 볼 수 있다. 그리고, 여러가지 폰트가 섞여있는 상황에서 글자가 올바르게 배치되도록 하기 위해서 baseline, ascent, descent라는 개념이 있다. 이 세 가지는 말하자면 **글자가 어디에 ‘앉는지’와 ‘얼마나 위아래로 뻗는지’를 결정하는 기준선들** 이다. * **baseline** 은 모든 글자가 공통으로 맞춰야 하는 “바닥선”이다. 대부분의 글자는 이 선 위에 앉아 있다. * **ascent** 는 글자가 위로 얼마나 뻗는지를 나타내는 값이다. 예를 들어 “h”나 “b” 같은 글자는 베이스라인 위로 높게 올라가므로 ascent 값이 크다. * **descent** 는 반대로 글자가 아래로 얼마나 내려가는지를 나타낸다. “p”나 “g”, “y”처럼 꼬리가 밑으로 내려가는 글자들이 descent를 가진다. 이걸 눈으로 보면 아주 단순해 보이지만, 렌더러 입장에서는 꽤 골치 아픈 개념이다. 왜냐면, 폰트마다 ascent와 descent의 비율이 다르고, 심지어 같은 폰트라도 굵기(weight)나 스타일(italic)에 따라 기준선이 조금씩 달라지기 때문이다. 그래서 브라우저는 단순히 “텍스트를 그린다”기보다는, 각 글자가 가진 메트릭 값을 모두 고려해서 줄 전체가 시각적으로 균형 잡히도록 맞춰야 한다. 우리가 평소에 아무렇지 않게 읽던 한 줄의 텍스트가 사실 꽤 정교한 계산 위에서 표시된다는 걸 알 수 있다. 그냥 “글자가 줄 맞춰진다”는 게 아니라, 각 문자의 메트릭 값이 조합되어 시각적으로 균형을 이루도록 배치되는 것이다. 이렇게 각 글자의 형태와 위치를 조정해 실제 표시될 글자(glyph)로 변환하는 일련의 과정을 **shaping** 이라고 한다. Chapter 3에서는 단어 단위로 렌더링하기 전에 Shaping 하는 과정을 소개한다. ## 폰트 렌더링이라는 심연 이 교재를 Linux/macOS 각각 다른 기기를 번갈아가면서 실습하는 사람은 이미 실감했을 것인데, font 글자의 가로폭을 계산하는 과정이 굉장히 느리다. 사실 이것은 tkinter 자체의 구현이 문제인데, tkinter에서 font.measure 함수를 호출할때 런타임마다 동작하는 방식이 다르다. macOS 구현체는 CoreText(소스코드는 비공개)라는 라이브러리에 내장된 측정 함수를 그대로 가져다 쓰기 때문에, 측정이 거의 네이티브라고 볼 수 있을 정도로 굉장히 빠르다. 하지만, Linux는....? 폰트 정보를 가져오고 측정함수를 호출하기 위해 X 서버를 거쳐야 하기 때문에, 성능이 몇배는 차이가 난다. 여기서 큰 차이를 만들어낸다. 실습이라는게 되고 안되고가 나뉠 정도로. Linux 환경에서는 화면을 매번 렌더링할 때마다 너무 느려서 실습을 포기할 정도가 될 수 밖에 없는데, 이건 정상적인 실습환경이라고 볼 수 없다. 그럼에도 불구하고, 방법은 있다. 바로, 브라우저에서 처음 렌더링하는 시점에, (폰트 패밀리, 폰트 크기, 폰트 스타일) 을 키로 활용하여서 각각에 매칭되는 가변폭 글자 각각의 길이를 미리 측정해서 어딘가에다가 캐싱해두는 것이다. 렌더링 루프에서 실시간으로 `font.measure()`를 호출하기 전에 미리 캐싱해두는 것이다. 그리고 단어의 길이를 측정할때는 단어를 구성하는 각 문자의 가로폭을 합치는 식으로 계산하면 된다. 예시 코드는 아래와 같다. from dataclasses import dataclass, field import tkinter.font @dataclass class FontMeasurer: cache: dict[tuple[float, str, str, str], dict[str, float]] = field(default_factory=dict) fixed_cjk_width: dict[tuple[float, str, str, str], float] = field(default_factory=dict) def _font_key(self, font: tkinter.font.Font): return ( font.cget("size"), font.cget("weight"), font.cget("slant"), font.cget("family"), ) def _is_cjk(self, ch: str) -> bool: code = ord(ch) return ( 0x4E00 <= code <= 0x9FFF or # Kanji 0xAC00 <= code <= 0xD7A3 or # Hangul 0x3040 <= code <= 0x30FF or # hiragana, katakana 0x31F0 <= code <= 0x31FF or # katakana extension 0x3400 <= code <= 0x4DBF or # CJK extension A 0xFF00 <= code <= 0xFF60 # Fullwidth roman characters and halfwidth katakana ) def _prefetch_ascii_widths(self, font: tkinter.font.Font, cache: dict[str, float]): """Prefetch widths for common ASCII characters.""" ascii_chars = ( [chr(i) for i in range(32, 127)] # printable ASCII ) for ch in ascii_chars: if ch not in cache: cache[ch] = font.measure(ch) def measure(self, font: tkinter.font.Font, text: str) -> float: if not text: return 0.0 key = self._font_key(font) cache = self.cache.setdefault(key, {}) # Prefetch widths for common ASCII characters (only once) if " " not in cache: self._prefetch_ascii_widths(font, cache) # Initialize fixed CJK width if not already done if key not in self.fixed_cjk_width: self.fixed_cjk_width[key] = font.measure("가") result = cache.get(text) if result is not None: return result # Single character case if len(text) == 1: if text not in cache: cache[text] = ( self.fixed_cjk_width[key] if self._is_cjk(text) else font.measure(text) ) return cache[text] # Multi-character case width = 0.0 for ch in text: w = cache.get(ch) if w is None: w = ( self.fixed_cjk_width[key] if self._is_cjk(ch) else font.measure(ch) ) cache[ch] = w width += w cache[text] = width return width font_measurer = FontMeasurer() 이는 인메모리에 접근해서 계산하는 것이기 때문에 X 서버를 거치는 것보다 굉장히 빠르다. 그리고, 일부 단어는 빈번하게 등장할 수 있기 때문에 렌더링하는 성능은 더 올라갈 수 밖에 없다. 물론 이것은 근본적인 해결책은 아닐 수 있다. 아까 언급했듯이, 가변폭 글자 각각의 가로폭을 합치는 것과 단어 자체의 엄밀한 가로폭은 다르다. 하지만, 브라우저가 동작하는걸 눈으로 확인하기도 어려운 상황에서 자연스러운 속도로 렌더링되게 한다면 이는 감당이 가능한 비용이다. 이 프로젝트의 목표가 “브라우저의 동작을 눈으로 직접 확인해보는 것”이라는 점을 고려하면, 속도와 정밀도 사이의 이 정도 타협은 충분히 감당 가능한 수준이다. 참고로, macOS는 위와 같이 캐싱을 굳이 하지 않아도 빠르다(.....) 실제 브라우저들은 이 문제를 훨씬 더 정교하게 다룬다. 예를 들어 크로미움(Chromium)은 `HarfBuzz` 라는 오픈소스 라이브러리를 내장해, 플랫폼에 상관없이 동일한 shaping과 측정 알고리즘을 사용한다. 덕분에 Linux에서도 macOS와 거의 같은 속도로 텍스트를 렌더링할 수 있다 ## 연습문제 풀이 3.1(중앙정렬)의 경우, 2.5(ltr 지원)에서 가로 넓이를 계산했었다면 어렵지 않게 풀 수 있다. 3.3(`<abbr>` 태그 지원)는 그냥 문제의 요구사항대로 upper case로 만들어주는 것만 신경써주면 된다. 3.4(soft-hyphen 지원) 의 경우, `current_width + w`가 `screen_width`를 넘어서는 시점에 어느 부분부터 자를지 계산만 잘해주고 다음 행으로 개행시키면 된다. ### 연습문제 3.2 : `<sup>`, `<sub>` 태그 지원 `<sup>`와 `<sub>`는 단순히 글자 크기를 조정하는 태그가 아니라, 줄의 기준선(baseline) 자체를 움직이는 태그다. 같은 줄 안에서도 글자가 서로 다른 높이에 놓일 수 있기 때문에, 단순히 y좌표를 더하거나 빼는 식으로 처리하면 줄 전체가 어긋나 버린다. 이번 구현에서는 이러한 문제를 해결하기 위해 기준선의 변화를 **스택(stack)** 으로 관리했다. `BufferLine`의 `context_stack`은 `<sup>`나 `<sub>`가 열릴 때마다 새로운 기준선 정보를 push하고, 닫힐 때 pop하여 복원하는 방식으로 작동한다. `<sup>`은 현재 폰트의 `ascent`를 기준으로 약 1/4만큼 위로, `<sub>`은 `descent`를 기준으로 약 1/4만큼 아래로 이동하며, 이렇게 쌓이는 컨텍스트 덕분에 첨자가 중첩되더라도 각 글자의 상대적인 높이를 정확히 계산할 수 있다. 이 구조의 장점은 첨자 내부에서도 폰트 관련 태그를 자유롭게 섞어 쓸 수 있다는 점이다. 예를 들어 `<sup>` 안에서 `<big>`, `<small>`, `<b>`(또는 `<strong>`), `<i>` 같은 태그가 들어와도 기준선이 흔들리지 않는다. 폰트 크기나 굵기, 스타일 변경은 `VerticalAlignContext` 안에서만 영향을 주기 때문에, 텍스트의 세로 위치는 여전히 안정적으로 유지된다. 실제로 `<sup><big>Text</big></sup>`처럼 크기를 키우거나 줄이더라도, 글자는 원래의 기준선 위에서 자연스럽게 정렬된다. 이 덕분에 폰트 스타일과 세로 정렬이 서로 간섭하지 않으면서도, 브라우저와 비슷한 안정적인 렌더링 결과를 얻을 수 있다. 줄이 끝날 때(`flush()`)는 스택에 쌓인 글자들의 상대적인 y좌표를 바탕으로 줄 전체의 높이를 계산한다. 흥미로운 점은, 기준선 컨텍스트가 줄 경계에서 바로 사라지지 않는다는 것이다. `<sup>`가 한 줄에서 열리고 다음 줄에서 닫히는 경우에도 이전의 기준선 상태가 그대로 유지되어, 여러 줄에 걸친 첨자 구조가 자연스럽게 이어진다. `BufferLine`은 스택이 완전히 비었을 때만 기준선을 0으로 복원하기 때문에, 각 줄의 높이는 독립적으로 계산되면서도 전체 문맥은 유지된다. 이런 구조 덕분에 깊은 중첩이나 복잡한 폰트 조합이 등장하더라도, 텍스트 레이아웃은 줄과 줄 사이에서 끊기지 않고 매끄럽게 이어진다. 예시 코드는 아래와 같다. class BufferLine: words: list[tuple[float, float, str, tkinter.font.Font]] = field(default_factory=list) baseline: float = 0.0 current_baseline: float = 0.0 context_stack: list[VerticalAlignContext] = field(default_factory=list) def clear(self): self.words.clear() def is_empty(self) -> bool: return len(self.words) == 0 @property def previous_baseline(self) -> float: if not self.context_stack: return 0 return self.context_stack[-1].relative_baseline_y def add_word(self, *, x: float, font: tkinter.font.Font, word: str): ... def calculate_bounds(self) -> tuple[float, float]: ... def add_context(self, context: VerticalAlignContext): self.context_stack.append(context) self.current_baseline = context.relative_baseline_y def pop_context(self) -> VerticalAlignContext: if not self.context_stack: raise RuntimeError("No context to pop") context = self.context_stack.pop() if self.context_stack: self.current_baseline = self.context_stack[-1].relative_baseline_y else: self.current_baseline = 0.0 return context class Layout: def handle_tag(tag: str): ... elif tag == 'sup': current_font = self.get_font(self.size, self.font_weight, self.style) metrics = current_font.metrics() ascent = metrics["ascent"] baseline_y = self.buffer_line.previous_baseline - int(ascent * 0.25) self.buffer_line.add_context( VerticalAlignContext( restore_size=self.size, relative_baseline_y=baseline_y, weight=self.font_weight, style=self.style ) ) previous_size = self.size self.size = int(previous_size * 0.75) elif tag == "sub": current_font = self.get_font(self.size, self.font_weight, self.style) metrics = current_font.metrics() descent = metrics["descent"] baseline_y = self.buffer_line.previous_baseline + int(descent * 0.25) self.buffer_line.add_context( VerticalAlignContext( restore_size=self.size, relative_baseline_y=baseline_y, weight=self.font_weight, style=self.style ) ) previous_size = self.size self.size = int(previous_size * 0.75) elif tag == '/sup': context = self.buffer_line.pop_context() self.size = int(context.restore_size) elif tag == '/sub': context = self.buffer_line.pop_context() self.size = int(context.restore_size) ### 연습문제 3.5 : `<pre>` 태그 지원 요구사항은 오히려 3.2에서 `<sup>` / `<sub>` 지원하는 것보다 훨씬 간결하다. `<pre>` 태그는 공백과 개행을 그대로 유지해야 하는데, 일반 텍스트 렌더링처럼 단어 단위로 쪼개거나 공백을 합치면 이 특성이 깨진다. 그래서 `<pre>`가 열리면 `pre_tag_depth`를 1 증가시켜 “pre 텍스트 모드”임을 표시하고, 이 상태에서는 단어가 아닌 **라인 단위** 로 텍스트를 처리하도록 했다. `splitlines(keepends=True)`로 줄바꿈 문자를 포함해 순회하며 각 줄 끝에서 `flush()`를 호출하면, 원본의 줄 구조와 들여쓰기가 그대로 보존된다. 닫히는 태그를 만나면 `pre_tag_depth`를 1 감소시켜 다시 일반 렌더링 모드로 복귀한다. 크게 보면 이 정도 차이만 있다. if self.pre_tag_depth > 0: # pre 태그 안에 들어갔을 때 처리하는 방식. (단어 단위가 아닌 줄 단위로 처리한다.) lines = tree.text.splitlines(keepends=True) for line in lines: self.process_word(line) if line.endswith('\n'): self.flush() else: # 기존의 처리 방식 for word in tree.text.split(): self.process_word( word if not self.small_caps else word.upper() )
hackers.pub
November 9, 2025 at 1:35 AM
오늘도... 브라우저 스터디 챕터 3 풀이를 올리고 말 것이다,......
November 8, 2025 at 7:23 AM
방금 10명 정도 가입하신듯...!!
November 8, 2025 at 2:20 AM
Reposted by Jaeyeol Lee
오늘 @fossforall 컨퍼런스 2025에서 發表(발표)한 〈야크 셰이빙: 새로운 오픈 소스의 원동력〉의 슬라이드를 共有(공유)합니다! 들어주신 분들 모두 感謝(감사)합니다!
야크 셰이빙: 새로운 오픈 소스의 원동력
야크 셰이빙: 새로운 오픈 소스의 원동력 불편함에서 시작된 2년간의 여정 洪民憙 (홍민희) hongminhee.org 커뮤니티와 기여 문화 Scan for English slides! →
docs.google.com
November 8, 2025 at 2:18 AM
최근에 하게 된 프로젝트가 있는데, 4일차 찍은 지금 도파민이 너무 터진다.....
November 7, 2025 at 10:23 AM
@mimul 안녕하세요! 반갑습니다!
November 6, 2025 at 3:49 AM
hackers.pub
November 5, 2025 at 8:59 AM
@nyblue 안녕하세요! 반갑습니다!
November 5, 2025 at 4:47 AM
모노리포 구조는 어쩔 수 없이 Zed를 쓰게 되는 것 같은데, 그 외에는 Neovim으로 개발하는게 제일 편한 듯
November 5, 2025 at 2:00 AM
LSP 플러그인이 안 깔려있는 상태에서 랭귀지 서버 세팅을 완료했는데... Neovim 이 녀석들 자체 스펙만으로도 계속 좋아지고 있긴 하구만....
November 4, 2025 at 1:21 PM
브라우저 구현 스터디하면서 파서 구현 중인데, 예외케이스 처리하고 상태 관리로 서커스해야할게 너무 많다. DFA 추상화라도 해야하나 싶어서 다른 구현체 봤더니 실제로 그렇게 하고 있다.

Chromium(Blink) : https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/parser/html_tokenizer.cc;l=1677 관련 표준 : https://html.spec.whatwg.org#tokenisati […]
Original post on hackers.pub
hackers.pub
November 3, 2025 at 6:56 AM
브라우저 스터디 최대한 미리 진도 빼놔야겠다. 머쓱....
November 3, 2025 at 6:06 AM
초안이고, 아직은 완성된 글이 아님. 약속이 있는 관계로 일단 중간 세이브....
브라우저 스터디 기록 (2)
Note 이 글은 Web Browser Engineering 을 독학하면서 시도했던 것들을 의식의 흐름대로 남긴 흔적입니다. TL;DR - Chapter 2 연습문제 풀이를 보고 싶다면 여기서 확인할 수 있다. Chapter 2는 전반적으로 쉬어가는 챕터라는 느낌이 강했다. Chapter 2의 내용을 요약하자면, "브라우저 주소 입력창에 g를 타이핑했을 때 일어나는 일들을 서술하시오"에서 "g를 타이핑했을 때" 입력을 감지하는 과정 그리고 응답을 받았을때 HTML을 화면에 그리는 과정이 어떻게 일어나는지를 서술하는 것에 가깝다. 마우스나 키보드 같은 입출력 장치에서 신호가 발생하면, CPU는 이를 감지하고 커널에 **인터럽트 요청(IRQ, Interrupt Request)** 을 전달한다. 커널은 이 요청을 처리하여 필요한 경우 브라우저 프로그램에 이벤트를 전달하고, 브라우저는 그 신호를 바탕으로 소켓을 통해 인터넷상의 서버에 요청을 보낸다. 서버로부터 응답이 돌아오면, 커널은 이를 다시 브라우저로 전달하고, 브라우저는 받은 데이터를 해석해 그래픽 시스템을 통해 화면에 렌더링한다. 이렇게 해서 우리는 화면 위의 x, y 좌표에 정밀하게 계산되어 그려진 브라우저 화면을 보게 된다. 여기서 핵심적인 요소는, 이벤트 루프를 통해 이벤트를 입력을 감지하고 화면에 그리는 일련의 과정인데, 이번 챕터에서는 간단하게 텍스트를 하나씩 하나씩 화면에 찍어내는 정도로만 그치고 있다. (어떤 운영체제를 쓰느냐에 따라 다를 수는 있겠지만) 브라우저를 구현하려면 Gtk/ Qt 같은 GUI 툴킷의 도움이 필요한데, 챕터 9까지는 간단한 구현을 위해 Tcl/Tk를 쓰고 있다. 그 이후에는 Skia/SDL로 바뀌는 것 같다. ## 연습문제 풀이 2.1는 그냥 개행을 구현하는 기능이고, 2.2/2.3/2.4는 그냥 스크롤 기능을 구현했다면 어렵지 않게 구현이 가능한 기능이니 그냥 넘어가면 될 것 같다. 2.6은 URL 파싱이 실패했을 때, about:blank로 fallback하고 about:blank일 때는 빈 화면이 띄워지게 하면 되니까 아주 간단하다. ### 연습문제 2.5 : Emoji 지원 ### 연습문제 2.7 : RTL 지원 이번에는 좀 트릭이 필요하다. 브라우저는 대부분의 화면에서 텍스트 요소 배치라던가 화면 요소 배치를 왼쪽에서 오른쪽으로 배치(LTR)하는 것이 보편적인데, 오른쪽에서 왼쪽으로 배치(RTL)하는 웹 페이지도 종종 있다. 오른쪽에서 왼쪽으로 필기하는 문화권(아랍어, 히브리어 등등)에서 특히 RTL 지원이 필요하다. 이는 HTML 속성으로도 ltr, rtl 여부를 지정할 수 있다. 물론.... 이 기능을 지원하는 순간부터 후속 챕터 작업할 때, 하위호환성 지원하느라 애를 먹을 수도 있다. 나같은 경우에는 히브리어를 기준으로 테스트했는데, 텍스트를 그대로 가져다 쓸 때는 정상적인 순서로 출력이 되는 것을 볼 수 있다. 하지만, 하나씩 하나씩 화면에 찍어내면 내가 알고 있는 그 문자 구성이 맞는지 의심이 들게 된다. 이를 처리하기 위해서는 RTL 언어를 어떻게 처리할 지에 대해서도 알고 있어야 하고, LTR 언어도 정상적인 순서로 출력되도록 처리하되, LTR 언어/RTL 언어 각각이 한 문장에서 올바른 순서로 출력이 되도록 해야 한다. 엄밀하게는 정규식으로 필터링해서 순서를 뒤집던가해서 하나씩 출력하게 하는 것도 고려해볼 수는 있다. 하지만.... 나는 그냥 다르게 접근했다. xy 좌표를 이미 display_list에 넣어버렸는데, 한 줄에 들어가는 텍스트를 출력하는거면 y 좌표 단위로 묶어서 아예 하나의 문자열로 모아서 출력하면 되지 않은가? RTL 문자를 어떻게 출력하는지는 어지간한 GUI 툴킷에서 따로 처리를 할 것이라는 믿음이 있었기에 그냥 RTL 순서 맞춰서 출력하는건 GUI 툴킷이 하도록 거인의 어깨에 올라탔다. lines: dict[int, list[tuple[int, str]]] = {} for x, y, c in drawable_characters: if y not in lines: lines[y] = [] lines[y].append((x, c)) for y, line_chars in lines.items(): total_width: int = len(line_chars) # 라인 전체 길이 text_segments: list[tuple[int | None, str]] = [] ... text_segments.append((x, word)) # x는 상대 좌표, word는 모아찍을 단어/문장 단위 각각의 문자를 어떻게 정상적으로 출력할지에 대해서 간단하게 살펴봤다. 그렇다면,어떻게 각 라인의 끝이 화면의 오른쪽에 딱 붙어서 출력되게 할 것인가? 이것도 굉장히 자명한 방법이 있는데, y 좌표를 기준으로 출력할 라인을 관리하게 했다면 지금 당장의 가정으로는 고정폭(HSTEP)을 기준으로 라인의 길이를 잴 수 있다. 그렇다면, 라인이 화면에 그려지는 시작점(start_x)를 화면 맨 오른쪽 좌표(WIDTH)에서 라인의 길이를 빼서 계산하고, start_x 중심으로 LTR 기준 화면이 그려지는 상대적인 좌표(x)를 더해서 그려내면 된다. 간단하다. Emoji 출력을 어떻게 할 지에 따라서는, 오히려 간단하게 해결할 수 있는데... display_list에 글자를 하나씩 하나씩 집어넣을때 이모지를 어느 위치에 출력할지에 대해서는 이미 좌표를 지정한 바가 있다. 그렇기 때문에, emoji가 나타나는 위치는 공백으로 치환하되, emoji를 그려야 하는 좌표를 emoji 출력을 위한 다른 리스트에 넣어놓고 텍스트 라인을 화면에 그리는 부분 따로, 이모지를 화면에 그리는 부분 따로 분리를 했다.
hackers.pub
November 2, 2025 at 3:54 AM
브라우저 스터디 기록 (2)
Note 이 글은 Web Browser Engineering 을 독학하면서 시도했던 것들을 의식의 흐름대로 남긴 흔적입니다. TL;DR - Chapter 2 연습문제 풀이를 보고 싶다면 여기서 확인할 수 있다. Chapter 2는 전반적으로 쉬어가는 챕터라는 느낌이 강했다. Chapter 2의 내용을 요약하자면, "브라우저 주소 입력창에 g를 타이핑했을 때 일어나는 일들을 서술하시오"에서 "g를 타이핑했을 때" 입력을 감지하는 과정 그리고 응답을 받았을때 HTML을 화면에 그리는 과정이 어떻게 일어나는지를 서술하는 것에 가깝다. 마우스나 키보드 같은 입출력 장치에서 신호가 발생하면, CPU는 이를 감지하고 커널에 **인터럽트 요청(IRQ, Interrupt Request)** 을 전달한다. 커널은 이 요청을 처리하여 필요한 경우 브라우저 프로그램에 이벤트를 전달하고, 브라우저는 그 신호를 바탕으로 소켓을 통해 인터넷상의 서버에 요청을 보낸다. 서버로부터 응답이 돌아오면, 커널은 이를 다시 브라우저로 전달하고, 브라우저는 받은 데이터를 해석해 그래픽 시스템을 통해 화면에 렌더링한다. 이렇게 해서 우리는 화면 위의 x, y 좌표에 정밀하게 계산되어 그려진 브라우저 화면을 보게 된다. 여기서 핵심적인 요소는, 이벤트 루프를 통해 이벤트를 입력을 감지하고 화면에 그리는 일련의 과정인데, 이번 챕터에서는 간단하게 텍스트를 하나씩 하나씩 화면에 찍어내는 정도로만 그치고 있다. (어떤 운영체제를 쓰느냐에 따라 다를 수는 있겠지만) 브라우저를 구현하려면 Gtk/ Qt 같은 GUI 툴킷의 도움이 필요한데, 챕터 9까지는 간단한 구현을 위해 Tcl/Tk를 쓰고 있다. 그 이후에는 Skia/SDL로 바뀌는 것 같다. ## 연습문제 풀이 2.1는 그냥 개행을 구현하는 기능이고, 2.2/2.3/2.4는 그냥 스크롤 기능을 구현했다면 어렵지 않게 구현이 가능한 기능이니 그냥 넘어가면 될 것 같다. 2.6은 URL 파싱이 실패했을 때, about:blank로 fallback하고 about:blank일 때는 빈 화면이 띄워지게 하면 되니까 아주 간단하다. ### 연습문제 2.5 : Emoji 지원 ### 연습문제 2.7 : RTL 지원 이번에는 좀 트릭이 필요하다. 브라우저는 대부분의 화면에서 텍스트 요소 배치라던가 화면 요소 배치를 왼쪽에서 오른쪽으로 배치(LTR)하는 것이 보편적인데, 오른쪽에서 왼쪽으로 배치(RTL)하는 웹 페이지도 종종 있다. 오른쪽에서 왼쪽으로 필기하는 문화권(아랍어, 히브리어 등등)에서 특히 RTL 지원이 필요하다. 이는 HTML 속성으로도 ltr, rtl 여부를 지정할 수 있다. 물론.... 이 기능을 지원하는 순간부터 후속 챕터 작업할 때, 하위호환성 지원하느라 애를 먹을 수도 있다. 나같은 경우에는 히브리어를 기준으로 테스트했는데, 텍스트를 그대로 가져다 쓸 때는 정상적인 순서로 출력이 되는 것을 볼 수 있다. 하지만, 하나씩 하나씩 화면에 찍어내면 내가 알고 있는 그 문자 구성이 맞는지 의심이 들게 된다. 이를 처리하기 위해서는 RTL 언어를 어떻게 처리할 지에 대해서도 알고 있어야 하고, LTR 언어도 정상적인 순서로 출력되도록 처리하되, LTR 언어/RTL 언어 각각이 한 문장에서 올바른 순서로 출력이 되도록 해야 한다. 엄밀하게는 정규식으로 필터링해서 순서를 뒤집던가해서 하나씩 출력하게 하는 것도 고려해볼 수는 있다. 하지만.... 나는 그냥 다르게 접근했다. xy 좌표를 이미 display_list에 넣어버렸는데, 한 줄에 들어가는 텍스트를 출력하는거면 y 좌표 단위로 묶어서 아예 하나의 문자열로 모아서 출력하면 되지 않은가? RTL 문자를 어떻게 출력하는지는 어지간한 GUI 툴킷에서 따로 처리를 할 것이라는 믿음이 있었기에 그냥 RTL 순서 맞춰서 출력하는건 GUI 툴킷이 하도록 거인의 어깨에 올라탔다. lines: dict[int, list[tuple[int, str]]] = {} for x, y, c in drawable_characters: if y not in lines: lines[y] = [] lines[y].append((x, c)) for y, line_chars in lines.items(): total_width: int = len(line_chars) # 라인 전체 길이 text_segments: list[tuple[int | None, str]] = [] ... text_segments.append((x, word)) # x는 상대 좌표, word는 모아찍을 단어/문장 단위 각각의 문자를 어떻게 정상적으로 출력할지에 대해서 간단하게 살펴봤다. 그렇다면,어떻게 각 라인의 끝이 화면의 오른쪽에 딱 붙어서 출력되게 할 것인가? 이것도 굉장히 자명한 방법이 있는데, y 좌표를 기준으로 출력할 라인을 관리하게 했다면 지금 당장의 가정으로는 고정폭(HSTEP)을 기준으로 라인의 길이를 잴 수 있다. 그렇다면, 라인이 화면에 그려지는 시작점(start_x)를 화면 맨 오른쪽 좌표(WIDTH)에서 라인의 길이를 빼서 계산하고, start_x 중심으로 LTR 기준 화면이 그려지는 상대적인 좌표(x)를 더해서 그려내면 된다. 간단하다. Emoji 출력을 어떻게 할 지에 따라서는, 오히려 간단하게 해결할 수 있는데... display_list에 글자를 하나씩 하나씩 집어넣을때 이모지를 어느 위치에 출력할지에 대해서는 이미 좌표를 지정한 바가 있다. 그렇기 때문에, emoji가 나타나는 위치는 공백으로 치환하되, emoji를 그려야 하는 좌표를 emoji 출력을 위한 다른 리스트에 넣어놓고 텍스트 라인을 화면에 그리는 부분 따로, 이모지를 화면에 그리는 부분 따로 분리를 했다.
hackers.pub
November 2, 2025 at 3:53 AM
브라우저 구현 진짜 빡세다(.....)
November 1, 2025 at 10:04 AM
@daengdaenglee 안녕하세요! 반갑습니다!
November 1, 2025 at 6:16 AM
왠지 아는 사람이 만든 계정인 것만 같다...
October 31, 2025 at 8:30 AM
집 도착하면 생활리듬 정상화할겸, 발표 준비도 일찍 준비할겸 일찍 자야겠다......
October 31, 2025 at 7:33 AM
발표 경력이 두자릿수는 넘어가지만, 시간이 촉박한 상황에서 발표하는건 여전히 쫄린다...
October 31, 2025 at 6:37 AM
마크다운 지원이... 시급하다....

RE: https://cosmosli.de/@jaeyeollee/ab42d37e-719b-473d-aedf-afb0ffe76617
cosmosli.de
October 31, 2025 at 6:23 AM
* OSSCA 2025 성과공유회 발표
* 브라우저 스터디 Chapter 2 풀이글? 작성
* 브라우저 스터디 Chapter 3 연습문제 밀기
* 코스모 슬라이드 개발
* 면접 준비

RE: https://hackerspub-ask-bot.deno.dev/message/019a37eb-f800-7de7-9de5-d40aa56e1d3a
hackerspub-ask-bot.deno.dev
October 31, 2025 at 2:12 AM