Just Web Components그냥 Web Components
Standard custom elements.
No runtime to adopt.표준 커스텀 엘리먼트.
따라 들일 런타임 없음.
Every component is a realcustomElements.define()— extendHTMLElement, or a built-in
like<body>viais="…". No virtual DOM, no template compiler:the browser is the framework.
Drop them into plain HTML or any app that renders DOM. Pass awindowand the same class registers into
an iframe — or aserver-side DOM, which is how SSR works here.모든 컴포넌트는 진짜customElements.define()입니다 —HTMLElement를 상속하거나,is="…"로<body>같은 내장 엘리먼트를 확장하세요. 가상 DOM도 템플릿 컴파일러도 없습니다:브라우저가 곧 프레임워크. 순수 HTML이든 DOM을 그리는 어떤 앱이든 그냥 넣으면 됩니다.window를 넘기면
같은 클래스가 iframe에도 —서버 쪽 DOM에도 등록됩니다. 여기 SSR이 그렇게 돌아요.
@elementDefine('user-card', {window: w })classUserCardextendsw.HTMLElement{ … }@elementDefine('app-body', {window: w,extends:'body'})classAppBodyextendsw.HTMLBodyElement{ … }<user-card user-id="7"></user-card>
Decorator Stack데코레이터 스택
Your method returns a value.
Decorators decide where it lands.메서드는 값을 반환하세요.
데코레이터가 둘 곳을 정합니다.
Triggers saywhen. Outputs saywhere. Hooks saywhether. None of them know about each other —
swap the trigger and the output doesn't even notice. The method body stays a pure function: no DOM, no plumbing.트리거는언제, 아웃풋은어디로, 훅은통과 여부를 맡습니다. 서로를 모르죠 —
트리거를 바꿔도 아웃풋은 눈치채지 못합니다. 메서드 본문은 순수 함수로 남습니다. DOM도, 배관도 없습니다.
@replaceChildrenLight({valueKey:'element'})@subscribeSwcAppRouteChange('/releases/{seq}')@attribute('nav','href', {valueKey:'href'})handleRelease(route:RouterEventType) {const{ seq } = route.pathData;return{element:ReleasePage(w, {attrs: { seq } }),href:'releases',
};
}
Shadow ⇄ Light섀도우 ⇄ 라이트
Shadow DOM or light DOM.
Choose per decorator.섀도우 DOM이냐 라이트 DOM이냐.
데코레이터마다 고르세요.
Encapsulated chrome in the shadow root, crawlable content in the light DOM —on the same element, at once.
Every output, event, query and observer takesroot: 'shadow' | 'light' | 'all' | 'auto'and comes as a named
variant:innerHtmlShadow,eventDelegateLight,queryShadow,mutationObserverAll…autofollows whether a shadow root exists, so moving a component between the two is a one-word change.캡슐화된 겉모습은 섀도우 루트에, 검색되는 본문은 라이트 DOM에 —한 엘리먼트에서, 동시에.
모든 아웃풋·이벤트·쿼리·옵저버가root: 'shadow' | 'light' | 'all' | 'auto'를 받고, 이름 붙은 변형으로도 옵니다:innerHtmlShadow,eventDelegateLight,queryShadow,mutationObserverAll…auto는 섀도우 루트가 있는지를 따라가서, 둘 사이를 옮기는 건 단어 하나 바꾸기입니다.
<my-article>#shadow-root ·the look겉모습
<style> <h1> <slot>@innerHtmlShadow@eventShadow
light DOM shows through <slot>라이트 DOM이 <slot> 자리에 보여요
light DOM ·the content본문
<article>…</article>@innerHtmlLight@eventDelegateLight
@mutationObserverAllwatches both둘 다 감시
@onConnectedBodyShadowrenderShell() {return'<style>…</style><h1></h1><slot></slot>';
}@onConnectedBodyLightrenderBody() {return'<article>…</article>'; }@innerHtmlShadow('h1')setTitle(t:string) {returnt; }@eventDelegateLight('article a','click')onLink(@matchedElementa:HTMLAnchorElement) { … }@mutationObserverAllonAnyChange() { … }
Hooks Everywhere어디에나 훅
filter → before → handler → finally.
On every trigger.filter → before → handler → finally.
모든 트리거에.
Clicks, mutations, resizes, media queries, attribute changes, timers, routes and messages
all shareone lifecycle. Gate it asynchronously, prepare data and inject it straight into the handler,
clean up on error — the same way, every single time.클릭, 뮤테이션, 리사이즈, 미디어쿼리, 속성 변경, 타이머, 라우트, 메시지가
전부하나의 라이프사이클을 씁니다. 비동기로 걸고, 데이터를 준비해서 핸들러에 바로 꽂고,
에러 나면 정리 — 매번 똑같이.
@eventClick('.save', {filter:async() =>awaitauth.canEdit(),before:async() =>awaitloadDraft(),finally:(_e, _meta, { error }) =>spinner.hide(error),
})onSave(@eventBeforeReturndraft:Draft) {returnsave(draft);
}
Order-free Injection순서 없는 주입
Ask for arguments by name.
In any order.인자는 이름으로 받으세요.
순서는 상관없이.
Events, lifecycles, routes, messages and observers all take their arguments throughparameter decorators— the event, the matched element, the host tree, abeforeresult,
and DI (dependency injection) services via@inject, mixed in one signature. Decorate nothing and the old positional
arguments still arrive:nothing that worked breaks.이벤트, 라이프사이클, 라우트, 메시지, 옵저버가 전부파라미터 데코레이터로 인자를 받습니다 —
이벤트, 매칭된 엘리먼트, 호스트 트리,before결과, 그리고@inject로 DI(의존성 주입) 서비스까지
한 시그니처에 섞어서. 아무것도 안 붙이면 예전 위치 인자가 그대로 옵니다:되던 건 안 깨집니다.
@onConnectedAfterinit(@hostSeths:HostSet,@inject(UserService.SYMBOL)users:UserService,) {}@eventClick('.save', {before:() =>loadDraft() })onSave(@matchedElementbtn:HTMLButtonElement,@eventBeforeReturndraft:Draft,) {}
@fetch@fetch
HTTP without the boilerplate.
Declare it, call it, keep it.보일러 없는 HTTP.
선언하고, 부르고, 기억하고.
Put it where it fits. On atrigger handlerthe request arrives as a plainPromiseand your method keeps
the return type you wrote. On adeclarefield typed as afunction, the framework writes the request function —
the declared type is the real signature. On a field typed as aPromise, it's data: fetched on first read, remembered,
cleared with= undefined. Failures reject (no try/catch, justthen), stale requests are aborted,
and everything stops when the element leaves.맞는 자리에 붙이세요.트리거 핸들러에 붙이면 요청이 평범한Promise로 오고 메서드는 쓴 반환 타입 그대로.함수타입의declare필드에 붙이면 요청 함수를 프레임워크가 만듭니다 — 선언한 타입이 곧 실제 시그니처.Promise타입 필드에 붙이면 데이터가 됩니다 — 처음 읽을 때 불러오고, 기억하고,= undefined로 비웁니다.
실패는 reject(try/catch 없이then으로), 묵은 요청은 끊고, 엘리먼트가 떠나면 전부 멈춥니다.
@fetchGet((_s, _h, [id]) =>`/api/posts/${id}`)declaregetPost:(id:number) =>Promise<Post>;@fetchGet('/api/me')declareme?:Promise<User>;@eventClick('.load')@innerHtml('.out', {fallback:() =>'Loading…'})load() {returnthis.getPost(1).then(p=>p.title);
}reload() {this.me=undefined; }
App Message Bus앱 메시지 버스
Signals are cheap.
Ship the data.시그널은 싸다.
데이터를 실어 보내라.
Publish byreturning a value. Subscribe with a decorator or with RxJS-styleobserveMessage().
Late subscribers replay the last message. Your header doesn't fetch the user after login —it receives it.Or just keep the latest onein a field.값을반환하면 발행입니다. 데코레이터나 RxJS식observeMessage()로 구독하세요.
늦게 온 구독자는 마지막 메시지를 재생합니다. 헤더는 로그인 뒤 유저를 fetch하지 않습니다 —받아요.아니면 최신 메시지를필드에그냥 두세요.
login-formreturn user@publishSwcAppMessage
AUTH_CHANGEDkeeps the last value마지막 값을 들고 있음
app-headershows the name이름 표시
side-menushows "My page""내 페이지" 표시
cart-badgejoined later — still gets it나중에 붙어도 받음(behavior)
@publishSwcAppMessage(AUTH_CHANGED)publishMe(me:User) {returnme; }@subscribeSwcAppMessageBehavior(AUTH_CHANGED)@innerHtml('.user')renderUser(@swcAppMessagemsg:SwcAppMessage<User>) {returnmsg.data?.name??'Sign in';
}@swcAppMessage(AUTH_CHANGED, {subject:'behavior'})me?:SwcAppMessage<User>;
Routing라우팅
No route table.
Elements subscribe to paths.라우트 표가 없습니다.
엘리먼트가 경로를 구독해요.
Any element can react to a path — the page draws itself, the menu highlights, the editor guards — each with one decorator,
and nothing to register centrally.{id}and query values arrive asarguments by name— or straight intofields.
Pick when to run:enter,update,leave, orbeforeLeaveto
stop unsaved changes from being lost — browser Back included. To move,return a path.
History or hash URLs, one option.어느 엘리먼트든 경로에 반응할 수 있어요 — 페이지는 스스로 그리고, 메뉴는 강조하고, 에디터는 지킵니다 — 각자 데코레이터 하나로,
한 곳에 등록할 것도 없이.{id}와 쿼리 값은이름으로 인자에들어옵니다 —필드에바로 받아도 되고요.
언제 돌지 고르세요:enter,update,leave, 그리고 저장 안 한 변경을 지키는beforeLeave— 브라우저 뒤로가기까지 막아요. 이동하려면경로를 반환하면 끝.
history 주소든 hash 주소든 옵션 하나.
/posts/42?tab=commentsthe URL changes주소가 바뀜return '/posts/42'@swcAppRouteGo
routerno route table — elements subscribe라우트 표 없음 — 엘리먼트가 구독
post-page'/posts/{id}' →draws post 4242번 글을 그림
side-navon: 'enter' →highlights "Posts""글" 메뉴 강조
post-editoron: 'beforeLeave' →return false blocks leavingfalse 반환이면 못 떠남
@subscribeSwcAppRouteChange('/posts/{id}')@innerHtml('.post')asyncshow(@swcAppRoutePathVariable('id')id:string,@swcAppRouteFirstQueryParam('tab')tab:string,) {returnthis.render(awaitthis.api.post(id), tab);
}@subscribeSwcAppRouteChange('/posts/{id}', {on:'beforeLeave',})guard() {return!this.dirty||confirm('Discard?');
}@eventClickDelegate('a', {preventDefault:true,
})@swcAppRouteGogo(@matchedElementa:HTMLAnchorElement) {returna.getAttribute('href');
}@swcAppRoutePathVariable('id', {path:'/posts/{id}'})postId?:string;@swcAppRouteQueryParam('tab')tab?:string|null;
Pooled Observers공유 옵저버
Stack observers freely.
One of each, per element.옵저버는 마음껏 쌓으세요.
엘리먼트당 하나씩만 생깁니다.
@mutationObserverand@resizeObserverare declarations, not instances.
However many you stack, an element sharesone MutationObserver and one ResizeObserver— delegate mode
tracks elements added later through that same MutationObserver. Bare@mutationObserverwatches the
whole subtree and is attachedbefore the first render, so it sees the render itself.
Everything disconnects when the element leaves.@mutationObserver와@resizeObserver는 인스턴스가 아니라 선언입니다.
몇 개를 쌓든 엘리먼트는MutationObserver 하나와 ResizeObserver 하나를 공유해요 — delegate 모드가
나중에 추가된 요소를 추적하는 것도 그 MutationObserver 하나로. 맨@mutationObserver는 하위 전체를 보고첫 렌더 전에붙어서 렌더 자체도 봅니다. 엘리먼트가 떠나면 전부 해제.
@mutationObserveronAnyChange(@hostSeths:HostSet) { … }@mutationObserverDelegate('.row')@resizeObserverDelegate('.row')onRow(rows:HTMLElement[]) { … }@resizeObserveronResize([el]:HTMLElement[]) { … }
Explicit Names이름이 곧 문서
Names you can read.
900+ of them.읽히는 이름.
900개 넘게.
Every DOM event × binding strategy is generated as its own decorator —eventClick,eventKeydownWindow,eventClickDelegateLight…900+ named aliases.
Autocomplete teaches the API and the name says what it does; no option strings to memorize.
The generic@event(...)is still there when you want it.DOM 이벤트 × 바인딩 방식마다 데코레이터가 따로 생성됩니다 —eventClick,eventKeydownWindow,eventClickDelegateLight…이름 붙은 별칭 900개 이상.
자동완성이 API를 가르치고 이름이 하는 일을 말해줍니다. 외울 옵션 문자열이 없어요.
범용@event(...)도 그대로 있습니다.
@eventClick('.save')onSave() { … }@eventKeydownWindowonKey(e:KeyboardEvent) { … }@eventClickDelegateLight('.row')onRow(@matchedElementrow:HTMLElement) { … }