본문 바로가기
project/reactify

Browser Router와 컴포넌트 메모이제이션

by qjatjs123123 2025. 2. 18.


안녕하세요!

'Reactify 개발기 2부 - 상태관리'에 이어, 이번에는 Browser Router, 컴포넌트 메모리제이션, 문제점 및 개선사항을 말씀드리고 

 

나만의 프레임워크 개발기는 마무리 지으려고 합니다.

 

 


 

Browser Router

React에서 SPA(Single Page Application) 방식으로 페이지를 전환할 때, 브라우저의 URL을 동적으로 관리하고, 페이지 전체를 새로고침하지 않고도 URL에 맞는 컴포넌트를 렌더링을 하기 위해 사용합니다.

 

즉, 기존에는 서버에서 완성된 HTML 파일을 내려받았으나, React와 같은 라이브러리에서는 경로에 맞는 자바스크립트를 사용해 필요한 컴포넌트만 업데이트하여 빠르고 효율적인 페이지 전환을 구현합니다.

 

또한 a태그는 이러한 SPA 본질과는 맞지 않기 때문에 사용하지 말아야 합니다.

 

 

저는 Browser Router를 history API 중 history.pushState을 사용하여 해결하였습니다.

history.pushState는 페이지 이동 없이 주소 표시줄(URL)을 변경할 수 있습니다.

 

 

코드를 통해 살펴봅시다.

다음은 Route 기능을 담당하는 클래스입니다.

export default class Route {
  private static instance: Route;
  private searchView;
  private ticketView;
  private root: View<null>;
  routes: { [key: string]: View<null> } = {};
  #stateStore: StateStore;

  private constructor(root: View<null>) {
    this.root = root;
    this.#stateStore = new StateStore();
    this.init();
    this.searchView = null;
    this.ticketView = null;
  }

  public static getInstance(root: View<null> | null): Route {
    if (!Route.instance && root) {
      Route.instance = new Route(root);
    }
    return Route.instance;
  }

  navigate(url: string) {
    history.pushState({}, "", url);
    this.root._element?.replaceWith(this.root.render() as HTMLElement);
  }

  init() {
    DROPBOX_ITEM.forEach(({ name, location }) => {
      if (location === window.location.pathname)
        this.#stateStore.setState("nav", { nav: name });
    });
  }

  browserRouter() {
    const path = window.location.pathname;

    if (path === "/") {
      throw import("../components/search/SearchView");
    } else if (path === "/ticket") {
      throw import("../components/ticket/TicketView");
    }
  }
}

 

먼저 이 클래스는 여러 인스턴스를 가지면 안되기 때문에 싱글톤으로 구현해야 합니다.

 

navigate함수는 history.pushState를 사용하여 주소 표시줄(URL)을 변경하고, root 컴포넌트를 재렌더링하는 역할을 합니다.

init함수는 함수명대로 초기화 해주는 역할을 담당합니다.

browserRoute함수는 현재 URL에 맞는 컴포넌트를 반환해주는 역할을 합니다.

 

 

즉, <a> 태그 대신 navigate 함수를 호출하여 특정 URL로 변경하고, root 컴포넌트를 재렌더링합니다.

그 후, 렌더링 과정에서 browserRoute 함수를 호출하여 현재 URL에 맞는 컴포넌트를 표시할 수 있습니다.

(dynamic Import 적용)

 

 


 

 

컴포넌트 메모리제이션

이 프레임워크는 부모 컴포넌트가 렌더링되면, 하위 모든 컴포넌트들도 함께 렌더링됩니다.

이 과정에서 하위 컴포넌트들이 다시 새로운 DOM 요소를 만들어 기존 것과 교체하는 로직이 실행됩니다.

 

즉 props나 데이터 변경이 없어도 재렌더링 된다는 문제가 있었습니다.

 

저는 이러한 문제를 React에서 memo 기법을 응용하여 해결하였습니다.

React에서 memo된 컴포넌트는 props가 변하지 않는다면 이전에 렌더링한 결과값을 반환합니다.

 

코드를 살펴 보겠습니다.

export default class ViewStore {
  private static instance: ViewStore;
  private viewMap: Map<View<any>, [View<any>, HTMLElement | null]>;
  public _viewMap : Map<String, View<any>>;

  private constructor() {
    this.viewMap = new Map();
    this._viewMap = new Map();
  }

  public static getInstance(): ViewStore {
    if (!ViewStore.instance) {
      ViewStore.instance = new ViewStore();
    }
    return ViewStore.instance;
  }

  public setViewMap(key: String, view: View<any>): void {
    this._viewMap.set(key, view);
  }

  public removeViewMap(key: String): void {
    this._viewMap.delete(key);
  }

  public getViewMap(key: String): View<any> {
    return this._viewMap.get(key);
  }

  public setViewMemo(view: View<any>, element: HTMLElement): void {
    this.viewMap.set(view, [view, element]);
  }

  public removeViewMemo(view: View<any>): void {
    this.viewMap.delete(view);
  }

  public clear(): void {
    this.viewMap.clear();
  }

  public getViewMemo(view: View<any>): HTMLElement | null {
    return this.viewMap.get(view)?.[1] ?? null;
}

  public isValidMemo(view: View<any>): boolean {
    const existingView = this.viewMap.get(view);
    if (!existingView) return false; 

    return this.isEquals(existingView[0], view);
  }
 
  private isEquals(obj1: any, obj2: any): boolean {
    return Object.is(obj1, obj2);
  }

}

 

ViewStore는 컴포넌트의 메모리제이션을 관리하는 클래스입니다.

또한 여러 인스턴스를 가질 수 없기에 싱글턴 패턴으로 구현하였습니다.

 

ViewStore에 컴포넌트와 해당 컴포넌트의 DOM 요소를 Key-Value 형식으로 저장하고,

랜더링시, 컴포넌트의 props를 비교하여 메모이제이션된 DOM 요소를 반환합니다.

 

 

이러한 메모리제이션 기법으로 랜더링을 최적화 할 수 있었습니다.

 

 

 


 

 

문제점 및 개선사항

비효율적인 렌더링 방식입니다.

 

렌더링 시마다 새로운 DOM 요소를 생성하고 기존 요소를 교체하는 방식으로 개발했는데, 이는 단순히 비효율적일 뿐만 아니라 CSS Transition 효과가 적용되지 않는 문제도 발생했습니다.

그 이유는 CSS Transition은 '변경 전(before)'과 '변경 후(after)' 상태가 모두 존재해야 하지만, 기존 요소(before)가 사라지면서 효과가 적용되지 않기 때문입니다.

 

 

이러한 문제는 가상 돔(Virtual DOM)으로 해결할 수 있습니다. 

가상 돔은 실제 태그가 변하지 않는 이상 기존 DOM 요소에 변경된 요소만 패치합니다.

 

그렇기 때문에 가상돔을 사용한다면 렌더링 성능을 개선할 수 있고, Transition 문제도 해결할 수 있습니다.

 

 

 

감사합니다!