UI 렌더링 우회

UI 렌더링 우회

이 사례의 핵심은 “렌더링을 잘 사용하자"가 아니라
renderer를 거치지 않으면 UI를 생성할 수 없게 만드는 것이다.

개요

렌더링 계층을 우회하지 못하게 만드는 것은 단순한 코드 스타일 문제가 아니라
UI가 생성되는 경로를 구조적으로 하나로 고정하는 문제다.

Glif에서는 다음을 목표로 한다.

  • UI는 반드시 renderer를 통해 생성된다
  • renderer 외부에서 만들어진 UI는 허용되지 않는다
  • 위반은 코드 리뷰가 아니라 정적 분석 또는 런타임에서 실패한다

이 문서의 질문

이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 direct path bypass에 가장 가깝다. 핵심 질문은 다음과 같다.

화면이 renderer와 공식 surface를 거쳐서만 생성되는가

문제

다음과 같은 코드가 반복적으로 생성된다.

  • document.createElement(...)
  • element.innerHTML = ...
  • appendChild(...)
  • 프레임워크 외부에서 DOM 삽입

이 방식은 빠르게 결과를 만든다.
하지만 구조적으로 다음 문제가 발생한다.

  • 렌더링 경로가 2개 이상으로 분기됨
  • 상태 기반 UI가 깨짐
  • lifecycle이 무력화됨
  • 디버깅 불가능한 UI 생성

즉 문제는 DOM API가 아니라
렌더링 경로가 시스템 밖으로 새는 것이다.

문제의 본질

사람과 AI는 다음 기준으로 코드를 만든다.

  • 가장 빠르게 화면을 만드는 경로
  • 기존 구조 이해 없이 동작하는 방식
  • 즉시 눈에 보이는 결과

renderer는 이 기준에서 불리하다.

  • 상태 연결 필요
  • 간접 생성 구조
  • 초기 진입 비용 존재

그래서 AI는 renderer를 우회하고
결과를 직접 생성하는 방향으로 수렴한다.

목표 구조

Glif에서는 렌더링 경로를 다음처럼 고정한다.

D2 diagram
핵심 규칙은 다음과 같다.
  • DOM은 renderer를 통해서만 생성된다

  • renderer 외부에서 생성된 노드는 허용되지 않는다

  • 렌더링 경로는 단일하다

하네스 적용 위치

이 문제는 세 레벨에서 동시에 막는다.

구조 레벨

UI 생성 책임을 renderer로 집중시킨다.

  • UI는 renderer 결과만 소비

  • DOM 생성 API는 직접 사용하지 않음

  • UI 레이어에서 DOM 접근을 구조적으로 차단

검증 레벨

정적 분석으로 DOM API 사용을 탐지한다.

  • document.createElement

  • innerHTML

  • appendChild

  • insertAdjacentHTML

이 규칙은 warning이 아니라 error로 강제한다.

실행 레벨

런타임에서도 우회를 감지한다.

  • renderer 외부 DOM 변경 탐지

  • 예상되지 않은 노드 삽입 감지

  • 위반 시 즉시 오류 발생 또는 제거

실제 구현

DOM 접근 차단 (ESLint)

UI 레이어에서 DOM 직접 접근을 금지한다.

// .eslintrc.js
module.exports = {
  rules: {
    "no-restricted-properties": [
      "error",
      {
        object: "document",
        property: "createElement",
        message: "Use renderer instead of direct DOM creation"
      }
    ],
    "no-restricted-syntax": [
      "error",
      {
        selector: "AssignmentExpression[left.property.name='innerHTML']",
        message: "Direct innerHTML is forbidden"
      }
    ]
  }
};

이 규칙은 선택이 아니라 필수다.
위반 시 빌드가 깨진다.

DOM Wrapper 강제

DOM은 직접 접근하지 않고 wrapper를 통해서만 접근한다.

export function mount(vnode: VNode): HTMLElement {
  // renderer가 만든 vnode만 허용
  return internalMount(vnode);
}

UI 코드에서는 DOM을 직접 다루지 않는다.

Runtime Guard (Mutation 감지)

const observer = new MutationObserver((mutations) => {
  for (const m of mutations) {
    if (!isFromRenderer(m)) {
      throw new Error("DOM mutation outside renderer is not allowed");
    }
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

개발 모드에서는 renderer 외 변경을 즉시 감지한다.

위반 예시와 실패 결과

다음 코드는 반드시 실패해야 한다.

const el = document.createElement("div");
el.innerHTML = "<div>Hello</div>";
document.body.appendChild(el);

실패 결과:

error: Direct DOM manipulation is forbidden. Use renderer.

또는 런타임에서:

Error: DOM mutation outside renderer is not allowed

이 순간부터 규칙은 문서가 아니라
실행 가능한 제약이 된다.

보조 강제 수단

이 케이스의 보조 강제 수단은 DOM access를 renderer 내부로 수렴시키는 데 있다. 정적 차단만 두면 helper wrapper가 새고, 타입 제한만 두면 runtime mutation이 남고, 테스트가 raw DOM을 허용하면 우회 패턴이 다시 학습된다.

  • ESLint → 정적 차단
  • 타입 시스템 → renderer 입력 surface 제한
  • runtime guard → renderer 밖 DOM 변경 차단
  • 테스트 → direct DOM path 회귀 방지

핵심은 DOM을 금지 API로 다루는 것이 아니라 renderer 바깥에서는 사실상 불필요하고 실패하는 경로로 만드는 것이다.

결과

하네스 적용 이후 변화는 명확하다.

  • DOM 생성 경로가 renderer 한곳으로 다시 모인다.
  • state와 lifecycle에 대한 가정이 표면 전체에서 다시 일관된다.
  • renderer 바깥 DOM 조작은 조용한 drift가 아니라 즉시 보이는 실패가 된다.

실무 적용 팁

DOM을 “금지"가 아니라 “불필요"하게 만든다

renderer만으로 충분한 API를 제공해야 한다.

  • append 대신 mount

  • html 문자열 대신 vnode

Dev / Prod 전략 분리

  • Dev: 강한 runtime guard

  • Prod: 최소한의 검증

테스트에서 우회 차단

  • snapshot 기반 렌더 검증

  • DOM 직접 생성 테스트 금지

요약

이 케이스에서 하네스는 다음과 같이 구성된다.

  • UI는 renderer를 통해서만 생성된다

  • DOM 직접 접근은 ESLint로 차단된다

  • renderer 외 DOM 변경은 런타임에서 감지된다

  • 위반은 빌드 또는 실행 단계에서 실패한다

결과적으로

  • 렌더링 경로는 하나로 고정되고

  • 상태 기반 UI만 남으며

  • 직접 생성이라는 우회 경로는 제거된다

“renderer를 사용하라"가 아니라
“renderer를 거치지 않으면 UI를 만들 수 없다"가 하네스다.