컴포넌트 레지스트리 우회

컴포넌트 레지스트리 우회

이 사례의 핵심은 “컴포넌트를 잘 등록하자"가 아니라
registry를 거치지 않으면 화면에 연결할 수 없게 만드는 것이다.

개요

provider 경계까지 통제해도
UI는 여전히 구조를 무너뜨릴 수 있다.

그 이유는 마지막 순간에
임의 컴포넌트를 직접 연결하는 우회가 남아 있기 때문이다.

대표적으로 다음과 같은 방식이다.

  • 화면 파일에서 컴포넌트를 직접 import해서 바로 렌더링
  • registry에 없는 컴포넌트를 조건 분기로 직접 붙임
  • schema 기반 렌더링 대신 switch 문으로 ad-hoc 분기 추가
  • plugin slot에 승인되지 않은 컴포넌트 주입

즉 문제는 컴포넌트를 쓰는 것 자체가 아니라
어떤 컴포넌트가 화면에 들어갈 수 있는지가 구조 밖으로 새는 것이다.

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

  • 화면에 연결되는 컴포넌트는 registry를 통해서만 선택된다
  • registry 밖 컴포넌트는 렌더 경로에 진입할 수 없다
  • 위반은 코드 리뷰가 아니라 정적 분석 또는 런타임에서 실패한다

이 문서의 질문

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

화면에 연결되는 컴포넌트가 registry와 승인된 surface를 통해서만 선택되는가

문제

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

import { HeroCard } from "@/ui/cards/HeroCard";

function HomePage() {
  return <HeroCard />;
}

또는

function renderBlock(type: string) {
  if (type === "hero") return <HeroCard />;
  if (type === "quote") return <QuoteCard />;
  if (type === "cta") return <CtaBanner />;
  return null;
}

또는

const block = config.component;
return React.createElement(block, props);

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

  • 어떤 컴포넌트가 노출되는지 추적하기 어려움

  • 승인되지 않은 표현이 화면에 섞임

  • schema와 실제 렌더링 결과가 분리됨

  • plugin/slot 시스템이 사실상 무력화됨

  • AI가 registry를 건너뛴 가장 짧은 연결 경로를 계속 재생산함

즉 문제는 직접 렌더링이 아니라
컴포넌트 선택 권한이 아무 데나 흩어지는 것이다.

문제의 본질

AI는 registry를 시스템의 통제 장치보다
우회 가능한 편의 레이어로 보는 경향이 있다.

기준은 단순하다.

  • 필요한 컴포넌트를 바로 import하면 된다

  • registry key를 찾는 과정이 번거롭다

  • schema와 manifest를 이해할 필요가 없다

  • 눈앞의 화면을 가장 빨리 맞출 수 있다

그래서 다음과 같은 우회가 쉽게 발생한다.

  • 페이지 컴포넌트에서 feature component 직접 import

  • switch(type)로 renderer를 임시 복제

  • plugin slot에 raw component 주입

  • string key 대신 component reference 직접 전달

즉 사람과 AI는 registry를 보존하기보다
컴포넌트를 직접 꽂는 경로를 만든다.

목표 구조

Glif에서는 컴포넌트 연결 경로를 다음처럼 고정한다.

D2 diagram
핵심 규칙은 단순하다.
  • 화면은 컴포넌트를 직접 고르지 않는다

  • schema 또는 key가 registry를 통해 해석된다

  • registry에 없는 컴포넌트는 렌더링되지 않는다

하네스 적용 위치

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

구조 레벨

컴포넌트 선택 권한을 registry로 집중시킨다.

  • 페이지는 key 또는 schema만 가진다

  • 실제 component reference는 registry 안에만 존재한다

  • slot/plugin도 registry entry만 허용한다

즉 페이지는 부품 상자가 아니라
승인된 컴포넌트를 요청하는 소비자가 된다.

검증 레벨

정적 분석으로 registry 우회를 탐지한다.

  • page/screen 레이어의 직접 component import 금지

  • React.createElement(rawComponent) 금지

  • 임의 switch(type) renderer 금지

  • registry 외 component reference 전달 금지

실행 레벨

런타임에서도 registry 경계를 확인한다.

  • unknown key는 fallback 없이 실패

  • 승인되지 않은 component type 주입 시 throw

  • registry manifest와 실제 component 매핑 불일치 시 오류

테스트 레벨

테스트도 registry 중심으로만 작성한다.

  • 페이지 테스트는 schema fixture로만 렌더

  • raw component direct mount 금지

  • registry snapshot으로 노출 surface 검증

실제 구현

component 선택 권한을 registry 하나로 모은다

registry 패턴의 핵심은 screen이나 page가 concrete component를 직접 고르지 못하게 만드는 것이다.

type BlockKey = "hero" | "quote" | "cta";

type RegistryEntry<K extends BlockKey> = {
  key: K;
  component: React.ComponentType<BlockPropsMap[K]>;
};

const registry: { [K in BlockKey]: RegistryEntry<K> } = {
  hero: { key: "hero", component: HeroBlock },
  quote: { key: "quote", component: QuoteBlock },
  cta: { key: "cta", component: CtaBlock },
};

이제 page는 key와 schema만 알고, component reference는 registry 내부에만 남는다.

렌더링은 resolver를 통해서만 일어난다

export function renderBlock(node: BlockNode) {
  const entry = registry[node.type];
  if (!entry) {
    throw new Error(`Unknown block type: ${node.type}`);
  }
  const Component = entry.component;
  return <Component {...node.props} />;
}

이 구조가 있어야 page direct import, config 안의 raw component reference, registry bypass mount가 같이 줄어든다.

config에도 component reference를 허용하지 않는다

type SafeBlockConfig =
  | { type: "hero"; props: { title: string } }
  | { type: "quote"; props: { text: string } }
  | { type: "cta"; props: { label: string } };

config는 “무엇을 렌더할지”만 말할 수 있고, “어떤 컴포넌트를 직접 쓸지”까지는 말할 수 없어야 한다.

plugin과 외부 설정도 registry membership을 통과시킨다

slot이나 extension이 끼는 시스템에서는 runtime membership guard가 필요하다. 그렇지 않으면 plugin 경로가 곧 direct mount bypass가 된다.

테스트도 schema와 resolver를 통해서만 렌더한다

const pageSchema = [
  { type: "hero", props: { title: "Hello" } },
  { type: "quote", props: { text: "World" } },
] satisfies BlockNode[];

page 테스트가 render(<HeroBlock />)부터 시작하면 registry는 금방 장식으로 전락한다.

위반 예시와 실패 결과

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

import { HeroBlock } from "@/ui/blocks/HeroBlock";

function LandingPage() {
  return <HeroBlock title="Hello" />;
}

실패 결과:

error: Pages must not import UI blocks directly. Use registry resolver.

또는

const config = {
  component: HeroBlock,
  props: { title: "Hello" },
};

실패 결과:

error: Raw component references are forbidden. Use registry keys instead.

또는 승인되지 않은 component 주입:

Error: Component is not registered in registry

이 순간부터 컴포넌트 연결 규칙은 문서가 아니라
실행 가능한 제약이 된다.

보조 강제 수단

이 케이스의 보조 강제 수단은 component selection 권한을 registry 하나로 집중시키는 데 있다. registry type만 두면 raw render가 남고, resolver만 두면 direct import가 남고, runtime guard가 없으면 plugin과 slot 경로가 다시 우회 surface가 된다.

  • registry type → component 선택 경로 고정
  • resolver → direct render 차단
  • ESLint import restriction → page direct import 차단
  • runtime membership guard → plugin/slot 우회 차단
  • manifest consistency test → 승인 surface 검증

핵심은 registry를 편의 map이 아니라 화면 연결의 유일한 승인 지점으로 만드는 것이다.

결과

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

  • screen과 page는 concrete component 대신 key와 schema만 소비하게 된다.
  • direct import 경로가 사라지고 component 선택은 registry 한곳으로 수렴한다.
  • 새 블록 추가와 교체가 registry 변경으로 모여 구조 추적이 쉬워진다.

실무 적용 팁

registry는 단순 편의 맵이 아니다

registry가 optional이면
AI는 반드시 direct import로 돌아간다.

page는 component를 아는 것이 아니라 key를 알아야 한다

page가 component reference를 알기 시작하면
통제 지점은 곧 사라진다.

plugin 시스템은 항상 우회로가 된다

slot, extension, plugin은 유연성처럼 보이지만
검증 없는 component 주입 경로가 되기 쉽다.

fallback으로 unknown component를 삼키지 않는다

모르는 key를 빈 화면이나 기본 컴포넌트로 넘기면
registry 위반은 조용히 축적된다.

요약

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

  • 화면은 registry key 또는 schema만 가진다

  • component reference는 registry 내부에만 존재한다

  • page/screen 레이어의 direct component import는 정적 분석으로 차단된다

  • plugin과 slot 주입은 runtime membership guard로 검증된다

  • 테스트도 schema와 registry resolver를 통해서만 렌더한다

결과적으로

  • 컴포넌트 선택 권한은 registry로 집중되고

  • 승인된 surface만 화면에 연결되며

  • direct import와 raw component injection이라는 우회 경로는 제거된다

“컴포넌트를 잘 등록하라"가 아니라
“registry를 거치지 않으면 화면에 연결할 수 없다"가 하네스다.