19.2 환경 설정
Qwik 개발 환경을 설정하고 첫 번째 프로젝트를 시작해봅니다. QwikCity는 Qwik의 공식 메타프레임워크로, 라우팅, SSR, API 라우트 등을 제공합니다.
사전 요구사항
# Node.js 18+ 필요 (20 LTS 권장)
node --version # v20.x.x 이상
# npm, pnpm, yarn 모두 지원
npm --version # 9.x 이상 권장
QwikCity 프로젝트 생성
기본 생성 명령어
npm create qwik@latest
실행하면 대화형 프롬프트가 시작됩니다:
? Where would you like to create your new project? › ./my-qwik-app
? Select a starter › (화살표로 선택)
❯ Empty App (Qwik City) ← 권장: 빈 앱
Library ← 라이브러리 제작
Qwik Todo App ← 예제 앱
? Would you like to install npm dependencies? › (Y/n)
❯ Yes
No
? Initialize a new git repository? › (Y/n)
❯ Yes
No
바로 생성 (프롬프트 없이)
# 특정 디렉토리에 Empty App 생성
npm create qwik@latest -- --it empty my-qwik-app
# 이후
cd my-qwik-app
npm start
초기 프로젝트 실행
cd my-qwik-app
npm install # 패키지 설치 (이미 했다면 생략)
npm start # 개발 서버 시작 (localhost:5173)
프로젝트 구조 설명
생성된 프로젝트의 디렉토리 구조:
my-qwik-app/
├── public/ # 정적 파일 (이미지, favicon 등)
│ └── favicon.svg
├── src/
│ ├── components/ # 재사용 가능한 컴포넌트
│ │ └── router-head/
│ │ └── router-head.tsx
│ ├── routes/ # 파일 기반 라우팅 (QwikCity 핵심)
│ │ ├── index.tsx # / 경로 (홈페이지)
│ │ ├── layout.tsx # 루트 레이아웃
│ │ └── service-worker.ts # 서비스 워커 (프리페칭)
│ ├── entry.dev.tsx # 개발 서버 진입점
│ ├── entry.preview.tsx # 프리뷰 진입점
│ ├── entry.ssr.tsx # SSR 진입점 (서버)
│ └── global.css # 전역 스타일
├── .eslintrc.cjs # ESLint 설정
├── .prettierrc # Prettier 설정
├── package.json
├── tsconfig.json # TypeScript 설정
└── vite.config.ts # Vite + Qwik Optimizer 설정
주요 파일 살펴보기
src/routes/index.tsx — 홈페이지 컴포넌트:
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
export default component$(() => {
return (
<>
<h1>안녕하세요 Qwik!</h1>
<p>시작하려면 src/routes/index.tsx 파일을 수정하세요.</p>
</>
);
});
export const head: DocumentHead = {
title: 'Qwik 앱',
meta: [
{
name: 'description',
content: 'Qwik 사이트 설명',
},
],
};
src/routes/layout.tsx — 루트 레이아웃:
import { component$, Slot } from '@builder.io/qwik';
import { routerHead } from '~/components/router-head/router-head';
export default component$(() => {
return (
<>
<RouterHead /> {/* <head> 태그 관리 */}
<body>
<header>
<nav>네비게이션</nav>
</header>
<main>
<Slot /> {/* 자식 라우트가 여기 렌더링 */}
</main>
<footer>푸터</footer>
</body>
</>
);
});
vite.config.ts — Vite + Qwik Optimizer:
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig(() => {
return {
plugins: [
qwikCity(), // QwikCity 플러그인 (라우팅, SSR)
qwikVite(), // Qwik Optimizer ($ 변환, 청크 분리)
tsconfigPaths(), // tsconfig paths 지원
],
preview: {
headers: {
'Cache-Control': 'public, max-age=600',
},
},
};
});
Qwik Optimizer — Vite 플러그인과 $ 변환 원리
Optimizer가 하는 일
개발자 코드 → Qwik Optimizer → 최적화된 코드
↓
1. $ 함수를 QRL(Qwik URL Reference)로 변환
2. 클로저를 별도 청크 파일로 분리
3. 서버/클라이언트 코드 경계 설정
4. 직렬화 가능성 검사 및 경고
Optimizer 동작 예시
// 개발자가 작성:
export const App = component$(() => {
const count = useSignal(0);
const greet = $(() => `안녕하세요! count: ${count.value}`);
return (
<div>
<button onClick$={() => count.value++}>증가</button>
<p onClick$={greet}>메시지</p>
</div>
);
});
// Optimizer 변환 후 (개념적으로):
// [chunk-app.js]
export const App = componentQrl(
qrl(() => import('./chunk-app_component.js'), 'App_component')
);
// [chunk-app_component.js]
export const App_component = () => {
const count = useSignal(0);
const greet = qrl(
() => import('./chunk-app_greet.js'), 'App_greet',
[count] // 캡처된 변수들
);
return ...;
};
// [chunk-app_onClick.js]
export const App_onClick = (count) => { count.value++; };
// [chunk-app_greet.js]
export const App_greet = (count) => `안녕하세요! count: ${count.value}`;
Optimizer 설정 커스터마이징
// vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
export default defineConfig(() => {
return {
plugins: [
qwikVite({
// 빌드 대상 지정
client: {
outDir: './dist/client',
},
ssr: {
outDir: './dist/server',
},
// 디버그 모드 ($ 변환 과정 출력)
debug: process.env.NODE_ENV === 'development',
}),
],
};
});
개발 서버 실행 및 HMR
개발 서버 명령어
# 개발 서버 시작 (기본 포트: 5173)
npm start
# 또는
npm run dev
# 특정 포트 지정
npm start -- --port 3000
# 외부 접근 허용 (다른 기기에서 테스트)
npm start -- --host
HMR (Hot Module Replacement) 동작 방식
Qwik의 HMR은 일반 Vite HMR보다 더 정교합니다:
파일 수정 감지
↓
변경된 컴포넌트만 재컴파일
↓
브라우저에 청크 업데이트 전송
↓
상태 유지하면서 UI 업데이트 (Signal 기반)
// HMR이 잘 작동하는 코드 패턴
// src/components/my-component.tsx
import { component$, useSignal } from '@builder.io/qwik';
export const MyComponent = component$(() => {
const count = useSignal(0); // HMR 시 상태 유지
// JSX 수정 → HMR 자동 반영
return (
<div class="my-component">
<h2>변경하면 즉시 반영됩니다</h2>
<button onClick$={() => count.value++}>
클릭: {count.value}
</button>
</div>
);
});
개발 서버 설정
// vite.config.ts
export default defineConfig(() => {
return {
plugins: [...],
server: {
port: 3000,
open: true, // 브라우저 자동 열기
host: true, // 외부 접근 허용
cors: true, // CORS 허용
},
// 환경 변수 설정
envPrefix: ['VITE_', 'PUBLIC_'],
};
});
TypeScript 기본 설정
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik",
"strict": true,
"noEmit": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"paths": {
"~/*": ["./src/*"]
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
Qwik TypeScript 핵심 타입
import type {
QwikIntrinsicElements, // JSX 요소 타입
QRL, // Qwik Reference Link 타입
Signal, // useSignal의 타입
NoSerialize, // 직렬화 불필요 마킹
Component, // component$ 반환 타입
PropFunction, // 컴포넌트 prop으로 전달하는 $ 함수
DocumentHead, // head 메타 타입
} from '@builder.io/qwik';
// 컴포넌트 Props 타입 정의
interface ButtonProps {
label: string;
disabled?: boolean;
// $ 함수를 prop으로 받을 때 PropFunction 사용
onClick$?: PropFunction<() => void>;
// 또는 QRL 타입 직접 사용
onHover$?: QRL<(event: MouseEvent) => void>;
}
export const Button = component$<ButtonProps>(({
label,
disabled = false,
onClick$
}) => {
return (
<button
disabled={disabled}
onClick$={onClick$}
>
{label}
</button>
);
});
Signal 타입 사용
import { component$, useSignal, useStore } from '@builder.io/qwik';
import type { Signal } from '@builder.io/qwik';
// useSignal은 Signal<T> 반환
const count: Signal<number> = useSignal(0);
const name: Signal<string> = useSignal('홍길동');
const user: Signal<User | null> = useSignal(null);
// useStore는 T 타입의 반응형 객체 반환
interface AppState {
count: number;
items: string[];
isLoading: boolean;
}
const state: AppState = useStore<AppState>({
count: 0,
items: [],
isLoading: false,
});
Qwik 전용 VS Code 확장 및 설정
필수 VS Code 확장
// .vscode/extensions.json
{
"recommendations": [
"builder.qwik-vscode", // Qwik 공식 확장
"dbaeumer.vscode-eslint", // ESLint
"esbenp.prettier-vscode", // Prettier
"bradlc.vscode-tailwindcss", // Tailwind CSS (사용 시)
"ms-vscode.vscode-typescript-next", // TypeScript 최신
"unifiedjs.vscode-mdx" // MDX 지원 (선택)
]
}
Qwik VS Code 확장 기능
qwik-vscode 확장이 제공하는 기능:
1. $ 함수 자동완성 및 타입 힌트
2. QRL 참조 추적 (F12 이동)
3. 컴포넌트 스니펫
4. 직렬화 경고 하이라이팅
5. useSignal/useStore 인텔리센스
VS Code 설정 최적화
// .vscode/settings.json
{
// TypeScript
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "./node_modules/typescript/lib",
// 저장 시 자동 포맷
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// ESLint
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"typescript",
"typescriptreact"
],
// 파일 연결
"files.associations": {
"*.tsx": "typescriptreact"
},
// Tailwind CSS 인텔리센스 (사용 시)
"tailwindCSS.experimental.classRegex": [
["class\\$\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}
유용한 코드 스니펫
// .vscode/qwik.code-snippets
{
"Qwik Component": {
"prefix": "qcomp",
"body": [
"import { component$ } from '@builder.io/qwik';",
"",
"export const ${1:ComponentName} = component$(() => {",
" return (",
" <div>",
" ${2:content}",
" </div>",
" );",
"});"
],
"description": "Qwik 컴포넌트 기본 구조"
},
"Qwik Signal": {
"prefix": "qsig",
"body": "const ${1:name} = useSignal(${2:initialValue});",
"description": "useSignal 생성"
},
"Qwik Store": {
"prefix": "qstore",
"body": [
"const ${1:state} = useStore({",
" ${2:key}: ${3:value},",
"});"
],
"description": "useStore 생성"
},
"Qwik Route": {
"prefix": "qroute",
"body": [
"import { component$ } from '@builder.io/qwik';",
"import type { DocumentHead } from '@builder.io/qwik-city';",
"",
"export default component$(() => {",
" return (",
" <main>",
" <h1>${1:Page Title}</h1>",
" </main>",
" );",
"});",
"",
"export const head: DocumentHead = {",
" title: '${1:Page Title}',",
"};"
],
"description": "QwikCity 라우트 컴포넌트"
}
}
ESLint + Prettier 설정 (Qwik용)
ESLint 설정
# ESLint 관련 패키지 설치 (보통 프로젝트 생성 시 포함)
npm install -D eslint eslint-plugin-qwik @typescript-eslint/parser @typescript-eslint/eslint-plugin
// .eslintrc.cjs
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:qwik/recommended', // Qwik 전용 규칙
],
rules: {
// Qwik 특화 규칙
'qwik/no-use-after-await': 'error', // await 후 훅 사용 금지
'qwik/use-method-usage': 'error', // 훅 사용 위치 제한
'qwik/valid-lexical-scope': 'error', // 직렬화 유효성
'qwik/jsx-no-script-url': 'error',
// TypeScript 규칙
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// 일반 규칙
'no-console': 'warn',
'prefer-const': 'error',
},
};
Prettier 설정
// .prettierrc
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
// .prettierignore
node_modules
dist
.qwik
build
coverage
*.min.js
lint-staged + husky 설정 (커밋 전 검사)
npm install -D husky lint-staged
npx husky init
// package.json에 추가
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css}": [
"prettier --write"
]
}
}
# .husky/pre-commit
npx lint-staged
빌드 및 미리보기
빌드 명령어
# 프로덕션 빌드
npm run build
# 빌드 과정:
# 1. TypeScript 컴파일 및 타입 체크
# 2. Qwik Optimizer 실행 ($ 변환, 청크 분리)
# 3. Vite 번들링
# 4. SSR 번들 생성 (서버용)
# 5. 클라이언트 번들 생성 (브라우저용)
# 6. 서비스 워커 번들 생성
빌드 결과물 구조
dist/
├── build/ # 클라이언트 사이드 청크들
│ ├── q-*.js # Qwik 자동 생성 청크 (해시 포함)
│ ├── qwikloader.js # ~1KB 로더
│ └── ...
├── client/ # SSG 정적 파일
│ ├── index.html
│ └── ...
└── server/
└── entry.express.js # SSR 서버 엔트리포인트
미리보기
# 빌드 결과물 로컬 서빙
npm run preview
# → http://localhost:4173
# 빌드 + 미리보기 한번에
npm run build && npm run preview
빌드 분석 및 최적화
// vite.config.ts에 빌드 분석 추가
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig(() => {
return {
plugins: [
qwikCity(),
qwikVite(),
tsconfigPaths(),
// 번들 분석 (빌드 후 stats.html 생성)
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
build: {
// 청크 최적화
rollupOptions: {
output: {
manualChunks: {
// 공통 라이브러리 분리
vendor: ['@builder.io/qwik', '@builder.io/qwik-city'],
},
},
},
},
};
});
환경 변수 설정
.env 파일 구조
# .env (모든 환경 공통)
PUBLIC_API_URL=https://api.example.com
PUBLIC_SITE_NAME=My Qwik App
# .env.local (로컬 전용, .gitignore에 포함)
PRIVATE_DB_URL=postgresql://localhost:5432/mydb
PRIVATE_JWT_SECRET=super-secret-key
# .env.production (프로덕션 전용)
PUBLIC_API_URL=https://api.mysite.com
환경 변수 사용
// 클라이언트에서 PUBLIC_ 변수 사용
import { component$ } from '@builder.io/qwik';
export const MyComponent = component$(() => {
// PUBLIC_ 접두사는 클라이언트에 노출됨
const apiUrl = import.meta.env.PUBLIC_API_URL;
const siteName = import.meta.env.PUBLIC_SITE_NAME;
return <p>API: {apiUrl} | 사이트: {siteName}</p>;
});
// 서버에서 PRIVATE_ 변수 사용
import { routeLoader$ } from '@builder.io/qwik-city';
export const useData = routeLoader$(async () => {
// 서버에서만 실행되므로 private 변수 접근 가능
const dbUrl = process.env.PRIVATE_DB_URL;
// DB 조회 등...
});
TypeScript 타입 선언
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly PUBLIC_SITE_NAME: string;
// 필요한 환경 변수 타입 선언
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
어댑터 설정 (배포 대상별)
QwikCity는 다양한 플랫폼을 위한 어댑터를 제공합니다:
# Cloudflare Pages 어댑터
npm run qwik add cloudflare-pages
# Vercel Edge Functions 어댑터
npm run qwik add vercel-edge
# Node.js Express 어댑터
npm run qwik add express
# Azure Static Web Apps
npm run qwik add azure-swa
# AWS Lambda
npm run qwik add aws-lambda
Cloudflare Pages 어댑터 설정
// vite.config.ts (Cloudflare 어댑터 추가 후)
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { cloudflarePagesAdapter } from '@builder.io/qwik-city/adapters/cloudflare-pages/vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig(() => {
return {
plugins: [
qwikCity(),
qwikVite(),
tsconfigPaths(),
cloudflarePagesAdapter(), // Cloudflare 어댑터
],
};
});
고수 팁 섹션
팁 1: 경로 별칭(Path Alias) 활용
// tsconfig.json
{
"compilerOptions": {
"paths": {
"~/*": ["./src/*"], // ~ → src/
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
}
}
// 사용 예
import { Button } from '~/components/button/button';
import { formatDate } from '@utils/date';
팁 2: 개발 전용 디버그 설정
// src/entry.dev.tsx
import { render } from '@builder.io/qwik';
import { Router } from '@builder.io/qwik-city';
import Root from './root';
// 개발 환경에서 Qwik 디버그 모드 활성화
if (import.meta.env.DEV) {
// 직렬화 경고 활성화
(globalThis as any).QWIK_DEVTOOLS = true;
}
render(document, <Root />);
팁 3: CI/CD를 위한 빌드 검증
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint # ESLint 검사
- run: npm run build # 빌드 검증
- run: npm run test # 테스트 (있다면)
팁 4: Qwik Speak으로 i18n 설정
# 다국어 지원 라이브러리
npm install qwik-speak
// src/speak-config.ts
import type { SpeakConfig } from 'qwik-speak';
export const config: SpeakConfig = {
defaultLocale: { lang: 'ko', currency: 'KRW', timeZone: 'Asia/Seoul' },
supportedLocales: [
{ lang: 'ko', currency: 'KRW', timeZone: 'Asia/Seoul' },
{ lang: 'en', currency: 'USD', timeZone: 'America/New_York' },
],
assets: ['app'], // translation 파일 이름
};
팁 5: 프로젝트 초기 구조 템플릿
실무 프로젝트 권장 구조:
src/
├── components/
│ ├── ui/ # 범용 UI 컴포넌트 (Button, Input 등)
│ ├── layout/ # 레이아웃 컴포넌트
│ └── features/ # 기능별 컴포넌트
├── routes/
│ ├── (auth)/ # 인증 관련 라우트 그룹
│ ├── (app)/ # 앱 라우트 그룹
│ └── api/ # API 라우트
├── services/ # API 서비스 함수
├── utils/ # 유틸리티 함수
├── types/ # 공통 타입 정의
└── hooks/ # 커스텀 훅 (use...)
요약
| 명령어 | 설명 |
|---|---|
npm create qwik@latest | 새 QwikCity 프로젝트 생성 |
npm start | 개발 서버 시작 (포트 5173) |
npm run build | 프로덕션 빌드 |
npm run preview | 빌드 결과물 미리보기 |
npm run qwik add <adapter> | 배포 어댑터 추가 |
npm run lint | ESLint 검사 |
| 설정 파일 | 역할 |
|---|---|
vite.config.ts | Vite + Qwik Optimizer 설정 |
tsconfig.json | TypeScript 설정 (jsxImportSource 중요) |
.eslintrc.cjs | ESLint + Qwik 규칙 |
.prettierrc | 코드 포맷 |
src/routes/ | 파일 기반 라우팅의 루트 디렉토리 |
src/entry.ssr.tsx | SSR 진입점 |
환경 설정이 완료되면 다음 장에서 Qwik의 핵심 개념인 Signal, Store, 다양한 훅들을 깊이 있게 학습합니다.