🔨 popup.js 뜯어 고치기
팝업은 화면 오른쪽 상단 아이콘을 누르면 나타나는데,
메인 페이지와 별개로 동작한다. window, document 객체가 별개로 만들어진다.
popup.js은 팝업 페이지에서 돌아가게 만든 스크립트 파일이다.
스크립트에서 관리하는 Element들은 9개나 된다.
"색상" 에서 오른쪽 엘리먼트(두개 중 아무거나)를 누르면 color picker가 활성화된다.
color picker는 iro라는 라이브러리를 적용했다.
이게 좀 골때리는게, 색상을 선택하면 아래 예시도 같이 바뀌어야 하고, 색상 오른쪽의 엘리먼트 색도 바꿔줘야 한다.
설정 언어나 크기조절을 바꿔도 예시는 변한다.
엘리먼트들의 결합도가 높은것이다.
그렇기 때문에 popup.js라는 스크립트 파일에 몰아 넣었고, 코드는 알아보기 힘들게 되었다.
코드를 분리할 차례가 왔다!
간단하게 배경색 선택만 분리해보았다.
다른 클래스를 추가하지 않고 콜백과 엘리먼트들의 관계를 표현했다.
UML을 봤을땐, 교차가 많아보이고 순환 참조가 발생한다.
보기에도 객체들의 관계를 이해하기 어렵다.
🧩 MVP 패턴 도입
순환 참조에 주목을 했으며 오픈채팅방에서 고수님들께 의견을 여쭈어 보았다.
- MVP나 MVVM패턴을 사용해 보자
- 콜백과 엘리먼트는 하나로 봐야 하기 때문에 서로 참조해도 괜찮은것 같다
두 객체의 관계를 결합도가 아닌 응집도로 볼 수 있구나 라는 생각과 함께 MVP 패턴을 찾아보았다.
MVP 패턴은 Model - Presenter - View 로 구성되는 방식이다.
MVC와 다른 점은 View와 Model이 직접적으로 참조하지 않는다는 특징이 있다.
두개의 차이점, 장단점은 논란이 있는 것 같다.
위 그림만 보면 두개는 별 차이가 없다.
실제로도 그렇다는 말도 있고,
web MVC에서 user action을 controller에서 받는 것 때문에 다르다! 라고 하는 분도 있다.
이 프로젝트에선
Model을 브라우저 로컬스토리지에 저장하는 로직으로 구성하고
View와 분리시켜 안정적인 구조를 만들 생각이다.
아래와 같이 설계했다.
보통 View당 Presenter를 하나씩 쓰지만 그럴 필요가 없는 것 같아 하나로 통합했다.
사실 컬러 선택기와 프리뷰는 같이 붙어있기도 하다.
또, 객체들은 유일해야 한다.
고로 PopupContext에서 객체들을 생성하고 필요한 객체는 의존성 주입을 하게 구성했다.
🧐 코드 작성하며 생긴 고민들 - Class 사용
가장 먼저 클래스를 작성하려 했는데,
클래스는 자바스크립트 써도 될까? 라는 고민이 들었다.
어디선가 비효율 적이다! 라는 말을 들은 적이 있던 것 같아 좀 찾아보았다.
(아래의 글 참고)
먼저 클래스와 팩토리 함수에 대해 알아보자.
📚 클래스
자바스크립트에선 클래스를 class키워드로 선언할 수 있다.
다른 언어의 클래스와 같이 new로 생성할 수 있고, 생성자, 맴버변수, 맴버함수도 만들 수 있다.
class A {
constructor(a) {
this.a = a;
}
getA() {
return a
}
}
let obj = new A(1)
obj.getA()
console.log(obj.a) // 이것도 가능!
obj.getA = () => {return 0} // 심지어 이런것도 된다!!
다만, 위 예제와 같이 맴버들을 보호할 수 없고 생성도 비싸다.
📚 팩토리 함수
클래스가 나오기 이전부터 자바스크립트 사용자는 팩토리 함수를 사용해왔다.
팩토리 함수는 아래와 같이 쓸 수 있다.
const A = (a) => {
let _a = a
const getA = () => {
return _a
}
return {
getA
}
}
let obj = A(1)
obj.getA()
console.log(obj._a) // undefined
new 키워드도 사라지고 맴버들을 보호할 수 있다.
비용도 클래스에 비해 싸다.
그리고 팩토리 함수는 다른 언어에선 볼 수 없는 자바스크립트 다운 방식이다.
자바 언어에서 이렇게 객체를 만들려고 하면 잘 돌아가지 않을 것이다.
자바스크립트는 Lexical scoping 개념이 도입되었다.
Lexical scoping는 변수가 선언되는 곳을 상위 스코프로 지정하는 방식이다.
함수와 Lexical scoping의 조합을 클로저라고 한다.
위의 예제에서 _a는 A(1)를 호출한 뒤 진작에 gc되어야 할 변수인 것 같지만,
getA가 이 스코프에 대한 참조를 가지고 있어 지워지지 않는다.
* 자바에선 람다한정으로 람다가 선언된 스코프의 final 이거나 사실상 final인 변수를 읽을 수 있다. (읽기만.. 자바는 스코프를 참조하는 것 이아닌 변수 복사가 이루어진다: 자세한건 람다 캡처링 참고)
이제 코드를 보자!
Presenter는 factory function을 이용해 아래와 같이 작성했다.
의존성을 생성자로 주입하지 않고 별도의 init 함수를 만든 이유는 View와 Presenter같의 순환 참조 때문이다.
export const ColorBgPresenter = () => {
let _colorBgPicker = null;
let _colorBgDisplay = null;
let _ccStatusExample = null;
let _model = null;
const init = (colorBgPicker, colorBgDisplay, ccStatusExample, model) => {
_colorBgPicker = colorBgPicker
_colorBgDisplay = colorBgDisplay
_ccStatusExample = ccStatusExample
_model = model
}
const toggleBackgroundColorPicker = () => {
if (_colorBgPicker.isDisplay()) {
_colorBgPicker.hide()
} else {
_colorBgPicker.display()
}
}
const hideBackgroundColorPicker = () => {
_colorBgDisplay.hide()
}
const setBackgroundColor = async bgColor => {
await _model.setBackgroundColor(bgColor)
_colorBgPicker.setColor(bgColor)
_colorBgDisplay.setColor(bgColor)
_ccStatusExample.setBackgroundColor(bgColor)
}
return {
init,
toggleBackgroundColorPicker,
hideBackgroundColorPicker,
setBackgroundColor
}
}
🧐 코드 작성하며 생긴 고민들 - 타입 선언
다 좋지만, 사용이 불편하다. class를 쓰지 않고 factory function을 사용해서 그런지 타입 추론이 개판이다.
확장 프로그램인 관계로 타입스크립트로 마이그레이션 하는 건 별로인것 같다.
(스토어 제출 검사를 통과할 자신이 없다. 😭)
jsDoc을 쓰면 어떨까?
js에서도 jsDoc으로 클래스 타입 추론을 활용할 수 있다.
@typedef 으로 클래스 타입을 새로 만들어 주면 된다.
공개 맴버변수와 함수는 @property에 명시해야한다.
/**
* @typedef {Object} Foo
* @property {function} getA
*/
/**
* This is Foo Class
* @param {*} a
* @returns {Foo}
*/
const Foo = a => {
let _a = a
const getA = () => {
return _a
}
return {
getA
}
}
아래의 글을 참고했다.
그래서 Presenter는 아래와 같이 작성했다.
import { CcStatusExample } from "../common/CcStatusExample.js";
import { ColorBgDisplay } from "./ColorBgDisplay.js";
import { ColorBgModel } from "./ColorBgModel.js";
import { ColorBgPicker } from "./ColorBgPicker.js";
/**
* @typedef {Object} ColorBgPresenter
* @property {(colorBgPicker: ColorBgPicker, colorBgDisplay: ColorBgDisplay, ccStatusExample: CcStatusExample, model: ColorBgModel) => void} init
* @property {function} toggleBackgroundColorPicker
* @property {function} hideBackgroundColorPicker
* @property {(bgColor: any) => Promise<void>} setBackgroundColor
*/
/**
* CC Preview Background Color Picker Presenter
* @returns {ColorBgPresenter}
*/
export const ColorBgPresenter = () => {
/** @type {ColorBgPicker} */
let _colorBgPicker = null;
/** @type {ColorBgDisplay} */
let _colorBgDisplay = null;
/** @type {CcStatusExample} */
let _ccStatusExample = null;
/** @type {ColorBgModel} */
let _model = null;
/**
* initialize
* @param {ColorBgPicker} colorBgPicker
* @param {ColorBgDisplay} colorBgDisplay
* @param {CcStatusExample} ccStatusExample
* @param {ColorBgModel} model
*/
const init = (colorBgPicker, colorBgDisplay, ccStatusExample, model) => {
_colorBgPicker = colorBgPicker
_colorBgDisplay = colorBgDisplay
_ccStatusExample = ccStatusExample
_model = model
}
const toggleBackgroundColorPicker = () => {
if (_colorBgPicker.isDisplay()) {
_colorBgPicker.hide()
} else {
_colorBgPicker.display()
}
}
const hideBackgroundColorPicker = () => {
_colorBgDisplay.hide()
}
const setBackgroundColor = async bgColor => {
await _model.setBackgroundColor(bgColor)
_colorBgPicker.setColor(bgColor)
_colorBgDisplay.setColor(bgColor)
_ccStatusExample.setBackgroundColor(bgColor)
}
return {
init,
toggleBackgroundColorPicker,
hideBackgroundColorPicker,
setBackgroundColor
}
}
의존성 객체들은 init 메서드를 통해 주입받도록 했다.
UML에선 모델과 소통을 callback로 한다고 했지만 칭했지만 실제로는 Promise로 작성했다. (더 간결함)
💬 객체들의 생성과 주입은 누가?
위의 UML에 나와있는 것처럼 PopupContext라는 클래스에서 객체들을 생성해주고 의존 객체를 주입한다.
또, 싱글톤처럼 작동해서 객체의 유일성을 보장해준다.
대충 아래와 같이 작성했다.
/**
* @typedef {Object} PopupContext
* @property {function} init
* @property {() => Storage} stoarge
* @property {() => LanguagePresenter} languagePresenter
* @property {() => ColorBgPresenter} colorBgPresenter
* @property {() => ColorTxtPresenter} colorTxtPresenter
* @property {() => CcPreviewFontSizePresenter} ccPreviewFontSizePresenter
*/
/**
* Popup Object Factory
* @param {Document} document
* @param {*} iro
*/
export const PopupContext = (document, iro) => {
/** 맴버변수들 */
...
/** common */
const ccStatusExample = () => {
...
}
const mainDiv = () => {
...
}
/** cc preview font size */
const ccPreviewFontSizePresenter = () => {
...
}
const ccPreviewFontSizePicker = () => {
...
}
const ccPreviewFontSizeModel = () => {
...
}
/** cc preview language */
const languagePresenter = () => {
...
}
const combineRegionCheckBox = () => {
...
}
const languagePicker = () => {
...
}
const languageModel = () => {
...
}
/** cc preview background color */
const colorBgPresenter = () => {
...
}
const colorBgDisplay = () => {
...
}
const colorBgPicker = () => {
...
}
const colorBgModel = () => {
...
}
/** cc preview text color */
const colorTxtPresenter = () => {
...
}
const colorTxtDisplay = () => {
...
}
const colorTxtPicker = () => {
...
}
const colorTxtModel = () => {
...
}
const storage = () => {
...
}
const messageManager = () => {
...
}
return {
init,
storage,
languagePresenter,
colorBgPresenter,
colorTxtPresenter,
ccPreviewFontSizePresenter
}
}
👏 마무리
약간의 오류를 고치고 나니 프로그램이 잘 돌아갔다.
그리고 이전에 발견하지 못한 오류도 찾을 수 있었다.
이번 리팩토링으로 MVP 패턴에 대해 알게되었고 응용해보았다.
또, js의 Factory function과 클로저에 대해 정리&응용 할 수 있는 시간이였다.
'프로그래밍 > 유튜브 자막 표시기' 카테고리의 다른 글
유튜브 자막 표시기 리팩토링 - 테스트 (0) | 2023.02.13 |
---|---|
유튜브 자막 표시기 리팩토링 - content_script.js (0) | 2023.02.07 |