PSHelper 리팩토링(2)

HANITZ·2023년 10월 28일
0

처음 PSHelper를 Vanila JS로 개발하면서 단순히 구현에 급급하다보니 파일 내의 코드가 굉장히 길어지고 복잡해졌다...(나름 분리한다고 했던 것은 비밀😭)
그래도 코드를 알아볼 수 있게끔 짰다고 생각했는데 주변 지인들에게 리뷰를 요청한 결과 구조적인 리팩토링을 추천받아 기록과 함께 진행해보고자 한다.

리팩토링 시작

리팩토링을 하고자 했지만 어떤 디자인 패턴을 적용해야할지 막막했다. 더군다나 내가 만들고 있는 것은 다른 웹사이트에 연동시키는 크롬익스텐션이기 때문에 생각해내기가 더 힘들었다.

그래서 단순하게 일단은 로직과 뷰를 다루는 코드들을 분리해보자는 방식으로 시도해보기로 했다.

뷰, 로직 파트 분리

Popup클래스에서 모든 것들을 컨트롤 했던 코드에서 Dom조작이 있는 부분들을 renderPopup함수로 분리하는 작업을 진행했다.
렌더링 방식은 Dom 변화가 필요한 경우 Popup클래스의 init메서드를 재호출 하고 init안에서 상황에 따라 맞는 화면을 미리 설정한 type을 지정해서 renderPopop함수를 호출하는 방식으로 진행했다.

Dom조작과 로직을 분리했다는 점에서 분명 이전보다는 낫다고 생각이 드는데 아직 제대로 된 리팩토링이 됐다는 느낌은 들지 않았다...

오랜 고민 끝에 리액트와 같은 상태기반 렌더링 컴포넌트를 바탕으로 리팩토링을 진행했다.

적용 이유

1.컴포넌트

의미부터 재사용이 가능한 독립된 모듈이란 것에서 알 수 있듯이 코드들의 독립성과 재사용성이 보장이되는 구조이다. 또한 형식의 통일성까지 이루어지기 때문에 유지,보수에도 기존의 방식보다 훨씬 효율적이다.

2.상태기반 렌더링

상태기반 렌더링은 각 컴포넌트가 가진 상태에 따라서 Dom이 조작된다.
즉,

  • 직접적인 Dom조작이 필요없다.
  • html을 따로 만들어줄 필요없다.
  • 데이터의 흐름이 단방향이기 때문에 구조가 단순해진다.
  • 상태에 따라 렌더링이 진행된다.

이런 장점들이 있다. 기존에 바닐라 자바스크립트로 프로젝트를 진행했기 때문에 이런 방식들이 기존보다 체계적이고 안정적인 작업을 이룰 수 있을 것같아 이 방식을 선택했다.

React Life Cycle

리액트의 라이프 사이클을 기반으로 컴포넌트를 만드는 작업을 진행했다.

Constructor

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 메서드 순으로 실행을 해준다.

render

// Component.ts
.
.
.
render(): void {
  addComponents.call(this);
  this.createChildComponents();
  this.setEvent();
}
  • addComponents: this노드에 하위 컴포넌트들을 연결해주는 함수
  • createChildComponents: 연결된 하위 컴포넌트를 불러오는 메서드
  • setEvent: 생성된 컴포넌트들에 이벤트를 주입하는 메서드

addComponents

// 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();
}
.
.

임의로 만든 엘리먼트를 실제 돔으로 변경하는 작업이다.

  • node: 기존 element(알아보기 쉽게 임의로 만든 Element)
  • newNode: this.template메서드의 string을 html로 파싱
  • newNode를 기존 node 다음 자리에 추가 후 this.node를 newNode로 변경
  • node의 클래스를 변경된 노드에 복사
  • 기존의 node를 삭제

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에 들어간 새로운 자식 컴포넌트들을 불러오는 메서드

  • addComponents로 LogoContainer, RepoContainer 엘리먼트가 렌더링되고 createChildComponents에서 이 컴포넌트들을 이어서 렌더링 하는 방식으로 작동한다.

componentDidMount

// Repository.ts  
async componentDidMount() {
    const repos = await getUserRepos();
    this.setState({repos});
  }

컴포넌트가 생성될 때 render메서드가 완료된 후 한번만 작동하는 메서드이다. 보통 외부 데이터, 라이브러리를 사용해서 Dom의 변경이 필요한 경우 사용된다.

  • 위는 repository데이터를 받아와서 리렌더링 작업을 수행시키기 위해 가져온 경우입니다.

setState

// 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메서드가 실행되어 컴포넌트작업을 다시 수행시킨다. 상태의 변화가 없을 때 불필요한 리렌더링을 방지해준다.

  • checkChangeState: 새로입력된 상태가 기존과 다른지 체크해주는 메서드
  • isSameTwo: 두 상태를 비교해서 변화가 없으면 false, 변화가 있으면 true 반환

이렇게 state를 기반으로 렌더링이 되는 형식을 적용해봤다. 기존의 Home의 Popup 컴포넌트의 경우는 외부 웹사이트와는 관련이 없기 때문에 매끄럽게 잘 적용되었던 것같다. 하지만 알고리즘 사이트에 적용이되는 컴포넌트들은 완전하게 적용이 된 느낌은 아닌 것같다. 그 부분들은 다른 형식의 패턴을 적용시키던지 고민이 조금 더 필요해 보인다.

0개의 댓글

관련 채용 정보