처음 PSHelper를 Vanila JS로 개발하면서 단순히 구현에 급급하다보니 파일 내의 코드가 굉장히 길어지고 복잡해졌다...(나름 분리한다고 했던 것은 비밀😭)
그래도 코드를 알아볼 수 있게끔 짰다고 생각했는데 주변 지인들에게 리뷰를 요청한 결과 구조적인 리팩토링을 추천받아 기록과 함께 진행해보고자 한다.
리팩토링을 하고자 했지만 어떤 디자인 패턴을 적용해야할지 막막했다. 더군다나 내가 만들고 있는 것은 다른 웹사이트에 연동시키는 크롬익스텐션이기 때문에 생각해내기가 더 힘들었다.
그래서 단순하게 일단은 로직과 뷰를 다루는 코드들을 분리해보자는 방식으로 시도해보기로 했다.
뷰, 로직 파트 분리
Popup클래스에서 모든 것들을 컨트롤 했던 코드에서 Dom조작이 있는 부분들을 renderPopup함수로 분리하는 작업을 진행했다.
렌더링 방식은 Dom 변화가 필요한 경우 Popup클래스의 init메서드를 재호출 하고 init안에서 상황에 따라 맞는 화면을 미리 설정한 type을 지정해서 renderPopop함수를 호출하는 방식으로 진행했다.
Dom조작과 로직을 분리했다는 점에서 분명 이전보다는 낫다고 생각이 드는데 아직 제대로 된 리팩토링이 됐다는 느낌은 들지 않았다...
오랜 고민 끝에 리액트와 같은 상태기반 렌더링 컴포넌트를 바탕으로 리팩토링을 진행했다.
의미부터 재사용이 가능한 독립된 모듈이란 것에서 알 수 있듯이 코드들의 독립성과 재사용성이 보장이되는 구조이다. 또한 형식의 통일성까지 이루어지기 때문에 유지,보수에도 기존의 방식보다 훨씬 효율적이다.
상태기반 렌더링은 각 컴포넌트가 가진 상태에 따라서 Dom이 조작된다.
즉,
이런 장점들이 있다. 기존에 바닐라 자바스크립트로 프로젝트를 진행했기 때문에 이런 방식들이 기존보다 체계적이고 안정적인 작업을 이룰 수 있을 것같아 이 방식을 선택했다.
리액트의 라이프 사이클을 기반으로 컴포넌트를 만드는 작업을 진행했다.
export default abstract class Component<T> {
node: Element;
state: T;
constructor({ node, state }: ComponentProps<T>) {
this.node = node;
this.state = state;
this.render();
this.componentDidMount();
}
constructor에서 상위에서 받아온 props를 node와 state에 연결시킨다. 그 후, render, componentDidMount 메서드 순으로 실행을 해준다.
// Component.ts
.
.
.
render(): void {
addComponents.call(this);
this.createChildComponents();
this.setEvent();
}
// jsUtils.ts
.
.
export function addComponents(this: any) {
const node = this.node;
const newNode = Array.from(
new DOMParser().parseFromString(this.template(), "text/html").body.children
);
node.parentNode.insertBefore(newNode[0], node.nextSibling);
this.node = node.nextSibling;
const nodeClass = node.classList.value.trim();
if (nodeClass) {
this.node.className = nodeClass;
}
node.remove();
}
.
.
임의로 만든 엘리먼트를 실제 돔으로 변경하는 작업이다.
createChildComponents
// Main.ts
createChildComponents() {
const { user, repoName, setStatePopup } = this.state;
new Logo({
node: selectEl("LogoContainer", this.node),
state: this.state,
});
if (user && !repoName) {
new Repository({
node: selectEl("RepoContainer", this.node),
state: {
setStatePopup,
},
});
}
}
template() {
const { user, repoName } = this.state;
return `
<div>
<LogoContainer class="logo-container"></LogoContainer>
${
user
? repoName
? ``
: `<RepoContainer class="repo-container" ></RepoContainer>`
: ``
}
</div>
`;
}
template에 들어간 새로운 자식 컴포넌트들을 불러오는 메서드
// Repository.ts
async componentDidMount() {
const repos = await getUserRepos();
this.setState({repos});
}
컴포넌트가 생성될 때 render메서드가 완료된 후 한번만 작동하는 메서드이다. 보통 외부 데이터, 라이브러리를 사용해서 Dom의 변경이 필요한 경우 사용된다.
// Component.ts
setState(newState: Partial<T>): void {
if (!this.checkChangeState(newState)) {
return;
}
this.state = {
...this.state,
...newState,
};
this.render();
}
checkChangeState(newState: Partial<T>): boolean {
for (const key in newState) {
if (!Object.prototype.hasOwnProperty.call(newState, key)) {
throw new Error(`${key}는 상태에 존재하지 않는 변수입니다.`);
}
if (!isSameTwo(this.state[key], newState[key])) {
return true;
}
}
return false;
}
// jsUtils.ts
//
export function isSameTwo(a: any, b: any) {
return JSON.stringify(a) === JSON.stringify(b);
}
기존의 state를 변경시킬 때 사용하는 메서드다. 새로 들어온 state가 기존 state와 다른 값을 가질 경우 render메서드가 실행되어 컴포넌트작업을 다시 수행시킨다. 상태의 변화가 없을 때 불필요한 리렌더링을 방지해준다.
이렇게 state를 기반으로 렌더링이 되는 형식을 적용해봤다. 기존의 Home의 Popup 컴포넌트의 경우는 외부 웹사이트와는 관련이 없기 때문에 매끄럽게 잘 적용되었던 것같다. 하지만 알고리즘 사이트에 적용이되는 컴포넌트들은 완전하게 적용이 된 느낌은 아닌 것같다. 그 부분들은 다른 형식의 패턴을 적용시키던지 고민이 조금 더 필요해 보인다.