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에서는 렌더링 경로를 다음처럼 고정한다.
DOM은 renderer를 통해서만 생성된다
renderer 외부에서 생성된 노드는 허용되지 않는다
렌더링 경로는 단일하다
하네스 적용 위치
이 문제는 세 레벨에서 동시에 막는다.
구조 레벨
UI 생성 책임을 renderer로 집중시킨다.
UI는 renderer 결과만 소비
DOM 생성 API는 직접 사용하지 않음
UI 레이어에서 DOM 접근을 구조적으로 차단
검증 레벨
정적 분석으로 DOM API 사용을 탐지한다.
document.createElementinnerHTMLappendChildinsertAdjacentHTML
이 규칙은 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를 만들 수 없다"가 하네스다.