18.2 환경 설정
Solid.js 프로젝트를 시작하는 방법은 크게 두 가지입니다. 공식 템플릿을 degit으로 복사하거나 Vite에 Solid 플러그인을 추가하는 방법입니다. 이 장에서는 두 방법 모두 다루고, TypeScript 설정, 개발 도구, ESLint/Prettier까지 완전한 개발 환경을 구성합니다.
프로젝트 생성 방법
방법 1: 공식 Solid 템플릿 (추천)
# JavaScript 템플릿
npx degit solidjs/templates/js my-solid-app
cd my-solid-app
npm install
npm run dev
# TypeScript 템플릿 (권장)
npx degit solidjs/templates/ts my-solid-app
cd my-solid-app
npm install
npm run dev
degit은 Git 저장소를 히스토리 없이 복사하는 도구입니다. git clone보다 빠르고, 시작점을 깨끗하게 유지할 수 있습니다.
방법 2: Vite + Solid 플러그인으로 직접 구성
# Vite 프로젝트 생성 (interactive)
npm create vite@latest my-solid-app -- --template solid
cd my-solid-app
npm install
npm run dev
# 또는 TypeScript 버전
npm create vite@latest my-solid-app -- --template solid-ts
방법 3: SolidStart (풀스택 프레임워크)
npm create solid@latest
# 대화형 설치 진행
# ✔ Which template would you like to use? › bare / hackernews / with-auth / ...
# ✔ Use TypeScript? › Yes
# ✔ Install dependencies? › Yes
SolidStart는 Solid.js의 풀스택 메타프레임워크로, Next.js의 Solid 버전입니다. SSR, SSG, 파일 기반 라우팅을 제공합니다.
프로젝트 구조 설명
solidjs/templates/ts 기반의 TypeScript 프로젝트 구조:
my-solid-app/
├── src/
│ ├── App.tsx # 루트 컴포넌트
│ ├── App.module.css # App 전용 CSS Module
│ ├── index.tsx # 진입점 (render 호출)
│ ├── logo.svg # 예시 에셋
│ └── components/ # (직접 생성) 컴포넌트 폴더
│ ├── Header.tsx
│ ├── Counter.tsx
│ └── ...
├── public/
│ └── favicon.ico # 정적 에셋
├── index.html # HTML 진입점 (Vite)
├── vite.config.ts # Vite 설정
├── tsconfig.json # TypeScript 설정
└── package.json
src/index.tsx — 진입점
/* @refresh reload */
import { render } from 'solid-js/web';
import './index.css';
import App from './App';
const root = document.getElementById('root');
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
'Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?',
);
}
render(() => <App />, root!);
/* @refresh reload */ 주석은 HMR(핫 모듈 교체)에서 이 파일이 변경되면 전체 페이지를 새로고침하도록 Solid의 Babel 플러그인에게 알려줍니다.
src/App.tsx — 루트 컴포넌트
import type { Component } from 'solid-js';
import { createSignal } from 'solid-js';
import styles from './App.module.css';
import logo from './logo.svg';
const App: Component = () => {
const [count, setCount] = createSignal(0);
return (
<div class={styles.App}>
<header class={styles.header}>
<img src={logo} class={styles.logo} alt="logo" />
<p>
편집하세요 <code>src/App.tsx</code> 그리고 저장하세요.
</p>
<p>
카운트: {count()}
<button onClick={() => setCount(count() + 1)}>+1</button>
</p>
</header>
</div>
);
};
export default App;
Vite 설정 (vite-plugin-solid)
vite.config.ts 파일이 Solid.js 프로젝트의 핵심입니다:
// vite.config.ts
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
plugins: [
solidPlugin({
// Solid babel 변환 옵션
solid: {
// 개발 환경에서 컴포넌트 이름 유지 (디버깅용)
generate: 'dom',
},
}),
],
// 개발 서버 옵션
server: {
port: 3000,
open: true, // 브라우저 자동 오픈
},
// 빌드 옵션
build: {
target: 'esnext',
outDir: 'dist',
sourcemap: true, // 소스맵 생성
},
// 경로 별칭
resolve: {
alias: {
'@': '/src', // import '@/components/Button'
'~': '/src',
},
},
});
경로 별칭 TypeScript 설정
tsconfig.json에도 별칭을 추가해야 IDE 자동완성이 동작합니다:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
TypeScript 템플릿 상세 설정
Solid.js 전용 TypeScript 타입
// solid-js 타입 활용 예시
import type { Component, JSX, ParentComponent, FlowComponent } from 'solid-js';
// 기본 컴포넌트
const Button: Component<{ onClick: () => void; label: string }> = (props) => (
<button onClick={props.onClick}>{props.label}</button>
);
// children을 받는 컴포넌트
const Card: ParentComponent<{ title: string }> = (props) => (
<div class="card">
<h2>{props.title}</h2>
{props.children}
</div>
);
// render prop 패턴
const List: FlowComponent<{ items: string[] }, JSX.Element> = (props) => (
<ul>
{props.items.map(item => <li>{props.children(item)}</li>)}
</ul>
);
CSS Module 타입 설정
Vite는 CSS Module을 기본 지원합니다:
// *.module.css 파일의 타입 선언이 필요한 경우
// src/vite-env.d.ts (자동 생성됨)
/// <reference types="vite/client" />
개발 서버 실행과 HMR
# 개발 서버 시작 (기본 포트: 3000)
npm run dev
# 특정 포트로 실행
npm run dev -- --port 4000
# 네트워크 공개 (다른 기기에서 접근 가능)
npm run dev -- --host
HMR (Hot Module Replacement) 동작 방식
Solid.js의 HMR은 vite-plugin-solid가 처리합니다. 컴포넌트 파일을 수정하면:
- 변경된 파일만 재컴파일
- 브라우저에 WebSocket으로 업데이트 전달
- 상태를 유지한 채로 컴포넌트만 교체
# 빌드 (프로덕션)
npm run build
# 빌드 결과물 미리보기
npm run preview
빌드 결과물 구조
dist/
├── assets/
│ ├── index-[hash].js # 번들된 JavaScript
│ └── index-[hash].css # 번들된 CSS
└── index.html # 진입점 HTML
Solid DevTools 설치 및 사용
브라우저 확장 설치
Chrome/Edge: Chrome Web Store에서 Solid DevTools 검색
또는 직접 설치:
# 개발 의존성으로 설치
npm install --save-dev solid-devtools
vite.config.ts에 플러그인 추가:
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import devtools from 'solid-devtools/vite'; // 추가
export default defineConfig({
plugins: [
devtools({
autoname: true, // 컴포넌트 이름 자동 추론
locator: {
targetIDE: 'vscode', // 클릭 시 VS Code에서 파일 열기
componentLocation: true,
jsxLocation: true,
},
}),
solidPlugin(),
],
});
src/index.tsx에 import 추가:
import 'solid-devtools'; // 맨 위에 추가
/* @refresh reload */
import { render } from 'solid-js/web';
// ...
DevTools 주요 기능
| 탭 | 기능 |
|---|---|
| Component | 컴포넌트 트리, props/signals 실시간 확인 |
| Signal | 앱 전체의 Signal 목록과 현재 값 |
| Owner Graph | Signal과 Effect의 의존성 그래프 |
| Locator | DOM 요소 클릭 시 소스 파일 바로 열기 |
ESLint + Prettier 설정
패키지 설치
# ESLint + Solid 플러그인
npm install --save-dev \
eslint \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
eslint-plugin-solid \
eslint-config-prettier \
prettier
ESLint 설정 (eslint.config.js, Flat Config 방식)
// eslint.config.js
import js from '@eslint/js';
import ts from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import solid from 'eslint-plugin-solid';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
// TypeScript
{
files: ['**/*.{ts,tsx}'],
plugins: {
'@typescript-eslint': ts,
},
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
...ts.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
// Solid.js 전용 규칙
{
files: ['**/*.{jsx,tsx}'],
plugins: { solid },
rules: {
...solid.configs.typescript.rules,
// Solid.js 핵심 규칙
'solid/no-destructure': 'error', // props 구조 분해 금지
'solid/reactivity': 'warn', // 반응성 패턴 경고
'solid/event-handlers': 'error', // 이벤트 핸들러 명명 규칙
},
},
// Prettier 충돌 규칙 비활성화 (항상 마지막에)
prettier,
// 제외 파일
{
ignores: ['dist/**', 'node_modules/**'],
},
];
Prettier 설정 (.prettierrc)
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"jsxSingleQuote": false,
"bracketSameLine": false
}
VS Code 설정 (.vscode/settings.json)
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.preferences.importModuleSpecifier": "shortest"
}
package.json 스크립트 추가
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write src/**/*.{ts,tsx,css}",
"type-check": "tsc --noEmit"
}
}
권장 VS Code 확장 프로그램
// .vscode/extensions.json
{
"recommendations": [
"esbenp.prettier-vscode", // Prettier 포맷터
"dbaeumer.vscode-eslint", // ESLint 통합
"bradlc.vscode-tailwindcss", // TailwindCSS (사용 시)
"ms-vscode.vscode-typescript-next", // 최신 TypeScript
"YoavBls.pretty-ts-errors", // TS 에러 가독성 향상
"solid.solid-snippets" // Solid.js 스니펫 (있을 경우)
]
}
실전 예제: 완전한 프로젝트 초기 구조
실제 프로젝트에서 권장하는 디렉토리 구조:
src/
├── components/ # 재사용 가능한 UI 컴포넌트
│ ├── ui/ # 기본 UI (Button, Input, Card 등)
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ └── index.ts # barrel export
│ └── features/ # 기능별 컴포넌트
│ ├── auth/
│ └── dashboard/
├── stores/ # 전역 상태 (createStore)
│ ├── userStore.ts
│ └── appStore.ts
├── hooks/ # 커스텀 Signal/Effect 훅
│ ├── useLocalStorage.ts
│ └── useFetch.ts
├── pages/ # 라우트별 페이지 컴포넌트
│ ├── Home.tsx
│ └── About.tsx
├── lib/ # 유틸리티 함수, API 클라이언트
│ ├── api.ts
│ └── utils.ts
├── styles/ # 전역 CSS
│ ├── global.css
│ └── variables.css
├── App.tsx
└── index.tsx
src/lib/utils.ts — 공통 유틸리티
// 타입 안전한 클래스 이름 합성 유틸리티
export function cx(...classes: (string | undefined | false | null)[]): string {
return classes.filter(Boolean).join(' ');
}
// 디바운스 함수
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
src/components/ui/Button.tsx — 재사용 버튼
import type { Component, JSX } from 'solid-js';
import { splitProps } from 'solid-js';
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
}
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700',
ghost: 'bg-transparent text-gray-600 hover:bg-gray-100',
};
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
};
const Button: Component<ButtonProps> = (props) => {
// splitProps로 컴포넌트 전용 props와 HTML 기본 props 분리
const [local, rest] = splitProps(props, ['variant', 'size', 'loading', 'class', 'children']);
const variant = () => local.variant ?? 'primary';
const size = () => local.size ?? 'md';
return (
<button
{...rest}
disabled={local.loading || rest.disabled}
class={`
inline-flex items-center justify-center rounded font-medium
transition-colors focus:outline-none focus:ring-2
disabled:opacity-50 disabled:cursor-not-allowed
${variantStyles[variant()]}
${sizeStyles[size()]}
${local.class ?? ''}
`.trim()}
>
{local.loading ? '로딩 중...' : local.children}
</button>
);
};
export default Button;
고수 팁
팁 1: /* @refresh reload */ 주석의 의미
이 주석은 HMR 시 컴포넌트 상태를 보존할 수 없을 때 전체 페이지를 새로고침하도록 지시합니다. 진입점(index.tsx)에만 넣고, 일반 컴포넌트에는 불필요합니다.
팁 2: Vite 환경 변수 타입 설정
// src/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
// 사용할 환경 변수 모두 선언
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
# .env.local (gitignore에 포함)
VITE_API_URL=http://localhost:8080
VITE_APP_TITLE=My Solid App
// 사용
const API_URL = import.meta.env.VITE_API_URL;
팁 3: 번들 크기 분석
# rollup-plugin-visualizer로 번들 시각화
npm install --save-dev rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
solidPlugin(),
visualizer({
open: true, // 빌드 후 자동으로 브라우저 열기
filename: 'dist/stats.html',
gzipSize: true,
}),
],
});
npm run build # 빌드 후 dist/stats.html이 브라우저에서 열림
팁 4: 절대 경로 import로 유지보수성 향상
// 상대 경로 (나쁜 예)
import Button from '../../../components/ui/Button';
// 절대 경로 (좋은 예)
import Button from '@/components/ui/Button';
vite.config.ts의 resolve.alias와 tsconfig.json의 paths 설정이 모두 되어있어야 합니다.
팁 5: Solid.js와 Tailwind CSS 함께 사용
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}', // Solid 파일 포함
],
theme: {
extend: {},
},
plugins: [],
};
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
정리
| 항목 | 명령/파일 |
|---|---|
| JS 템플릿 생성 | npx degit solidjs/templates/js my-app |
| TS 템플릿 생성 | npx degit solidjs/templates/ts my-app |
| Vite 설정 | vite.config.ts + vite-plugin-solid |
| 개발 서버 | npm run dev (기본 포트 3000) |
| HMR | /* @refresh reload */ 주석으로 제어 |
| DevTools | solid-devtools 패키지 + 브라우저 확장 |
| ESLint | eslint-plugin-solid (no-destructure 규칙 필수) |
| 경로 별칭 | vite.config.ts alias + tsconfig.json paths |
다음 장에서는 Solid.js의 핵심인 반응형 프리미티브(Signal, Effect, Memo, Resource)를 깊이 살펴봅니다.