과제를 진행하면서 DOM에 대한 피드백을 많이 접하게 되었는데, 그 중에서 DOM에 대한 라이브러리를 직접 만들어 사용해보라는 리뷰가 눈에 띄어 구글링을 해보고 직접 구현해보려 했다. 하지만 막상 어떤 패턴을 사용해야할지 감이 오질 않아, 다양한 예제들을 접하면서 적용해볼 수 있을만 한 레퍼런스를 두고 그것을 참고해 만들어보기로 하였다.
고민 지점
- 예상할 수 있는 side effect 는 최대한 피하기.
querySelector()
vs.querySelectorAll()
- 싱글턴 패턴 vs. 생성자 패턴
Template
전체적인 패턴은 여기에 있는 코드를 대부분 차용했다. IIFE를 이용하여, 생성자 함수와 프로토 타입 메소드를 선언해두고, 그것을 인스턴스로 만들어주는 함수를 바로 반환함으로써, 굳이 new
키워드를 반복적으로 쓰지 않아도 jQuery 문법처럼 사용할 수 있게끔 목표를 두고 해당 라이브러리를 구현했다.
사실 이 때까지만 해도 IIFE를 사용하면 다 싱글턴이라고 생각했다. 하지만 디자인 패턴에 관한 아티클을 읽으며, 패턴에 대한 이해도가 많이 부족했음을 알 수 있었다. 결국 내가 구현하려는 패턴은 들어오는 인자 값에 따라 다른 인스턴스를 만들어주어야 하기 때문에 전형적인 생성자 패턴으로 가야 하는 것이 맞고, 대신 인스턴스화하는 함수까지 해당 모듈 안에 넣어 주는 약간의 트릭인 셈이다.
const $ = (() => {
const constructor = function () {}
constructor.prototype.method = function () {}
const instantiate = function () {
return new constructor();
}
return instantiate;
})();
위의 예시는 앞서 말했던 템플릿을 코드로 옮겨 놓은 것이다. 처음에는 함수 표현식을 arrow function 으로 선언해두려고 했는데, arrow function 은 생성자 함수로도, 프로토타입의 메소드로도 쓰일 수가 없다는 글을 읽게 되어 익명 함수로 바꾸어 주었다.
Parameter
이제 $
이름을 가진 해당 함수를 만들었으니, 그 안에 매개 변수로 들어갈 선택자를 어떤 식으로 다루어 줄 지 고민해 보았다. 선택자가 들어오면, 생성자 함수에서 그에 맞는 DOM 요소를 불러오고 반환될 인스턴스에 프로퍼티로 넣어주게 되면 그것을 타겟 삼아 여러 메소드를 연결 시켜주면 되지 않을까?
const $ = (() => {
const constructor = function (selector, parentNode) {
this.targets = parentNode.querySelectorAll(selector);
this.target = this.targets.length === 1 && this.targets[0];
}
constructor.prototype.method = function () {}
const instantiate = function (selector, parentNode = document) {
return new constructor(selector, parentNode);
}
return instantiate;
})();
이 부분을 만들게 되었을 때 두 가지 요소를 고려했다.
- 꼭 document가 아니더라도 상위 태그에 한정지어 DOM을 불러올 수 있는 방법
- DOM 요소가 한 개 혹은 여러 개인지 상관 없이 일관적으로 쓸 수 있는 방법
첫 번째 방법은 parentNode
를 인자로 추가해주고 default parameter 를 설정해주어 좀 더 유연하게 동작하도록 만들었다. 두 번째 방법을 고민했을 때는 내가 id
, class name
을 언제 사용하는지에 연관 지어 정리해놓고, 최대한 예외 상황이 발생하지 않게끔 메소드를 만들어야겠다 싶었다. 다음은 개인적으로 자주 사용하는 그 상황들을 나열해본 것이다.
- DOM에서 무언가를 얻어오는 행동 : value, attribute etc.
- DOM에 무언가를 설정하는 행동 : value, event etc.
이렇게 보니, 특정한 노드에서 특정한 값을 받아 오는 경우를 제외하고는 querySelcectorAll()
로 생성된 Node List 를 반복 시켜 주면 될 것 같았다.
Method
이렇게 라이브러리를 만들고 있던 도중 슬랙 채널에서 같은 고민을 하는 동료의 글을 보게 되었다. 그 분은 직접 내장 객체에 접근하여 커스텀 라이브러리 객체와 병합을 시켜주는 방법으로 접근 했었는데 같이 논의를 해본 결과, 내장 객체에 직접 무언가를 조작하는 것은 쉐도잉 외에도 여러가지 이슈를 동반할 가능성이 있을 것 같아 일단 보류를 했던 기억이 있다.
그래서 내 경우에는 라이브러리 내의 프로토타입 메서드가 기존 내장된 메소드에 접근해 같은 역할을 수행하게끔 연결만 시켜주자, 생각하게 되었다. 처음에는 이미 존재하고 있는 메소드인데, 굳이 한번 더 박싱해서 선언을 해줘야 하나 의문이 들었지만 막상 만들어 놓고 사용해 보니 여러가지 측면에서 꽤 편했다.
- 특정 반환 값을 얻어와야 하는 메소드가 아니라면,
return this
를 선언해 메소드 체이닝을 할 수 있도록 코드를 작성해 주었다. 이렇게 하니 동일한 인스턴스를 여러번 써야할 수고를 줄일 수 있었다. - 모든 메소드 안에서 반복문을 돌려 내장 메소드를 돌리게 하니
id
,className
상관 없이 메소드만 호출하면 원하는 동작을 수행 시킬 수 있었다.
아마 이러한 custom DOM library에 불완전성을 꼽으라면, 주어진 선택자에 따라 영향을 미치는 범위가 달라질 수도 있다는 건데 개인적으로 이러한 문제는 애초에 selector를 선언해줄 때 조심하면 되는 문제라고 생각한다. 개별 요소에는 id를 부여하고 여러 개의 DOM 요소를 동시에 다룰 것이라면 data attirbute를 부여한 뒤, 해당 라이브러리를 편하게 사용하면 된다. 단, 여러 개의 요소에 같은 타입의 선택자를 부여한 뒤, 그 중 일부만 운용을 하고 싶다면 기존의 메소드를 사용하면 되긴 하겠다만, 아직까지는 그러한 상황을 만난 적이 없어 특정 예외 상황에 부딪히면 그 때 또 고민해보지 않을까 싶다.
그리고 또 하나 추가하자면, 이러한 라이브러리를 만들 때 협업하는 사람들이 있다면 꼭 그에 대한 명세서를 세심히 작성해야 할 것 같다. 아무리 내장 객체의 메소드를 비슷한 이름으로 연결시켜주었다고 해도, 해당 라이브러리에 원하는 기능이 존재하는지는 만든 사람이 아니고서야 알기 힘들기 때문에 소통적인 측면을 고려해보았을 때는 라이브러리에 대한 설명을 정리한 문서가 어딘가에는 꼭 있어야 할 것이다.
Example
아직은 완전하지 않지만, 이번 과제를 수행하며 사용했던 DOM library의 일부를 옮겨 정리해 보았다.
const $ = (() => {
const constructor = function (selector, parentNode) {
if (!selector) {
return;
}
this.targets = parentNode.querySelectorAll(selector);
this.target = this.targets.length === 1 && this.targets[0];
}
constructor.prototype.each = function (callBack) {
if (!callBack || typeof callBack !== 'function') {
return;
}
this.targets.forEach((target, idx) => callBack(target, idx));
return this;
};
constructor.prototype.addClass = function (className) {
this.each(target => target.classList.add(className));
return this;
};
constructor.prototype.removeClass = function (className) {
this.each(target => target.classList.remove(className));
return this;
};
constructor.prototype.getValue = function () {
return this.target.value;
};
const instantiate = function (selector, parentNode = document) {
return new constructor(selector, parentNode);
}
return instantiate;
})();
'Development' 카테고리의 다른 글
[21.05.15] React로 생각하기 (1) | 2024.01.08 |
---|---|
[21.04.25] 호이스팅에 대한 오해와 진실 (0) | 2024.01.08 |
[21.02.20] OOP - public, private and protected (0) | 2024.01.08 |
[21.02.01] YDKJSY - Prototypes (0) | 2024.01.08 |
[21.01.29] YDKJSY - Mixing (Up) "Class" Objects (0) | 2024.01.07 |