Qwik
The Resumable Framework
Resumability 기반 차세대 웹 프레임워크. Angular 창시자 Miško Hevery가 개발. Hydration 없이 즉시 인터랙티브한 초고속 웹앱 구현.
The Resumable Framework
Resumability 기반 차세대 웹 프레임워크. Angular 창시자 Miško Hevery가 개발. Hydration 없이 즉시 인터랙티브한 초고속 웹앱 구현.
Qwik은 "Resumability(재개 가능성)"라는 혁신적인 개념을 도입한 차세대 웹 프레임워크입니다. 기존 프레임워크들이 서버에서 렌더링한 HTML을 클라이언트에서 다시 실행(Hydration)해야 인터랙티브해지는 것과 달리, Qwik은 서버에서 실행된 애플리케이션 상태를 HTML에 직렬화(serialize)하여 클라이언트에서 그대로 "재개"할 수 있습니다. 이를 통해 JavaScript 로드와 실행 없이도 즉시 상호작용이 가능합니다.
Qwik은 Angular의 창시자이자 Google의 Engineering Director였던 Miško Hevery가 Builder.io에서 개발했습니다. 그는 Angular와 React 등 기존 프레임워크의 성능 한계를 직접 경험하며, 근본적으로 다른 접근 방식이 필요하다고 판단했습니다. Qwik은 "O(1) JavaScript"를 목표로 하며, 애플리케이션 크기와 상관없이 일정한 초기 로딩 성능을 유지합니다.
Hydration 대신 Resumability를 사용하는 것이 Qwik의 핵심 차별점입니다. 전통적인 SSR 프레임워크는 서버에서 HTML을 생성하고, 클라이언트에서 전체 JavaScript를 다운로드한 후 컴포넌트 트리를 다시 실행하여 이벤트 리스너를 연결합니다. 이 과정이 바로 Hydration이며, 번들 크기에 비례하여 TTI(Time To Interactive)가 늦어집니다. Qwik은 이벤트 리스너와 상태를 HTML 속성으로 직렬화하고, 실제 사용자 인터랙션이 발생할 때만 필요한 코드를 lazy load합니다.
실무에서 Qwik은 초고속 로딩과 우수한 SEO를 필요로 하는 프로젝트에 이상적입니다. 특히 이커머스, 마케팅 사이트, 콘텐츠 중심 웹앱에서 Core Web Vitals 점수를 극적으로 향상시킬 수 있습니다. Qwik City라는 메타 프레임워크를 통해 파일 기반 라우팅, 데이터 로딩, 미들웨어 등 풀스택 기능을 제공하며, React 개발자에게 익숙한 JSX 문법을 사용합니다.
| 특성 | Hydration (React, Vue) | Resumability (Qwik) |
|---|---|---|
| 초기 JS 로드 | 전체 번들 다운로드 필요 | 거의 0KB (필요 시 로드) |
| TTI (상호작용 시간) | 번들 크기에 비례하여 증가 | 앱 크기와 무관하게 즉시 |
| 서버 작업 | HTML 생성 후 클라이언트가 재실행 | 상태를 직렬화하여 클라이언트가 재개 |
| 이벤트 리스너 | Hydration 완료 후 연결 | HTML에 인라인되어 즉시 동작 |
| 메모리 사용 | 전체 컴포넌트 트리 메모리에 유지 | 인터랙션된 부분만 메모리에 로드 |
실무 팁: Qwik은 특히 느린 네트워크 환경(3G)이나 저사양 모바일 기기에서 체감 성능 차이가 극대화됩니다. Google의 Core Web Vitals에서 FID(First Input Delay)와 INP(Interaction to Next Paint) 개선에 효과적입니다.
// src/components/counter.tsx - Qwik 컴포넌트
import { component$, useSignal, $ } from '@builder.io/qwik';
// component$: Qwik의 컴포넌트 정의 ($ 접미사가 핵심)
export const Counter = component$(() => {
// useSignal: 반응형 상태 (React의 useState와 유사)
const count = useSignal(0);
// $: 이벤트 핸들러를 lazy-loadable하게 만듦
const increment = $(() => {
count.value++;
console.log('클릭됨! 이 코드는 클릭 시점에 로드됩니다');
});
// 서버에서 실행되는 함수
const fetchData = $(async () => {
// 이 코드도 호출 시점에 lazy load됨
const response = await fetch('/api/data');
return response.json();
});
return (
<div class="counter">
<p>Count: {count.value}</p>
{/* onClick$: 이벤트 핸들러도 $ 접미사 사용 */}
<button onClick$={increment}>+1 증가</button>
{/* 인라인 핸들러도 가능 */}
<button onClick$={() => count.value--}>-1 감소</button>
<button onClick$={fetchData}>데이터 불러오기</button>
</div>
);
});
// 상품 카드 컴포넌트 - Props 사용 예시
interface ProductProps {
name: string;
price: number;
imageUrl: string;
}
export const ProductCard = component$<ProductProps>((props) => {
const isHovered = useSignal(false);
// useTask$: 서버/클라이언트에서 실행되는 사이드 이펙트
// useVisibleTask$: 클라이언트에서만 (DOM 필요한 경우)
return (
<article
class={`product-card ${isHovered.value ? 'hovered' : ''}`}
onMouseEnter$={() => isHovered.value = true}
onMouseLeave$={() => isHovered.value = false}
>
<img
src={props.imageUrl}
alt={props.name}
width={300}
height={200}
/>
<h3>{props.name}</h3>
<p class="price">{props.price.toLocaleString()}원</p>
<button onClick$={() => alert(`${props.name} 장바구니 추가`)}>
장바구니 담기
</button>
</article>
);
});
// Qwik City - 파일 기반 라우팅 구조
// src/routes/
// ├── index.tsx → /
// ├── about/
// │ └── index.tsx → /about
// ├── products/
// │ ├── index.tsx → /products
// │ └── [id]/
// │ └── index.tsx → /products/:id (동적 라우트)
// └── layout.tsx → 레이아웃 (모든 페이지 공통)
// ============================================
// src/routes/products/[id]/index.tsx
// 동적 라우팅 + 데이터 로딩 예시
// ============================================
import { component$ } from '@builder.io/qwik';
import { routeLoader$, useLocation } from '@builder.io/qwik-city';
import type { DocumentHead } from '@builder.io/qwik-city';
// routeLoader$: 서버에서 데이터 로딩 (Next.js의 getServerSideProps와 유사)
// 이 함수는 절대 클라이언트에 전송되지 않음 (서버 전용)
export const useProductData = routeLoader$(async (requestEvent) => {
const productId = requestEvent.params.id;
// DB 직접 접근 가능 (서버에서만 실행됨)
const product = await db.products.findUnique({
where: { id: productId }
});
if (!product) {
// 404 페이지로 리다이렉트
throw requestEvent.redirect(302, '/404');
}
return product;
});
// 페이지 컴포넌트
export default component$(() => {
// useLoader의 반환값 사용
const product = useProductData();
const loc = useLocation();
return (
<main class="product-detail">
{/* .value로 데이터 접근 */}
<h1>{product.value.name}</h1>
<p class="price">{product.value.price.toLocaleString()}원</p>
<p>{product.value.description}</p>
<!-- 현재 URL 정보 -->
<p>현재 경로: {loc.url.pathname}</p>
<p>상품 ID: {loc.params.id}</p>
</main>
);
});
// SEO: 동적 메타 태그 설정
export const head: DocumentHead = ({ resolveValue }) => {
const product = resolveValue(useProductData);
return {
title: `${product.name} | My Shop`,
meta: [
{ name: 'description', content: product.description },
{ property: 'og:title', content: product.name },
{ property: 'og:image', content: product.imageUrl },
],
};
};
// ============================================
// src/routes/layout.tsx - 레이아웃 컴포넌트
// ============================================
import { component$, Slot } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<header>
<nav>
<a href="/">홈</a>
<a href="/products">상품</a>
<a href="/about">소개</a>
</nav>
</header>
{/* Slot: 자식 페이지가 렌더링되는 위치 */}
<Slot />
<footer>
<p>© 2024 My Shop</p>
</footer>
</>
);
});
"Core Web Vitals 점수가 계속 안 나와서 코드 스플리팅도 해보고 lazy loading도 적용했는데 한계가 있어요. 근본적인 문제는 Hydration이에요. 페이지 로드되고 React 번들 다운로드하고 다시 실행해서 이벤트 연결할 때까지 사용자가 기다려야 하잖아요. Qwik은 Resumability 덕분에 이 과정이 없어서 TTI가 거의 0에 가까워요. 특히 이커머스처럼 초기 로딩이 매출에 직결되는 서비스라면 진지하게 고려해볼 만합니다."
"React가 나쁜 게 아니라 Hydration 아키텍처 자체의 한계예요. React 18의 Streaming SSR이나 Server Components로 개선하고 있지만 여전히 클라이언트에서 JavaScript 실행이 필요해요. Qwik은 아예 다른 접근인데, $ 접미사로 모든 함수를 serializable하게 만들어서 서버 상태를 HTML에 인코딩해요. 대신 생태계가 React만큼 성숙하지 않고, 팀원들 학습 곡선도 고려해야 해요. 새 프로젝트면 Qwik, 기존 React 프로젝트면 Next.js 13+ App Router가 현실적인 선택이죠."
"Qwik의 핵심 혁신은 Resumability입니다. 기존 SSR은 서버에서 렌더링하고 클라이언트에서 Hydration으로 다시 부팅하는데, Qwik은 서버 실행 상태를 HTML 속성으로 직렬화해서 클라이언트가 중단된 지점에서 재개합니다. $ 접미사가 붙은 함수들은 빌드 타임에 별도 청크로 분리되어 실제 호출 시점에만 네트워크 요청됩니다. 이게 O(1) JavaScript를 가능하게 합니다."
"이 핸들러에서 클로저로 외부 객체를 캡처하고 있는데, Qwik에서는 직렬화 가능한 값만 $ 함수 안에서 참조할 수 있어요. 클래스 인스턴스나 DOM 참조는 직렬화가 안 됩니다. useSignal로 상태를 관리하거나, 필요한 값만 파라미터로 전달하세요. 그리고 useVisibleTask$는 정말 필요한 경우에만 쓰세요. 이게 클라이언트에서만 실행되는 거라 Resumability의 장점을 잃게 됩니다. useTask$로 대체할 수 있는지 먼저 검토해보세요."
React나 Vue에 비해 라이브러리와 UI 컴포넌트 생태계가 제한적입니다. 일부 npm 패키지는 Qwik의 직렬화 모델과 호환되지 않아 래퍼를 작성하거나 대안을 찾아야 할 수 있습니다. 커뮤니티 규모도 작아서 Stack Overflow나 블로그 자료가 부족합니다.
$ 접미사(달러 사인)와 Serialization 규칙을 이해해야 합니다. 함수, 이벤트 핸들러, 훅에 $를 붙이는 패턴이 처음에는 어색할 수 있고, 직렬화 불가능한 값(클래스 인스턴스, DOM 참조 등)을 넘기면 런타임 에러가 발생합니다. React 경험자도 멘탈 모델 전환에 시간이 필요합니다.
최신 브라우저를 타겟으로 하며, IE는 지원하지 않습니다. 또한 Resumability를 위해 서버 사이드 렌더링이 필수이므로 정적 호스팅만으로는 Qwik의 장점을 활용할 수 없습니다. Cloudflare Workers, Vercel Edge, AWS Lambda@Edge 등 엣지 런타임 배포를 권장합니다.