본문으로 건너뛰기

3. Svelte 컴포넌트 기초

Svelte 컴포넌트는 재사용 가능하고 독립적인 UI 단위로, HTML, CSS, JavaScript를 하나의 .svelte 파일에 담습니다. 웹 표준 기술을 확장하여 직관적이면서도 강력한 개발 경험을 제공하며, 컴파일 시점에 최적화된 코드로 변환됩니다. 이 장에서는 컴포넌트의 구조와 기본 문법을 통해 Svelte 개발의 핵심을 마스터해보겠습니다.


3.1 컴포넌트 구조 이해하기

Single File Component 개념

Svelte 컴포넌트는 Single File Component(SFC) 패턴을 사용합니다. 하나의 .svelte 파일에 로직, 템플릿, 스타일이 모두 포함되어 관련 코드가 한 곳에 모여 있어 유지보수가 용이합니다. React의 JSX나 Vue의 SFC와 유사하지만, 더 간결하고 웹 표준에 가까운 문법을 사용합니다.

<!-- Counter.svelte -->
<script>
// JavaScript 로직
let count = $state(0);

function increment() {
count += 1;
}
</script>

<!-- HTML 템플릿 -->
<button onclick="{increment}">클릭 횟수: {count}</button>

<style>
/* CSS 스타일 */
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background: #ff3e00;
color: white;
cursor: pointer;
}
</style>

<script>, <style>, 마크업 섹션

Script 섹션

Script 섹션은 컴포넌트의 JavaScript 로직을 담당하며, 컴포넌트가 생성될 때 실행됩니다. 최상위 레벨에서 선언된 변수들은 템플릿에서 자동으로 접근 가능하며, Svelte 5의 Rune 시스템을 통해 반응성을 관리합니다. export 키워드를 사용하면 외부에서 전달받을 수 있는 props로 선언됩니다.

<script>
// 컴포넌트 상태 (반응형)
let count = $state(0);
let message = $state('Hello Svelte');

// 계산된 값
let doubled = $derived(count * 2);

// Props 받기
let { title = 'Default Title' } = $props();

// 함수 정의
function handleClick() {
count += 1;
}

// 컴포넌트 생성 시 실행
console.log('Component created');
</script>

마크업 섹션

마크업 섹션은 컴포넌트의 HTML 템플릿을 정의하며, 표준 HTML을 확장한 Svelte 문법을 사용합니다. JavaScript 표현식을 {}로 감싸서 동적 값을 렌더링하고, 조건문이나 반복문 등의 제어 구조를 템플릿에서 직접 사용할 수 있습니다. 이벤트 핸들러는 onclick, onchange 등의 속성으로 간단하게 연결됩니다.

<!-- 변수 출력 -->
<h1>{title}</h1>
<p>카운트: {count}, 두 배값: {doubled}</p>

<!-- 조건부 렌더링 -->
{#if count > 5}
<p>많이 클릭했네요!</p>
{:else if count > 0}
<p>조금 더 클릭해보세요</p>
{:else}
<p>버튼을 클릭해보세요</p>
{/if}

<!-- 이벤트 핸들러 -->
<button onclick="{handleClick}">클릭</button>

Style 섹션

Style 섹션에 작성된 CSS는 해당 컴포넌트에만 적용되는 스코프 CSS입니다. Svelte 컴파일러가 자동으로 고유한 클래스명을 생성하여 다른 컴포넌트와 스타일이 충돌하지 않도록 보장합니다. :global() 수식어를 사용하면 전역 스타일을 정의할 수 있고, CSS 변수를 통한 동적 스타일링도 지원합니다.

<style>
/* 컴포넌트 스코프 스타일 */
button {
background: var(--primary-color, #ff3e00);
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}

button:hover {
background: #e73c00;
}

/* 전역 스타일 */
:global(body) {
font-family: sans-serif;
}
</style>

섹션들의 관계와 데이터 흐름

섹션역할다른 섹션과의 관계
Script로직과 상태 관리마크업에 데이터 제공
마크업UI 구조와 이벤트 처리Script의 데이터를 화면에 표시
Style시각적 디자인마크업 요소에 스타일 적용

실습해보기: Svelte REPL에서 위의 Counter 예제를 직접 실행해보세요!


3.2 기본 문법

변수 선언과 출력

Svelte에서 변수 출력은 JavaScript 템플릿 리터럴과 유사하게 {} 구문을 사용합니다. 중괄호 안에는 모든 JavaScript 표현식을 작성할 수 있으며, 변수가 변경되면 자동으로 DOM이 업데이트됩니다. HTML 태그의 속성값으로도 사용할 수 있어 동적인 UI 구성이 간단합니다.

기본 변수 출력

<script>
let name = $state('김개발');
let age = $state(25);
let city = $state('서울');

// 계산된 값
let greeting = $derived(`안녕하세요, ${name}님!`);
</script>

<!-- 변수 출력 -->
<h1>{greeting}</h1>
<p>나이: {age}세</p>
<p>거주지: {city}</p>

<!-- 표현식 계산 -->
<p>내년 나이: {age + 1}세</p>
<p>이름 글자수: {name.length}글자</p>

속성에서 변수 사용

<script>
let imageUrl = $state('/profile.jpg');
let altText = $state('프로필 이미지');
let isVisible = $state(true);
let className = $state('profile-image');
</script>

<!-- 속성에 변수 사용 -->
<img
src="{imageUrl}"
alt="{altText}"
class="{className}"
style="display: {isVisible ? 'block' : 'none'}"
/>

<!-- 단축 문법 (속성명과 변수명이 같을 때) -->
<img src="{imageUrl}" {alt} {className} />

복잡한 표현식과 함수 호출

<script>
let users = $state([
{ name: '김개발', role: 'developer' },
{ name: '박디자인', role: 'designer' },
{ name: '이기획', role: 'planner' },
]);

function formatRole(role) {
const roleMap = {
developer: '개발자',
designer: '디자이너',
planner: '기획자',
};
return roleMap[role] || role;
}
</script>

<div>
<h2>팀원 목록 ({users.length}명)</h2>
{#each users as user}
<div>{user.name} - {formatRole(user.role)}</div>
{/each}
</div>

<!-- 조건부 텍스트 -->
<p>
{users.length > 0 ? `총 ${users.length}명의 팀원` :
'팀원이 없습니다'}
</p>

HTML과의 차이점

예약어 처리

Svelte의 템플릿에서는 HTML 속성을 그대로 사용할 수 있지만, JavaScript의 예약어가 props로 사용될 때는 주의가 필요합니다. HTML 속성인 class, for 등은 템플릿에서 정상적으로 작동하지만, 컴포넌트의 props로 받을 때는 JavaScript 예약어 때문에 별명을 사용해야 합니다. 이는 JavaScript의 구조 분해 할당 문법 제약 때문입니다.

상황사용법예시
HTML 속성그대로 사용 가능class="my-class", <label for="input-id">
Props 받기별명 사용 필요let { class: className } = $props()
조건부 클래스class: 디렉티브class:active={isActive}
<script>
let inputId = $state('username');
let cssClass = $state('form-input');
let isActive = $state(true);
</script>

<!-- HTML 속성은 그대로 사용 -->
<input
id="{inputId}"
class="{cssClass} {isActive ? 'active' : ''}"
/>

<!-- 레이블과 연결 -->
<label for="{inputId}">사용자명:</label>

<!-- 조건부 클래스 (더 간단한 방법) -->
<button class:active="{isActive}">버튼</button>

<!-- Props로 class를 받는 컴포넌트 예시 -->
<!-- let { class: className = '' } = $props(); -->

이벤트 핸들러

Svelte는 HTML의 onclick 등을 직접 사용하며, React처럼 onClick으로 카멜케이스를 쓸 필요가 없습니다. 이벤트 핸들러에는 함수 참조를 전달하거나 인라인으로 함수를 작성할 수 있습니다. 이벤트 객체는 자동으로 전달되며, 추가 매개변수가 필요할 때는 화살표 함수를 사용합니다.

<script>
let count = $state(0);
let message = $state('');

function handleClick() {
count += 1;
}

function handleSubmit(event) {
event.preventDefault();
console.log('폼 제출:', message);
}
</script>

<!-- 기본 이벤트 핸들러 -->
<button onclick="{handleClick}">클릭 횟수: {count}</button>

<!-- 인라인 이벤트 핸들러 -->
<button onclick="{()" ="">count += 1}> 인라인 클릭</button>

<!-- 폼 이벤트 -->
<form onsubmit="{handleSubmit}">
<input
type="text"
bind:value="{message}"
placeholder="메시지 입력"
/>
<button type="submit">전송</button>
</form>

<!-- 매개변수가 있는 이벤트 -->
<button onclick="{()" ="">
handleIncrement(5)}> 5씩 증가
</button>

조건부 속성

<script>
let isDisabled = $state(false);
let isRequired = $state(true);
let type = $state('text');
</script>

<!-- 조건부 속성 -->
<input
type={type}
disabled={isDisabled}
required={isRequired}
placeholder={isRequired ? '필수 입력' : '선택 입력'}
/>

<!-- 불린 속성 단축 문법 -->
<input
{disabled}
{required}
/>

실습해보기: Svelte REPL에서 이 코드들을 직접 실행해보세요!


3.3 스타일링 기초

컴포넌트 스코프 CSS

Svelte의 가장 강력한 기능 중 하나는 컴포넌트 스코프 CSS입니다. 각 컴포넌트의 <style> 블록에 작성된 CSS는 해당 컴포넌트에만 적용되며, 다른 컴포넌트와 스타일이 충돌하지 않습니다. 컴파일러가 자동으로 고유한 클래스명을 생성하여 스타일 격리를 보장하므로, CSS 클래스명 충돌을 걱정할 필요가 없습니다.

스코프 CSS 작동 원리

<!-- Button.svelte -->
<script>
let { variant = 'primary' } = $props();
</script>

<button class="btn {variant}">
<slot />
</button>

<style>
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.primary {
background-color: #3b82f6;
color: white;
}

.primary:hover {
background-color: #2563eb;
}

.secondary {
background-color: #6b7280;
color: white;
}
</style>

컴파일 후 결과 (실제로는 고유한 해시값 사용):

.btn.svelte-xyz123 {
padding: 0.75rem 1.5rem;
/* ... */
}

.primary.svelte-xyz123 {
background-color: #3b82f6;
/* ... */
}

스코프 CSS 장점

특징설명기존 CSS와 비교
자동 격리컴포넌트간 스타일 충돌 방지BEM, CSS Modules 불필요
간단한 문법일반 CSS와 동일한 문법추가 학습 비용 없음
최적화사용하지 않는 CSS 자동 제거번들 크기 최적화

글로벌 스타일링

때로는 전역에 적용되는 스타일이 필요합니다. Svelte는 :global() 수식어를 제공하여 이를 해결합니다. 글로벌 스타일은 신중하게 사용해야 하며, 주로 리셋 CSS나 전체 애플리케이션에 공통으로 적용될 스타일에만 사용하는 것이 좋습니다. 컴포넌트 내에서 자식 컴포넌트의 스타일을 조정할 때도 :global()을 활용할 수 있습니다.

전역 스타일 정의

<!-- App.svelte -->
<script>
let darkMode = $state(false);
</script>

<div class="app" class:dark-mode="{darkMode}">
<header>
<h1>My App</h1>
<button onclick="{()" ="">
darkMode = !darkMode}> 테마 변경
</button>
</header>

<main>
<!-- 다른 컴포넌트들 -->
</main>
</div>

<style>
/* 전역 리셋 스타일 */
:global(* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
line-height: 1.6;
}

/* 컴포넌트 스코프 스타일 */
.app {
min-height: 100vh;
background-color: #ffffff;
color: #333333;
}

.dark-mode {
background-color: #1a1a1a;
color: #ffffff;
}

/* 중첩된 글로벌 스타일 */
.app :global(h1, h2, h3) {
color: #2563eb;
}

.dark-mode :global(h1, h2, h3) {
color: #60a5fa;
}
</style>

조건부 글로벌 스타일

<script>
let theme = $state('light');
</script>

<div class="theme-provider {theme}">
<!-- 컨텐츠 -->
</div>

<style>
.theme-provider :global(.card) {
border: 1px solid #e5e7eb;
background: white;
}

.theme-provider.dark :global(.card) {
border: 1px solid #374151;
background: #1f2937;
}
</style>

CSS 변수 사용법

CSS 변수(커스텀 프로퍼티)는 Svelte에서 매우 강력한 스타일링 도구입니다. JavaScript에서 동적으로 CSS 변수값을 변경하여 실시간으로 스타일을 조정할 수 있으며, 컴포넌트 간 스타일 값을 전달하는 데도 유용합니다. 테마 시스템이나 동적 색상 변경 등에 특히 효과적입니다.

기본 CSS 변수 활용

<script>
let primaryColor = $state('#3b82f6');
let buttonSize = $state('medium');
let borderRadius = $state(8);

const sizeMap = {
small: { padding: '0.5rem 1rem', fontSize: '0.875rem' },
medium: { padding: '0.75rem 1.5rem', fontSize: '1rem' },
large: { padding: '1rem 2rem', fontSize: '1.125rem' },
};
</script>

<div class="controls">
<input type="color" bind:value="{primaryColor}" />

<select bind:value="{buttonSize}">
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>

<input
type="range"
min="0"
max="20"
bind:value="{borderRadius}"
/>
</div>

<button
class="dynamic-button {buttonSize}"
style:--primary-color="{primaryColor}"
style:--border-radius="{borderRadius}px"
style:--padding="{sizeMap[buttonSize].padding}"
style:--font-size="{sizeMap[buttonSize].fontSize}"
>
동적 버튼
</button>

<style>
.dynamic-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: var(--padding);
font-size: var(--font-size);
cursor: pointer;
transition: all 0.3s ease;
}

.dynamic-button:hover {
filter: brightness(0.9);
}
</style>

테마 시스템 구현

<!-- ThemeProvider.svelte -->
<script>
let { theme = 'light' } = $props();

const themes = {
light: {
'--bg-primary': '#ffffff',
'--bg-secondary': '#f8fafc',
'--text-primary': '#1e293b',
'--text-secondary': '#64748b',
'--border-color': '#e2e8f0',
'--accent-color': '#3b82f6',
},
dark: {
'--bg-primary': '#0f172a',
'--bg-secondary': '#1e293b',
'--text-primary': '#f1f5f9',
'--text-secondary': '#94a3b8',
'--border-color': '#334155',
'--accent-color': '#60a5fa',
},
};
</script>

<div
class="theme-provider {theme}"
style="{Object.entries(themes[theme])"
.map(([key,
value])=""
>
`${key}: ${value}`) .join('; ')} >
<slot />
</div>

<style>
.theme-provider {
min-height: 100vh;
background-color: var(--bg-primary);
color: var(--text-primary);
transition: all 0.3s ease;
}

.theme-provider :global(.card) {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 1.5rem;
}

.theme-provider :global(.button-primary) {
background-color: var(--accent-color);
color: var(--bg-primary);
}
</style>

사용 예시

<!-- App.svelte -->
<script>
import ThemeProvider from './ThemeProvider.svelte';
let currentTheme = $state('light');
</script>

<ThemeProvider theme="{currentTheme}">
<header>
<h1>테마 시스템 데모</h1>
<button class="button-primary" onclick="{()" ="">
currentTheme = currentTheme === 'light' ? 'dark' :
'light'} > {currentTheme === 'light' ? '🌙' : '☀️'}
테마 변경
</button>
</header>

<main>
<div class="card">
<h2>카드 컴포넌트</h2>
<p>테마에 따라 색상이 자동으로 변경됩니다.</p>
</div>
</main>
</ThemeProvider>

실습해보기: Svelte REPL에서 이 테마 시스템을 직접 구현해보세요!


정리

Svelte 컴포넌트의 기초를 완전히 마스터했습니다! 이제 다음과 같은 핵심 개념들을 이해했습니다:

핵심 요약

  • 컴포넌트 구조: <script>, 마크업, <style> 섹션의 역할과 상호작용
  • 기본 문법: {} 구문을 통한 동적 값 출력과 JavaScript 표현식 사용
  • 스타일링: 컴포넌트 스코프 CSS, 글로벌 스타일링, CSS 변수 활용

실무 활용 팁

  • 컴포넌트는 단일 책임 원칙을 따라 명확한 목적을 가지도록 설계
  • 스타일은 컴포넌트 스코프를 최대한 활용하고, 글로벌 스타일은 최소화
  • CSS 변수를 통한 동적 스타일링으로 유연한 UI 구현

다음 단계: 4장 "Rune 시스템"에서는 Svelte 5의 혁신적인 반응성 시스템인 Rune을 깊이 있게 알아보겠습니다. $state, $derived, $effect 등을 통해 더욱 강력하고 직관적인 상태 관리를 경험해보세요!