컴포넌트 레지스트리 우회
이 사례의 핵심은 “컴포넌트를 잘 등록하자"가 아니라
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에서는 컴포넌트 연결 경로를 다음처럼 고정한다.
화면은 컴포넌트를 직접 고르지 않는다
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를 거치지 않으면 화면에 연결할 수 없다"가 하네스다.