본문으로 건너뛰기
Previous
Next
Qwik Complete Guide | Resumable JavaScript Framework

Qwik Complete Guide | Resumable JavaScript Framework

Qwik Complete Guide | Resumable JavaScript Framework

이 글의 핵심

Qwik is a new framework that delivers instant-loading web apps through resumability. Unlike React/Vue, Qwik serializes app state on the server and resumes on the client without hydration.

Introduction

Qwik is a new kind of web framework that solves the hydration problem. Instead of shipping JavaScript to re-create the app state on the client, Qwik serializes the state on the server and resumes execution on the client.

Created by Miško Hevery (creator of Angular and AngularJS) and backed by Builder.io, Qwik represents a fundamental rethinking of how web frameworks should work. The core innovation — resumability — eliminates hydration entirely.

Why Qwik Matters

The hydration tax:

  • Next.js/Remix apps ship 40-200KB JavaScript that runs immediately
  • Users wait 1-3 seconds on mobile before the page becomes interactive
  • Even static content requires JavaScript to “hydrate” and become interactive

Qwik’s solution:

  • Zero JavaScript by default — only loads code when user interacts
  • Instant interactivity — buttons work immediately, no hydration wait
  • 1KB overhead vs 40KB+ for React/Vue

Real-world adoption:

  • Builder.io (creators of Qwik) uses it for their visual development platform
  • Qwik City (meta-framework) reached 1.0 in 2023
  • Growing interest from enterprises tired of slow Time to Interactive (TTI)
  • ~40k weekly npm downloads (rapid growth since 1.0 release)

Performance impact (real production metrics):

  • Time to Interactive: <100ms (vs 1-3s for traditional SSR)
  • Initial JavaScript: 1-5KB (vs 40-200KB for Next.js/Remix)
  • Lighthouse scores: consistent 100 on Performance (hard to achieve with React)

When to use Qwik:

  • E-commerce sites — every 100ms TTI improvement = 1% conversion rate increase
  • Content-heavy sites — blogs, documentation where most content is static
  • Mobile-first apps — slow networks benefit most from minimal JS
  • SEO-critical pages — landing pages, marketing sites

When to use Next.js/Remix instead:

  • Complex dashboards with lots of client-side state
  • Need larger ecosystem and more libraries
  • Team already expert in React patterns
  • Using React Native (Qwik is web-only)

The Hydration Problem

Traditional SSR (React/Next.js):

  1. Server renders HTML
  2. Browser displays HTML (fast!)
  3. JavaScript downloads
  4. Hydration: React re-runs all components to attach listeners
  5. App becomes interactive (slow!)

Qwik’s Resumability:

  1. Server renders HTML + serialized state
  2. Browser displays HTML (fast!)
  3. App is immediately interactive
  4. JavaScript loads only when needed

Bundle Size Comparison

For a simple counter:

  • React SSR: 40KB JavaScript (entire React runtime)
  • Qwik: 1KB JavaScript (only event handler)

1. Installation

npm create qwik@latest
cd my-qwik-app
npm install
npm run dev

2. Basic Concepts

Components

import { component$ } from '@builder.io/qwik';

export const Counter = component$(() => {
  return <div>Hello, Qwik!</div>;
});

Note the $ suffix: It tells Qwik optimizer to lazy-load this code.

State with useSignal

import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);
  
  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick$={() => count.value++}>Increment</button>
    </div>
  );
});

Key differences from React:

  • useSignal instead of useState
  • Access with .value
  • onClick$ instead of onClick (lazy-loaded!)

3. Event Handling

onClick$

export const Button = component$(() => {
  const handleClick$ = $(() => {
    console.log('Clicked!');
  });
  
  return <button onClick$={handleClick$}>Click me</button>;
});

Inline Handlers

export const Form = component$(() => {
  const name = useSignal('');
  
  return (
    <input
      value={name.value}
      onInput$={(e) => name.value = e.target.value}
    />
  );
});

4. useStore (Objects)

import { component$, useStore } from '@builder.io/qwik';

export const UserProfile = component$(() => {
  const user = useStore({
    name: 'John',
    age: 30,
    email: '[email protected]',
  });
  
  return (
    <div>
      <input
        value={user.name}
        onInput$={(e) => user.name = e.target.value}
      />
      <p>Age: {user.age}</p>
    </div>
  );
});

5. Async Data with routeLoader$

// routes/users/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const useUsers = routeLoader$(async () => {
  const res = await fetch('https://api.example.com/users');
  return res.json();
});

export default component$(() => {
  const users = useUsers();
  
  return (
    <ul>
      {users.value.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
});

Key points:

  • routeLoader$ runs on the server
  • Data is serialized and sent to client
  • No hydration needed!

6. Forms with routeAction$

import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';

export const useAddUser = routeAction$(async (data) => {
  // Runs on server
  const res = await fetch('https://api.example.com/users', {
    method: 'POST',
    body: JSON.stringify(data),
  });
  return await res.json();
});

export default component$(() => {
  const action = useAddUser();
  
  return (
    <Form action={action}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">Add User</button>
      
      {action.value?.success && <p>User added!</p>}
    </Form>
  );
});

7. Routing with Qwik City

File-based Routing

src/routes/
├── index.tsx          → /
├── about.tsx          → /about
├── blog/
│   ├── index.tsx      → /blog
│   └── [slug].tsx     → /blog/:slug
└── users/
    └── [id]/
        └── index.tsx  → /users/:id

Dynamic Routes

// routes/blog/[slug].tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';

export const usePost = routeLoader$(async ({ params }) => {
  const res = await fetch(`/api/posts/${params.slug}`);
  return res.json();
});

export default component$(() => {
  const post = usePost();
  
  return (
    <article>
      <h1>{post.value.title}</h1>
      <div>{post.value.content}</div>
    </article>
  );
});
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';

export const Nav = component$(() => {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
});

8. Lifecycle Hooks

useVisibleTask$

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';

export const Chart = component$(() => {
  const chartRef = useSignal<HTMLDivElement>();
  
  useVisibleTask$(({ track }) => {
    track(() => chartRef.value);
    
    // Runs when component becomes visible
    if (chartRef.value) {
      // Initialize chart library
      initChart(chartRef.value);
    }
  });
  
  return <div ref={chartRef}></div>;
});

useTask$

import { component$, useSignal, useTask$ } from '@builder.io/qwik';

export const AutoSave = component$(() => {
  const text = useSignal('');
  
  useTask$(({ track }) => {
    track(() => text.value);
    
    // Runs on server AND client when text changes
    console.log('Text changed:', text.value);
  });
  
  return <input value={text.value} onInput$={(e) => text.value = e.target.value} />;
});

9. Context API

import { component$, createContextId, useContextProvider, useContext } from '@builder.io/qwik';

// Create context
export const UserContext = createContextId<{ name: string }>('user');

// Provider
export const App = component$(() => {
  const user = useStore({ name: 'John' });
  useContextProvider(UserContext, user);
  
  return <UserProfile />;
});

// Consumer
export const UserProfile = component$(() => {
  const user = useContext(UserContext);
  return <div>{user.name}</div>;
});

10. Real-World Example: Todo App

import { component$, useStore } from '@builder.io/qwik';
import { routeLoader$, routeAction$, Form } from '@builder.io/qwik-city';

export const useTodos = routeLoader$(async () => {
  // Load from database
  return [
    { id: 1, text: 'Learn Qwik', done: false },
    { id: 2, text: 'Build app', done: false },
  ];
});

export const useAddTodo = routeAction$(async (data) => {
  // Save to database
  return { success: true };
});

export const useToggleTodo = routeAction$(async (data) => {
  // Update in database
  return { success: true };
});

export default component$(() => {
  const todos = useTodos();
  const addAction = useAddTodo();
  const toggleAction = useToggleTodo();
  
  return (
    <div>
      <h1>Todo App</h1>
      
      <Form action={addAction}>
        <input name="text" required />
        <button type="submit">Add</button>
      </Form>
      
      <ul>
        {todos.value.map((todo) => (
          <li key={todo.id}>
            <Form action={toggleAction}>
              <input type="hidden" name="id" value={todo.id} />
              <input
                type="checkbox"
                checked={todo.done}
                onChange$={() => {
                  // Submit form on change
                }}
              />
              <span>{todo.text}</span>
            </Form>
          </li>
        ))}
      </ul>
    </div>
  );
});

11. Performance Benefits

Zero JavaScript by Default

// This page ships ZERO JavaScript
export default component$(() => {
  return (
    <div>
      <h1>Hello, World!</h1>
      <p>Static content needs no JS!</p>
    </div>
  );
});

Fine-grained Lazy Loading

// Only this button's handler is loaded when clicked
export default component$(() => {
  return (
    <div>
      <h1>Page content (no JS)</h1>
      <button onClick$={() => console.log('Clicked')}>
        Click me (1KB JS loaded on click)
      </button>
    </div>
  );
});

12. Deployment

Cloudflare Pages

npm run build
wrangler pages publish dist

Vercel

npm run build
vercel deploy

Node.js

npm run build
npm run serve

13. Best Practices

1. Use $ for Lazy Loading

// Good: Lazy-loaded
const handleClick$ = $(() => console.log('Clicked'));

// Bad: Eagerly loaded
const handleClick = () => console.log('Clicked');

2. Use useSignal for Primitives

// Good
const count = useSignal(0);

// Unnecessary for primitives
const state = useStore({ count: 0 });

3. Prefer routeLoader$ over useTask$

// Good: Runs once on server
export const useData = routeLoader$(async () => {
  return await fetchData();
});

// Bad: Runs on server AND client
useTask$(async () => {
  const data = await fetchData();
});

Summary

Qwik delivers instant-loading apps through resumability:

  • Zero hydration - app resumes, doesn’t restart
  • Fine-grained lazy loading - load code only when needed
  • Zero JavaScript by default - only ship what’s needed
  • Instant interactivity - no waiting for hydration
  • Optimized automatically - $ syntax enables magic

Key Takeaways:

  1. Use $ suffix for lazy-loading
  2. useSignal for state, .value to access
  3. routeLoader$ for server data
  4. routeAction$ for forms
  5. Zero JavaScript for static content

Next Steps:

  • Compare with [Next.js 15](/en/blog/nextjs-15-complete-guide/
  • Learn [Astro](/en/blog/astro-blog-complete-guide/
  • Try [Solid.js](/en/blog/solid-js-complete-guide/

Resources:


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Complete Qwik guide for building instant-loading web apps. Learn resumability, fine-grained lazy loading, and zero-hydra… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Qwik, JavaScript, Performance, SSR, Resumability, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.