티스토리 뷰

728x90

개발 초보가 나름대로 오픈소스를 살펴보았습니다.

큰 그림만 따라가며 이해했고, 잘못되거나 생략된 정보가 많을 수 있습니다.

혹시라도 잘못된 부분을 댓글로 알려주시면 반영하도록 하겠습니다.

 


들어가며

요즘 자바스크립트를 더 깊게 공부하고 있습니다.

어제 공부한 부분은 이벤트와 관련된 부분인데요, 아시다시피 이벤트 핸들러를 등록하는 방법은 세 가지가 있습니다.

  1. HTML attribute로 등록
  2. DOM property로 등록
  3. addEventListener()를 이용한 등록

1, 2는 자바스크립트 레벨에서 눈에 보이는 뭔가가 있습니다. getAttribute(), 혹은 이벤트 관련 프로퍼티로 핸들러 함수를 직접 조회할 수 있거든요.

 

근데 addEventListener()는 좀 다릅니다. property, attribute와 아예 별개로 핸들러가 저장되고, 동일한 이벤트에 여러 개의 핸들러를 등록할 수도 있습니다. 근데 막상 등록한 핸들러를 조회하는 기능이 없습니다. 이게 진짜 너무 궁금하더라고요.. 대체 핸들러들을 어디다 저장을 해놨길래 나는 자바스크립트상에서 확인도 못하는데 브라우저는 알고 있지? 하는 생각이 들었습니다.

 

구글 그 어디에서도 addEventListener()가 어떻게 동작하는지 알려주질 않습니다. 아 이건 브라우저단 구현이구나, 싶어서 webkit 오픈 소스를 직접 확인해보기로 했습니다.

 

스펙 문서 확인하기

먼저 MDN에서 addEventListener()가 EventTarget 객체에 속해있다는 사실을 확인할 수 있었습니다.

 

 

같은 내용을 DOM 표준 문서에서도 확인할 수 있습니다.

 

추가로, EventTarget 객체는 event listener list를 가지고 있다는 정보를 얻었습니다.

 

여기서 나온 EventTarget 객체는 모든 노드가 상속받는 객체입니다. DOM 객체들은 다음 상속 구조로 이루어져 있습니다.

EventTarget에 이벤트 리스너 관련 메서드가 존재하기 때문에, 우리가 일반적으로 사용하듯 노드 객체에서 이벤트 리스너를 사용할 수 있게 됩니다.

 

 

코드 살펴보기

전체 코드는 여기서 보실 수 있습니다.

(https://github.com/WebKit/WebKit/tree/9252a1c7a007629de9a6f86e213e5a90591b4c98/Source/WebCore/dom)

 

EventTarget.h(링크)

우선 EventTarget.h 를 살펴봅시다.

처음 코드를 보면 뭐가 엄청 많습니다. 핵심 코드만 추려보겠습니다.

// EventTarget.h

struct EventTargetData {
    WTF_MAKE_NONCOPYABLE(EventTargetData); WTF_MAKE_FAST_ALLOCATED;
public:
    EventTargetData() = default;
    EventListenerMap eventListenerMap;
    bool isFiringEventListeners { false };
};

class EventTarget : public ScriptWrappable, public CanMakeWeakPtr<EventTarget> {
    WTF_MAKE_ISO_ALLOCATED(EventTarget);
public:
    ...
    WEBCORE_EXPORT virtual bool addEventListener(const AtomString& eventType, Ref<EventListener>&&, const AddEventListenerOptions&);
    ...

protected:
    ...
    virtual EventTargetData& ensureEventTargetData() = 0;
    ...
};

 

이름이 addEventListener이면서, 위에서 본 인터페이스와 동일한 꼴의 함수를 발견했습니다. WEBCORE_EXPORT가 붙은걸 보니 정확히는 몰라도 일단 바깥에서 사용할 수 있게끔 만든 거구나 하고 짐작할 수 있습니다. 아마 이 함수가 우리가 찾던 함수 같습니다.

 

구현부를 확인해봅시다.

 

EventTarget.cpp(링크)

// EventTarget.cpp

...
bool EventTarget::addEventListener(const AtomString& eventType, Ref<EventListener>&& listener, const AddEventListenerOptions& options)
{
#if ASSERT_ENABLED
    listener->checkValidityForEventTarget(*this);
#endif

    if (options.signal && options.signal->aborted())
        return false;

    auto passive = options.passive;

    if (!passive.has_value() && Quirks::shouldMakeEventListenerPassive(*this, eventType, listener.get()))
        passive = true;

    bool listenerCreatedFromScript = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup();

    // ↓↓↓ 여기가 핵심 ↓↓↓
    if (!ensureEventTargetData().eventListenerMap.add(eventType, listener.copyRef(), { options.capture, passive.value_or(false), options.once }))
        return false;
    // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
    
    if (options.signal) {
        options.signal->addAlgorithm([weakThis = makeWeakPtr(*this), eventType, listener = makeWeakPtr(listener.get()), capture = options.capture] {
            if (weakThis && listener)
                weakThis->removeEventListener(eventType, *listener, capture);
        });
    }

    if (listenerCreatedFromScript)
        InspectorInstrumentation::didAddEventListener(*this, eventType, listener.get(), options.capture);

    if (eventNames().isWheelEventType(eventType))
        invalidateEventListenerRegions();

    eventListenersDidChange();
    return true;
}

핵심이라고 주석 쳐놓은 부분 외는 정확히 파헤치진 못했으나, addEventListener()의 옵션 처리 혹은 특별한 케이스를 위한 코드로 보입니다. 저기까지 파고들기엔 너무 고달파서 저는 중요한 부분만 확인했습니다ㅜㅜ..

 

아무튼 핵심 부분을 보시면 ensureEventTargetData()를 호출합니다. 해당 함수는 헤더에 순수 가상 함수로 선언이 되어있고, 나중에 Node 클래스에서 구현하게 됩니다. 중요한 건 그 반환 값입니다. 이 함수는 EventTargetData& 타입을 반환하며, 그 안에 있는 eventListenerMap에 접근하여 add 메서드를 호출합니다.

 

EventTargetData 구조체 또한 헤더 윗단에 구현되어있습니다. 확인해보시면 EventListenerMap 타입의 변수를 갖고 있고, 그 변수의 메서드를 호출하기 때문에 이 클래스 또한 살펴보아야 합니다.

 

EventListenerMap.h (링크)

class EventTarget;

using EventListenerVector = Vector<RefPtr<RegisteredEventListener>, 1>;

class EventListenerMap {
public:
    ...
    bool add(const AtomString& eventType, Ref<EventListener>&&, const RegisteredEventListener::Options&);
    ...
private:
    ...
    Vector<std::pair<AtomString, std::unique_ptr<EventListenerVector>>, 2> m_entries;

#ifndef NDEBUG
    std::atomic<int> m_activeIteratorCount { 0 };
#endif

    Lock m_lock;
};

여기서도 핵심 코드만 추려보았습니다.

 

RefPtr, Ref, Vector 등 뭔지 모를 래퍼가 너무 많아 각 클래스가 정의된 코드의 중요한 부분만 훑어봤습니다. 

Vector(링크)는 std::vector 클래스와 비슷하게 동작하지만 좀 더 효율적, 안정적으로 사용하기 위해 따로 정의해둔 자료구조 같고, Ref(링크)와 RefPtr(링크)은 C++의 참조자, 포인터와 비슷한 용도로 사용하면서 추가적인 기능을 수행하기 위해 만든 클래스가 아닐까 합니다. 여러 코드들을 살펴보면 Ref의 타입으로 들어가는 클래스는 항상 RefCounted(링크) 클래스를 상속받습니다. 그리고 Ref의 생성자에서 RefCounted 클래스의 ref()를 호출하는데, ref() 메서드는 말 그대로 ref count를 1씩 증가시킵니다. 아마 가비지 콜렉터처럼 메모리 누수를 방지하고 더 안정적으로 관리하기 위해 사용하는 게 아닐까 궁예질을 한 번 해봅니다..

 

헤더를 보면 EventListenerVector 타입을 정의해놓은 게 눈에 띕니다. RegisteredEventListener(링크) 클래스의 포인터를 저장하는 벡터인데요, 해당 클래스는 단순히 이벤트 리스너의 콜백, 옵션 등을 저장하는 클래스입니다. 즉, EventListenerVector는 이름 그대로 이벤트 리스너들을 관리하는 벡터라고 볼 수 있습니다.

 

아래쪽에 pair<string, *EventListenerVector> 타입의 m_entries라는 멤버 변수가 있습니다. 이 클래스의 이름이 이벤트 리스너 이잖아요? 이름 그대로 생각하면 됩니다. 스트링으로 된 이벤트 타입을 key로 하고, 그에 해당하는 이벤트 리스너들의 벡터를 value로 하는 map입니다. 물론 std::map만큼 효율 좋게 돌아가진 않습니다. 데이터를 관리하는 방식에서의 map이라고 생각합시다.

 

그리고 여기에 대한 정보가 아까 언급됐었습니다(블로그 내 링크).

Each EventTarget object has an associated event listener list (a list of zero or more event listeners). It is initially the empty list.

이 말을 충실히 구현해둔 클래스입니다.

 

헤더에서 이벤트 리스너들을 어떻게 관리하는지 알 수 있었습니다. 이제 add() 메서드 구현부를 확인합시다.

 

EventListenerMap.cpp(링크)

// EventListenerMap.cpp

...

bool EventListenerMap::add(const AtomString& eventType, Ref<EventListener>&& listener, const RegisteredEventListener::Options& options)
{
    Locker locker { m_lock };
    
    assertNoActiveIterators();

    if (auto* listeners = find(eventType)) {
        if (findListener(*listeners, listener, options.capture) != notFound)
            return false; // Duplicate listener.
        listeners->append(RegisteredEventListener::create(WTFMove(listener), options));
        return true;
    }

    auto listeners = makeUnique<EventListenerVector>();
    listeners->uncheckedAppend(RegisteredEventListener::create(WTFMove(listener), options));
    m_entries.append({ eventType, WTFMove(listeners) });
    return true;
}

EventListenerVector* EventListenerMap::find(const AtomString& eventType) const
{
    for (auto& entry : m_entries) {
        if (entry.first == eventType)
            return entry.second.get();
    }

    return nullptr;
}

static inline size_t findListener(const EventListenerVector& listeners, EventListener& listener, bool useCapture)
{
    for (size_t i = 0; i < listeners.size(); ++i) {
        auto& registeredListener = listeners[i];
        if (registeredListener->callback() == listener && registeredListener->useCapture() == useCapture)
            return i;
    }
    return notFound;
}

 

구현부를 보시면 생각보다 간단합니다.

일반적인 맵 자료구조를 사용하듯 키가 존재하는지 먼저 확인하고, 키가 존재하지 않으면 새롭게 만들어 추가합니다. 그리고 용도에 맞게 value(EventListenerVector)를 이용합니다.

 

11번 줄을 보시면 eventType에 해당되는 key가 있는지 탐색합니다. 관련 데이터가 있으면 그 벡터의 포인터를, 없으면 널 포인터를 반환합니다.

 

해당 타입의 이벤트 리스너 리스트가 맵에 존재하면 그 리스트에서 인자로 주어진 이벤트 리스너를 탐색합니다.

findListener() != notFound, 즉, 리스트상에 추가할 리스너가 이미 있으면 주석에 쓰인 것처럼 중복된 리스너이므로 그대로 종료합니다. 그렇지 않다면 리스트에 리스너를 추가해줍니다.

실제로도 JS상에서 addEventListener()는 중복된 이벤트 리스너를 추가하지 않습니다.

 

만약 리스트가 맵에존재하지 않으면, 새로운 벡터를 만들어 리스너를 추가하고, 맵에 이벤트 타입을 key로 가지는 새로운 리스너 벡터를 추가합니다(18~20번 줄). 이후 성공적으로 리스너를 추가했으므로 true를 반환합니다.

 

이제 다시 처음 봤던 EventTarget 클래스의 addEventListener() 함수로 돌아가게 됩니다. 이로써 addEventListener()의 핵심 작동 원리를 모두 살펴볼 수 있었습니다.

 

추가로, Vector 클래스의 append()와 uncheckedAppend() 메서드 차이가 뭘까해서 코드를 좀 봤는데, 아마 메모리 관리 측면에서 다른 방식으로 작동하게끔 만든 메서드인듯합니다. 정확한 차이는 잘 모르겠네요..

 

Node 상속(링크 - Node.h, Node.cpp)

사실 코드를 보면 미심쩍은 부분이 있습니다. EventTarget 클래스의 addEventListener(), ensureEventTargetData()는 가상 함수이고, 후자는 심지어 순수 가상 함수입니다. 근데 addEventListener() 내에서 그 함수를 호출합니다. 대체 어떻게 돌아가는 코드인가?? 했는데 Node 클래스에 그 답이 있었습니다.

 

// Node.h

class Node : public EventTarget {
...
public:
    WEBCORE_EXPORT bool addEventListener(const AtomString& eventType, Ref<EventListener>&&, const AddEventListenerOptions&) override;
    EventTargetData& ensureEventTargetData() final;
...
}

Node는 EventTarget을 상속받고, 두 메서드를 오버 라이딩합니다.

 

// Node.cpp

bool Node::addEventListener(const AtomString& eventType, Ref<EventListener>&& listener, const AddEventListenerOptions& options)
{
    return tryAddEventListener(this, eventType, WTFMove(listener), options);
}

static inline bool tryAddEventListener(Node* targetNode, const AtomString& eventType, Ref<EventListener>&& listener, const AddEventListenerOptions& options)
{
    if (!targetNode->EventTarget::addEventListener(eventType, listener.copyRef(), options))
        return false;

    targetNode->document().addListenerTypeIfNeeded(eventType);
    if (eventNames().isWheelEventType(eventType))
        targetNode->document().didAddWheelEventHandler(*targetNode);
    else if (eventNames().isTouchRelatedEventType(eventType, *targetNode))
        targetNode->document().didAddTouchEventHandler(*targetNode);

#if PLATFORM(IOS_FAMILY)
    if (targetNode == &targetNode->document() && eventType == eventNames().scrollEvent) {
        if (auto* window = targetNode->document().domWindow())
            window->incrementScrollEventListenersCount();
    }

#if ENABLE(TOUCH_EVENTS)
    if (eventNames().isTouchRelatedEventType(eventType, *targetNode))
        targetNode->document().addTouchEventListener(*targetNode);
#endif
#endif // PLATFORM(IOS_FAMILY)

#if ENABLE(IOS_GESTURE_EVENTS) && ENABLE(TOUCH_EVENTS)
    if (eventNames().isGestureEventType(eventType))
        targetNode->document().addTouchEventHandler(*targetNode);
#endif

    return true;
}

EventTargetData& Node::ensureEventTargetData()
{
    if (hasEventTargetData())
        return *eventTargetDataMap().get(this);

    JSC::VM* vm = commonVMOrNull();
    RELEASE_ASSERT(!vm || vm->heap.worldIsRunning());

    auto locker = holdLock(s_eventTargetDataMapLock);
    setHasEventTargetData(true);
    return *eventTargetDataMap().add(this, makeUnique<EventTargetData>()).iterator->value;
}

Node::addEventListener() 내에서 tryAddEventListener()를 호출합니다.

tryAddEventListener() 함수의 10번 줄에서 EventTarget::addEventListener()를 호출합니다. 복잡해 보이지만 그 아래쪽은 특정 케이스를 처리하는 부분입니다.

그리고 EventTarget의 메서드를 Node 내에서 호출하고, Node에는 ensureEventTargetData()가 구현되어있기 때문에 EventTarget 내에서 그 메서드를 호출할 수 있던 것이었습니다.

 

느낀 점

코드를 보면서 정말 많이 배웠습니다.

  1. 모든 변수명, 함수명, 클래스명 등 이름들이 정말 명확합니다. 어떤 용도로, 어디서 사용될지 이름만 보고도 알 수 있습니다. 솔직히 관련 클래스들 헤더만 줘도 전체적인 구조를 파악할 수 있을 것 같습니다.
  2. 기능별, 관심사별로 잘게 쪼개져있는 함수들, 클래스들은 보기만 해도 마음이 편안해집니다...
  3. 에러 처리, 메모리 관리나 자료구조와 같은 부분에서 웹킷 전체에서 공통으로 사용하는 base 클래스들을 많이 만들어두었습니다. Web Template Framework의 앞 글자를 딴 wtf..이라는 폴더에서 해당 코드들을 모두 관리합니다. 이런 부분은 다음에 프로젝트를 할 때 나름대로 적용해볼 수 있을듯합니다.
  4. 위 모든 장점이 합쳐져서 코드가 정말 정말 간결하고 알아보기 쉽게 짜여있습니다. 처음 보는 코드여도 어떤 기능을 하는지 빠르게 살펴볼 수 있고, 전체 구조를 파악하기 쉽습니다.
  5. 동작 원리는 생각보다 많이 간단했습니다. EventTarget 객체는 타입별 이벤트 리스너들의 리스트를 관리하며, add 요청이 오면 리스너를 추가해줍니다. 로직만 보면 백준 실버보다 못한 느낌인데 케이스 처리, 예외 처리가 정말 많다는 생각이 듭니다. 이게 실무의 고통일까요??

아는 게 없어서 겨우 이 정도밖에 못 느꼈다는 게 억울합니다. 이론으로 공부하던 클린 코드를 직접 확인한 기분입니다.

다른 오픈소스들도 이런 식으로 잘 짜여있는지 너무 궁금해졌습니다. 이 좋은걸 남들만 알았다니.. 시간을 내서 자주 쓰는 라이브러리나 프레임워크 오픈 소스를 한번 확인해봐야겠다는 생각이 드네요. 이번엔 자바스크립트로 작성된 오픈 소스가 좋겠습니다.

 

그리고 혼자 이해하고 넘어가는 것과 그 내용을 글로 쓰는 건 차이가 크다고 느껴집니다. 분명 이해를 다 한 줄 알았는데 막상 글로 쓰려니 짐작으로 넘긴 부분이 눈에 밟혀 코드를 더 깊게 볼 수 있었습니다. 포스팅을 좀 많이 해야겠다는 생각이 듭니다.

 

참고 링크

MDN - EventTarget : https://developer.mozilla.org/ko/docs/Web/API/EventTarget

 

EventTarget - Web API | MDN

EventTarget은 이벤트를 받을 수 있으며, 이벤트에 대한 수신기(listener)를 가질 수 있는 객체가 구현하는 DOM 인터페이스입니다.

developer.mozilla.org

DOM Standard - 2.7. Interface EventTarget : https://dom.spec.whatwg.org/#interface-eventtarget

 

DOM Standard

 

dom.spec.whatwg.org

Github - Webkit/WebCore/dom : https://github.com/WebKit/WebKit/tree/9252a1c7a007629de9a6f86e213e5a90591b4c98/Source/WebCore/dom

 

GitHub - WebKit/WebKit: Official git mirror of the WebKit repository, https://svn.webkit.org/repository/webkit, future canonical

Official git mirror of the WebKit repository, https://svn.webkit.org/repository/webkit, future canonical repository. - GitHub - WebKit/WebKit: Official git mirror of the WebKit repository, https://...

github.com

Github - Webkit/wtf : https://github.com/WebKit/WebKit/tree/152d9bd44bc07a934e1277119d13185204b44fa0/Source/WTF/wtf

 

GitHub - WebKit/WebKit: Official git mirror of the WebKit repository, https://svn.webkit.org/repository/webkit, future canonical

Official git mirror of the WebKit repository, https://svn.webkit.org/repository/webkit, future canonical repository. - GitHub - WebKit/WebKit: Official git mirror of the WebKit repository, https://...

github.com

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함