브라우저 스터디 기록 (2)
Jaeyeol Lee @kodingwarrior@hackers.pub
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 지원
요구사항은 간단하다. Emoji를 그리기만 하면 된다. OpenMoji에 올라가 있는 72x72 사이즈 이미지를 받고, 각각의 이모지 이미지를 4배씩 줄인 18x18 이미지를 그리면 된다. 여기까지는 굉장히 간단하다. 파일은 2000개를 넘어가지만, 각각의 이모지는 16진수의 유니코드 일련번호로 이름이 매겨져 있기 때문에 또 어렵지는 않다.
하지만, 여기에 몇가지 함정이 있는데.......
- tkinter로 이미지를 그려낼때 이미지 객체에 대한 참조를 잃어버리면 이미지가 그려지지 않는다는 점.
- 여러 개의 요소가 복합적으로 들어간 이모지도 같이 지원을 해야 한다는 점 (코드포인트에 대한 고려가 필요하다)
이미지 캐싱
Tkinter에서는 PhotoImage 객체가 파이썬 변수의 참조를 잃는 순간, 가비지 컬렉터에 의해 메모리에서 해제된다. 즉, 이미지를 생성하고 나서 바로 create_image()로 넘기면 렌더링이 끝나기도 전에 사라져 버린다. 따라서 모든 이모지 이미지는 별도의 캐시에 저장해두어야 한다.
emoji_image_cache = {}
def load_emoji_image(file_path):
if file_path not in emoji_image_cache:
image = tk.PhotoImage(file=file_path)
emoji_image_cache[file_path] = image.subsample(4)
return emoji_image_cache[file_path]
이렇게 캐시를 두면 이미지를 중복 로드하지 않아도 되고, Tkinter가 참조를 유지하기 때문에 화면에서 사라지지 않는다. 이건 단순히 성능 최적화가 아니라, “이미지를 표시하기 위해 반드시 필요한 조건”이다.
코드 포인트
이모지는 단일 코드포인트로만 구성되지 않는다. 코드포인트(code point) 란 유니코드(Unicode)에서 각 문자에 부여된 고유한 번호로, 컴퓨터는 실제 문자를 저장하는 대신 이 숫자를 사용해 문자를 구분한다.
예를 들어, 👨💻(남성 개발자)은 세 개의 코드포인트 U+1F468, U+200D, U+1F4BB로 이루어진 조합형 이모지다. OpenMoji에서는 이 코드포인트 값을 그대로 파일 이름으로 사용하기 때문에 "1F468-200D-1F4BB.png"에서 .png를 제거하면 "1F468-200D-1F4BB"라는 문자열이 남는다.
접근 방법은 단순하다. 이 문자열을 하이픈(-)으로 나누고, 각 항목을 16진수 기준으로 파싱해서 정수로 바꾼 다음 chr()로 문자로 변환하면 된다. 단일 코드포인트라면 하나만 처리하고, 하이픈이 있다면 여러 문자를 이어붙여 하나의 문자열로 만들면 된다.
codepoint_str = filename[:-4]
if "-" in codepoint_str:
cps = [int(cp, 16) for cp in codepoint_str.split("-")]
char = "".join(chr(cp) for cp in cps)
else:
char = chr(int(codepoint_str, 16))
이 과정을 통해 "1F600.png" -> 😀, "1F468-200D-1F4BB.png" -> 👨💻 처럼 파일명에서 실제 이모지 문자를 복원할 수 있다. 그렇다면, 이를 emoji_map 라는 딕셔너리의 키 값으로 활용이 가능하고, 이에 맞게 이미지를 불러올 파일 경로를 그대로 매핑할 수 있다.
연습문제 2.7 : RTL 지원
브라우저는 일반적으로 텍스트와 화면 요소를 왼쪽에서 오른쪽(LTR, Left-To-Right) 으로 배치한다. 하지만, 오른쪽에서 왼쪽(RTL, Right-To-Left) 으로 쓰는 문화권(예: 아랍어, 히브리어 등)에서는 RTL 지원이 필수적이다.
HTML에서도 dir="ltr" 혹은 dir="rtl" 속성으로 방향을 지정할 수 있지만, 이 기능을 지원하기 시작하면 이후 하위 호환성 문제로 꽤 골치 아파질 수 있다.
나의 경우, 히브리어를 기준으로 테스트를 진행했다. 텍스트 전체를 그대로 렌더링하면 정상적인 순서로 표시되지만, 문자를 하나씩 직접 찍어내는 방식으로 렌더링하면 문자의 조합이 어긋난 듯 보인다. 따라서 RTL 언어를 어떻게 처리할지, 그리고 LTR 언어와 섞여 있을 때 어떻게 자연스러운 순서를 유지할지 이해하고 있어야 한다. 엄밀하게는 정규식을 이용해 문자 순서를 뒤집어 출력하는 방식도 고려할 수 있지만, 나는 조금 다른 접근을 택했다.
예제를 충실히 따라했다면, 우리는 이미 각 문자의 x, y 좌표를 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():
line_length: int = len(line_chars) # 라인 전체 길이
text_segments: list[tuple[int | None, str]] = []
...
text_segments.append((x, word)) # x는 ltr 기준 상대 좌표, word는 모아찍을 단어/문장 단위
그 다음 고민은 라인의 끝을 어떻게 화면의 오른쪽에 정확히 맞출 것인가 였다.
만약 라인을 y 좌표 기준으로 관리하고 있고, 고정폭 글꼴(HSTEP)을 사용한다면, 라인의 전체 길이를 쉽게 계산할 수 있다.
그렇다면 시작 좌표 start_x는 단순히 start_x = WIDTH - line_length 로 계산할 수 있고, 이후에는 LTR 기준의 상대적 x 좌표를 더해주기만 하면 된다.
간단하고 명료하다.
이모지 출력은 오히려 더 단순하게 처리했다. display_list에 문자를 하나씩 추가할 때 이미 각 이모지의 출력 좌표를 알고 있으므로, 이모지가 들어갈 자리는 공백 문자로 치환하고, 이모지만 따로 렌더링할 리스트를 만들어 분리했다. 그 결과, 텍스트 라인은 텍스트 전용 루틴에서, 이모지는 별도의 루틴에서 그려지도록 했다.