Let's Build a Browser Engine!
제1 장: 시작

이 글은 Matt BrubeckLet’s Build a Browser Engine!을 번역한 글입니다.

알림 : 부족한 실력 탓에 잘못된 번역, 부자연스러운 문장이 있을 수 있습니다. 해당 문제에 대한 의견을 댓글이나 GitHub 저장소 Pull Request를 통해 제안해 주시면 감사한 마음으로 적극 반영하도록 하겠습니다. 감사합니다.

Let’s Build a Browser Engine!

제1 장: 시작

필자는 토이 HTML 렌더링 엔진을 만들고 있습니다. 아마 이 글의 독자들도 필자와 같을 것이라고 생각됩니다. 이 글은 시리즈의 첫 번째 장입니다.

전체 시리즈에 걸쳐 필자가 작성한 코드를 설명할 것이며, 어떻게 자신만의 코드를 만들 수 있는지 보일 것입니다. 하지만 먼저 왜 만드는지를 설명하도록 하겠습니다.

무엇을 만드나요?

먼저 용어부터 살펴봅시다. 브라우저 엔진은 브라우저의 한 부분으로, 인터넷에서 웹페이지를 가져와 사용자가 읽고 보고 들을 수 있는 행태로 그 내용을 바꾸는 작업을 “보이지 않는 곳에서” 수행합니다. 대표적으로 Blink, Gecko, WebKit, Trident 브라우저 엔진이 있습니다. 반대로, 브라우저의 UI(탭, 도구 모음, 메뉴 등)는 크롬 이라고 합니다. FireFox와 SeaMonkey는 크롬은 다르지만 동일한 Gecko 엔진을 사용하는 브라우저입니다.

브라우저 엔진은 HTTP 클라이언트, HTML 파서, CSS 파서, JavaScript 엔진(파서, 인터프리터, 컴파일러로 구성) 등 많은 하위 컴포넌트를 포함합니다. HTML과 CSS와 같은 웹형식을 파싱하고 화면에 보이는것으로 변환하는 작업을 수행하는 컴포넌트들을 레이아웃 엔진 또는 렌더링 엔진으로 부르기도 합니다.

왜 토이 렌더링 엔진인가요?

완전한 기능의 브라우저 엔진은 엄청나게 복잡합니다. Blink, Gecko, WebKit은 각각 수백만 줄의 코드 들입니다. 심지어 보다 단순한 ServoWeasyPrint 같은 렌더링 엔진조차 수만 줄의 코드들입니다. 입문자가 이해하기 쉽지 않습니다.

거대하며 복잡한 소프트웨어에 대해 생각해봅시다. 만약 독자가 컴파일러나 운영체제와 수업을 듣게 된다면, 아마 특정 시점에서는 “토이” 컴파일러나 커널을 만들거나 수정하게 될 것입니다. 이것은 학습을 위해 고안된 단순한 모델입니다. 이 프로그램들은 코드를 작성한 사람 외에 다른 사람에 의해 실행되는 경우는 없을 것입니다. 하지만 토이 시스템을 만들어 보는 것은 실제로 어떻게 작동하는지 배우는데 유용한 방법입니다. 비록 실생활에서 컴파일러나 커널을 만들 일이 없더라도, 어떻게 작동하는지 이해함으로써 프로그램 작성 시 더 잘 활용할 수 있게 됩니다.

그렇다면, 브라우저 개발자가 되고 싶거나 브라우저 엔진 내부를 이해하기 위해 토이 시스템을 만들어 보는 것이 어떨까요? “실제” 프로그래밍 언어의 일부분을 구현한 토이 컴파일러처럼 토이 렌더링 엔진은 HTML과 CSS의 일부를 구현할 수 있습니다. 매일 사용하는 브라우저를 대체할 순 없더라도 간단한 HTML 문서 렌더링에 필요한 기초 단계들을 살펴볼 수 있을 것입니다.

직접 따라 해 보세요.

필자는 독자들이 직접 따라 해 보길 바랍니다. 이 시리즈는 프로그래밍 경험과 고 수준의 HTML, CSS의 개념을 알고 있다면 쉽게 따라 할 수 있을 것입니다. 그러나 막 입문하여 어려움을 겪거나 이해하지 못하는 것에 부딪힌다면, 언제든지 질문해 주길 바랍니다.

시작하기 전에 몇 가지 선택할 것이 있습니다.

프로그래밍 언어

독자는 어떠한 프로그래밍 언어로든 토이 레이아웃 엔진을 만들 수 있습니다. 정말입니다! 잘 알고 좋아하는 언어를 사용해도 좋습니다. 아니면 새로운 언어를 배우는 걸 즐기는 경우 이를 계기로 배우는 것도 좋습니다.

Gecko나 WebKit 같은 주요 브라우저 엔진에 기여해보길 원하는 독자들은 C++로 작업을 진행해볼 수 있습니다. 이들 엔진의 주요 언어를 사용함으로써 독자의 코드와 쉽게 비교해볼 수 있을 것입니다.

필자의 토이 프로젝트 RobinsonRust 로 작성되었습니다. 필자는 Mozilla에서 Servo 팀에 일원이 되면서 Rust 프로그래밍을 즐기게 되었습니다. 추가로 이 프로젝트를 통한 필자의 목표는 Servo의 구현을 보다 잘 이해하는 것입니다. 때때로 Robinson은 Servo의 데이터 구조와 코드의 단순화된 버전을 사용하기도 합니다.

라이브러리와 지름길

이 프로젝트처럼 실습을 통한 학습을 할 때 독자는 처음부터 직접 작성한 코드가 아닌 남의 코드를 사용하는 것이 과연 “편법” 인지 아닌지 결정할 필요가 있습니다. 필자는 독자 본인이 반드시 이해하고 싶은 부분은 직접 코드를 작성하고 나머지 부분에 라이브러리를 사용함에 있어 주저하지 않길 바랍니다. 특정 라이브러리 사용법을 배우는 것 자체로 가치 있는 연습이 될 수 있습니다.

필자는 필자 자신을 위한 것뿐만 아니라 이 글과 실습 과정에 예제 코드로써 큰 도음을 주기 위해 Robinson을 작성하고 있습니다. 이러한 이유와 또 다른 이유로 필자는 Robinson이 가능한 작고 독립적이길 원합니다. 그래서 지금까지 Rust 표준 라이브러리를 제외한 어떠한 외부 코드를 사용하지 않았습니다. (이점은 언어의 개발이 진행되는 동안 동일 Rust 버전으로 여러 의존성을 설정해야 하는 번거로움도 덜어줍니다.) 하지만 이 원칙이 항상 지켜지는 것은 아닙니다. 예를 들어, 필자가 작성한 저 수준 드로잉 라이브러리를 사용하지 않고 그래픽 라이브러리를 사용하게 될 수도 있습니다.

코드 작성을 피하는 또 다른 방법은 단순히 그 부분을 빼는 것입니다. 예를 들어, Robinson은 아직 네트워킹 코드가 없으며 오직 로컬 파일만을 읽을 수 있습니다. 토이 프로그램에서는 독자가 건너뛰고 싶으면 건너뛰어도 좋습니다. 필자는 글을 진행하면서 지름길을 언급할 것입니다. 그러므로 독자는 관심 없는 부분은 우회해서 곧장 재밌는 곳으로 이동할 수 있습니다. 마음이 바뀌면 나중에라도 공백을 메울 수 있을 것입니다

첫 번째 단계 : DOM

코드를 작성할 준비가 되었나요? DOM을 위한 데이터 구조를 만드는 것으로 작게 시작해 보겠습니다. Robinson의 DOM 모듈을 살펴봅시다.

DOM은 노드의 트리입니다. 노드는 0개 이상의 자식을 갖습니다. (DOM은 다양한 속성과 메소드를 갖지만 현재로선 대부분을 무시할 수 있습니다.)

struct Node {
    // data common to all nodes:
    children: Vec<Node>,

    // data specific to each node type:
    node_type: NodeType,
}

노드 유형은 여러 가지가 있지만, 현재로선 대부분을 제외하고 노드를 요소(Element) 혹은 텍스트(Text) 노드로 표현하겠습니다. 상속이 있는 언어에서 이것은 노드의 하위 유형이 될 것입니다. Rust에서는 enum이 됩니다. (“tagged union” 혹은 “sum type”의 Rust 키워드)

enum NodeType {
    Text(String),
    Element(ElementData),
}

요소(Element)는 태그(Tag)의 이름과 임의의 속성(Attribute)들을 포함합니다. 속성은 이름과 값을 맵으로 저장할 수 있습니다. Robinson은 네임스페이스를 지원하지 않기 때문에 태그와 속성 이름만을 단순 문자열로 저장합니다.

struct ElementData {
    tag_name: String,
    attributes: AttrMap,
}

type AttrMap = HashMap<String, String>;

마지막으로 새로운 노드를 쉽게 만들 수 있는 몇 가지 생성자 함수를 만듭니다.

fn text(data: String) -> Node {
    Node { children: Vec::new(), node_type: NodeType::Text(data) }
}

fn elem(name: String, attrs: AttrMap, children: Vec<Node>) -> Node {
    Node {
        children: children,
        node_type: NodeType::Element(ElementData {
            tag_name: name,
            attributes: attrs,
        })
    }
}

이것이 전부입니다! 최종 DOM 구현에는 훨씬 더 많은 데이터와 수십 가지 메소드가 포함될 것이지만, 시작에는 이 정도면 충분합니다.

연습

아래 사항들은 직접 실습해볼 몇 가지 사항들입니다. 관심이 있다면 진행하고 그렇지 않다면 건너뛰길 바랍니다.

  1. 독자가 선택한 프로그래밍 언어로 새 프로그램 작성을 시작하고 DOM 텍스트 노드와 요소의 트리를 표현하는 코드를 작성하세요.

  2. 최신 버전의 Rust를 설치한 다음, Robinson을 다운로드 후 빌드 하세요. dom.rs를 열고 NodeType을 확장하여 주석 노드와 같은 추가 유형의 노드들을 포함시키세요.

  3. DOM 노드 트리의 출력 코드를 작성하세요.

다음 장에서는 HTML 소스 코드를 DOM 노드 트리로 변환하는 파서를 추가해 보겠습니다.

참고 자료

브라우저 엔진 내부에 대한 자세한 내용은 Tali Garsie의 멋진 자료How Browsers Work (한글 번역본 : 브라우저는 어떻게 동작하는가?) 와 그 안에 포함된 추가 자료 링크를 참고하길 바랍니다.

코드의 경우, 아래에 “작은” 오픈 소스 웹 렌더링 엔진 목록을 참조하길 바랍니다. 대부분은 Robinson 보다 몇 배 더 크지만, Gecko나 Webkit보다는 훨씬 작습니다. WebWhirr는 2000여 줄의 코드로 이들 중 “토이” 엔진이라고 부를 수 있는 유일한 엔진입니다.

이 프로젝트들이 영감을 얻거나 참고하는데 유용하다는 것을 알게 될 것입니다. 만약 유사한 프로젝트를 알거나 시작하는 경우 필자에게 알려주기 바랍니다!

Discussion and feedback