004: 컴포넌트 통신
Props를 통한 데이터 전달
컴포넌트 간 통신의 가장 기본적인 방법은 props를 통해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 것입니다. Svelte 5에서는 $props 룬을 사용하여 props를 정의합니다.
환영합니다, 사용자님!
컴포넌트 코드:
<script lang="ts">
// $props 룬을 사용하여 props 정의
let { name = '방문자', greeting = '안녕하세요' } = $props<{
name?: string;
greeting?: string;
}>();
</script>
<h1>{greeting}, {name}님!</h1>부모-자식 컴포넌트 통신
부모 컴포넌트는 자식 컴포넌트에 props를 전달하고, 자식 컴포넌트는 이벤트나 콜백 함수를 통해 부모 컴포넌트와 통신할 수 있습니다.
부모-자식 컴포넌트 통신 데모
이 예제는 props와 이벤트를 사용한 부모-자식 컴포넌트 간의 통신을 보여줍니다.
부모 컴포넌트 코드:
<script lang="ts">
import ChildComponent from './ChildComponent.svelte';
let message = $state('');
function handleAction(text: string) {
message = text;
}
</script>
<div class="space-y-4">
<div class="mb-4">
<h3 class="text-xl text-sky-600 mb-2">부모-자식 컴포넌트 통신 데모</h3>
<p class="text-gray-200 mb-4">
이 예제는 props와 이벤트를 사용한 부모-자식 컴포넌트 간의 통신을 보여줍니다.
</p>
</div>
<ChildComponent
message="부모 컴포넌트에서 전달한 메시지"
onAction={handleAction}
/>
{#if message}
<div class="mt-4 rounded bg-zinc-700 p-3">
<p class="text-gray-200">자식으로부터 받은 메시지: {message}</p>
</div>
{/if}
</div>부모 컴포넌트에서 전달한 메시지
자식 컴포넌트 코드:
<script lang="ts">
let { message, onAction } = $props();
function handleClick() {
onAction('자식에서 액션 발생!');
}
</script>
<div class="p-4 border border-orange-500 rounded">
<p class="text-gray-200">{message}</p>
<button
onclick={handleClick}
class="bg-orange-600 text-white py-1 px-3 rounded hover:bg-orange-500"
>
부모에게 알리기
</button>
</div>슬롯과 스니펫을 통한 콘텐츠 전달
슬롯(Slot)은 컴포넌트가 자식 요소를 받아들일 수 있게 해주는 전통적인 방식입니다. Svelte 5에서는 추가로 스니펫(Snippet) 기능을 통해 더 동적인 콘텐츠 전달 방식을 제공합니다.
Svelte 5 스니펫 컴포넌트:
<script lang="ts">
let { title, children, footer } = $props<{
title?: string;
children?: () => { html: string; handlers?: Record<string, Function> };
footer?: () => { html: string; handlers?: Record<string, Function> };
}>();
title = title ?? '카드 제목';
$effect(() => {
if (children?.()?.handlers) {
Object.entries(children().handlers).forEach(([id, handler]) => {
document.getElementById(id)?.addEventListener('click', handler as any);
});
}
});
</script>
<div class="rounded border border-orange-500 p-4">
<h2 class="mb-2 text-xl text-sky-600">{title}</h2>
<div class="mb-4">
{#if children}
{@html children().html}
{:else}
<p class="text-gray-200">콘텐츠가 제공되지 않았습니다.</p>
{/if}
</div>
<div class="border-t border-zinc-600 pt-2">
{#if footer}
{@html footer().html}
{:else}
<p class="text-sm text-gray-400">기본 푸터</p>
{/if}
</div>
</div>스니펫 사용 예제:
<script lang="ts">
import Card from './Card.svelte';
let clickCount = $state(0);
function mainContent() {
return {
html: `
<p class="text-gray-200">클릭 횟수: ${clickCount}</p>
<button id="incrementBtn" class="bg-orange-600 text-white py-1 px-3 rounded hover:bg-orange-500">
카운트 증가
</button>
`,
handlers: {
incrementBtn: () => clickCount++
}
};
}
function footerContent() {
return {
html: `<p class="text-sm text-sky-600">
동적 이벤트 처리 예제입니다.
</p>`
};
}
</script>
<Card
title="스니펫 예제"
children={mainContent}
footer={footerContent}
/>실제 동작 예제:
스니펫 예제
클릭 횟수: 0
동적 이벤트 처리 예제입니다.
Svelte 5 스니펫의 주요 특징:
- 함수를 통한 콘텐츠 전달:
children과footerprops로 함수 전달 - HTML 문자열 반환: 스니펫 함수는 HTML 문자열을 반환
- 동적 콘텐츠 생성: 함수 내에서 동적으로 콘텐츠 생성 가능
- 이벤트 핸들링: ID와 핸들러 함수를 통한 이벤트 처리
컨텍스트 API
컨텍스트 API는 props 드릴링(여러 계층을 통해 props를 전달하는 것) 없이 컴포넌트 트리 전체에
데이터를 공유할 수 있는 방법을 제공합니다. setContext와 getContext 함수를 사용합니다.
공유 상수:
// constants.ts
export const THEME_KEY = Symbol('theme');컨텍스트 제공자:
<script lang="ts">
import { setContext } from 'svelte';
import ThemeConsumer from './ThemeConsumer.svelte';
import { THEME_KEY } from './constants';
// 테마 상태
let theme = $state('dark');
// 컨텍스트 설정
setContext(THEME_KEY, {
getTheme: () => theme,
toggleTheme: () => { theme = theme === 'dark' ? 'light' : 'dark'; }
});
</script>
<div class="rounded border border-orange-500 p-4"
class:bg-white={theme === 'light'}
class:bg-zinc-900={theme === 'dark'}>
<h3 class:text-black={theme === 'light'}
class:text-white={theme === 'dark'}>
테마: {theme}
</h3>
<ThemeConsumer />
</div>컨텍스트 소비자:
<script lang="ts">
import { getContext } from 'svelte';
import { THEME_KEY } from './constants';
// Define the type for our theme context
type ThemeContext = {
getTheme: () => string;
toggleTheme: () => void;
};
// 컨텍스트 가져오기 (기본값 제공)
const themeContext = (getContext<ThemeContext>(THEME_KEY) || {
getTheme: () => 'light',
toggleTheme: () => console.warn('Theme context not provided')
}) as ThemeContext;
const { getTheme, toggleTheme } = themeContext;
</script>
<div class="mt-4">
<p class:text-black={getTheme() === 'light'}
class:text-white={getTheme() === 'dark'}>
현재 테마: {getTheme()}
</p>
<button
onclick={toggleTheme}
class="mt-2 rounded bg-orange-600 px-3 py-1 text-white hover:bg-orange-500"
>
테마 전환
</button>
</div>실제 동작 예제:
테마: dark
현재 테마: dark
컨텍스트 API 특징:
- 컴포넌트 트리 내에서만 작동 (전역 상태 관리가 아님)
- 컨텍스트는 컴포넌트가 초기화될 때 설정됨
- Symbol을 키로 사용하여 이름 충돌 방지
- 반응형 값을 직접 전달하기보다 getter/setter 함수 전달 권장
- 중첩된 컨텍스트 제공자 사용 가능
컴포넌트 통신 패턴 요약
- Props 전달: 부모에서 자식으로 데이터 전달
let { value } = $props(); - 이벤트 디스패치: 자식에서 부모로 데이터 전달
let { onAction } = $props(); onAction('메시지 전달'); - 슬롯: 부모에서 자식으로 마크업 전달
<slot name="content">기본 콘텐츠</slot>
- 컨텍스트 API: 컴포넌트 트리 전체에 데이터 공유
setContext(KEY, value); // 제공자 const value = getContext(KEY); // 소비자
바인딩과 양방향 데이터 흐름
Svelte는 강력한 양방향 바인딩 기능을 제공합니다. 이를 통해 컴포넌트 간의 데이터 흐름을 효율적으로 관리할 수 있습니다.
바인딩의 주요 특징:
- bind:value - 폼 요소와의 양방향 바인딩
- bind:this - DOM 요소나 컴포넌트 인스턴스 참조
- bind:group - 라디오/체크박스 그룹 바인딩
- bind:checked - 체크박스 상태 바인딩
컴포넌트 생명주기
컴포넌트의 생명주기를 이해하고 관리하는 것은 효율적인 애플리케이션 개발에 필수적입니다.
생명주기 관리 예제:
<script lang="ts">
let mounted = $state(false);
$effect(() => {
mounted = true;
return () => {
// cleanup code
mounted = false;
};
});
</script>
생명주기 관리의 핵심 포인트:
- 컴포넌트 마운트/언마운트 처리
- $effect를 사용한 부수 효과 관리
- cleanup 함수를 통한 리소스 정리
- 비동기 작업 처리와 취소
실전 예제와 패턴
컴포넌트 통신 패턴:
- 중재자 패턴 - 부모 컴포넌트를 통한 자식 간 통신
- 이벤트 버스 패턴 - 전역 이벤트를 통한 통신
- 상태 공유 패턴 - 컨텍스트나 스토어를 통한 상태 공유
- Prop Drilling 방지 전략
패턴 실습 예제:
중재자 패턴
소스 코드:
// SenderChild.svelte
<script lang="ts">
let { onSend } = $props();
let input = $state('');
function handleSubmit() {
onSend(input);
input = '';
}
</script>
<div class="flex gap-2">
<input bind:value={input} placeholder="메시지 입력..." />
<button onclick={handleSubmit}>전송</button>
</div>
// ReceiverChild.svelte
<script lang="ts">
let { message } = $props();
</script>
<div>
{#if message}
<p>받은 메시지: {message}</p>
{:else}
<p>메시지를 기다리는 중...</p>
{/if}
</div>
// MediatorPattern.svelte (부모 컴포넌트)
<script lang="ts">
let message = $state('');
function handleMessage(text: string) {
message = text;
}
</script>
<div class="space-y-4">
<SenderChild onSend={handleMessage} />
<ReceiverChild {message} />
</div>실제 동작:
메시지를 기다리는 중...
이벤트 버스 패턴
소스 코드:
// eventBus.ts
type Callback = (data: any) => void;
const eventBus = {
subscribers: new Map<string, Set<Callback>>(),
subscribe(event: string, callback: Callback) {
if (!this.subscribers.has(event)) {
this.subscribers.set(event, new Set());
}
this.subscribers.get(event)?.add(callback);
return () => {
this.subscribers.get(event)?.delete(callback);
};
},
publish(event: string, data: any) {
if (this.subscribers.has(event)) {
this.subscribers.get(event)?.forEach(callback => callback(data));
}
}
};
export default eventBus;
// EventBusSender.svelte
<script lang="ts">
import eventBus from './eventBus';
let input = $state('');
function handleSubmit() {
eventBus.publish('message', input);
input = '';
}
</script>
<div class="flex gap-2">
<input bind:value={input} placeholder="메시지 입력..." />
<button onclick={handleSubmit}>전송</button>
</div>
// EventBusReceiver.svelte
<script lang="ts">
import eventBus from './eventBus';
let messages = $state<string[]>([]);
$effect(() => {
const unsubscribe = eventBus.subscribe('message', (text: string) => {
messages = [...messages, text];
});
return unsubscribe;
});
</script>
<div class="space-y-2">
{#if messages.length > 0}
{#each messages as message}
<div class="rounded bg-zinc-700 p-3">
<p>{message}</p>
</div>
{/each}
{:else}
<p>메시지를 기다리는 중...</p>
{/if}
</div>실제 동작:
메시지를 기다리는 중...
상태 공유 패턴
소스 코드:
// SharedStateProvider.svelte
<script lang="ts">
import { setContext } from 'svelte';
interface SharedState {
count: number;
messages: string[];
increment: () => void;
addMessage: (text: string) => void;
}
let count = $state(0);
let messages = $state<string[]>([]);
const sharedState: SharedState = {
get count() {
return count;
},
get messages() {
return messages;
},
increment: () => count++,
addMessage: (text: string) => (messages = [...messages, text])
};
setContext('sharedState', sharedState);
let { children } = $props<{ children: any }>();
</script>
{@render children()}
실제 동작:
카운트: 0
메시지가 없습니다.
카운트: 0
메시지가 없습니다.
Prop Drilling 방지
소스 코드:
// WithPropDrilling.svelte
<script lang="ts">
import FirstLevel from './FirstLevel.svelte';
let data = $state({ count: 0, message: '최상위 데이터' });
function increment() {
data.count++;
}
</script>
<FirstLevel {data} {increment} />
// FirstLevel.svelte
<script lang="ts">
import SecondLevel from './SecondLevel.svelte';
let { data, increment } = $props();
</script>
<SecondLevel {data} {increment} />
// SecondLevel.svelte
<script lang="ts">
import ThirdLevel from './ThirdLevel.svelte';
let { data, increment } = $props();
</script>
<ThirdLevel {data} {increment} />
// ThirdLevel.svelte
<script lang="ts">
let { data, increment } = $props();
</script>
<div>
<p>카운트: {data.count}</p>
<p>메시지: {data.message}</p>
<button onclick={increment}>증가</button>
</div>
// WithoutPropDrilling.svelte
<script lang="ts">
import { setContext } from 'svelte';
import FirstLevel from './FirstLevel.svelte';
interface SharedData {
count: number;
message: string;
increment: () => void;
}
let data = $state({ count: 0, message: '컨텍스트로 공유된 데이터' });
function increment() {
data.count++;
}
const sharedData: SharedData = {
count: data.count,
message: data.message,
increment
};
setContext('sharedData', sharedData);
</script>
<FirstLevel />실제 동작:
Prop Drilling 사용
Prop Drilling 예제
First Level Component
Second Level Component
Third Level Component
카운트: 0
메시지: 최상위 데이터
Context API 사용
First Level Component
Second Level Component
Third Level Component
카운트: 0
메시지: 컨텍스트로 공유된 데이터
실습 과제
실시간 채팅 컴포넌트 구현하기
지금까지 배운 컴포넌트 통신 패턴을 활용하여 다음 기능이 있는 채팅 컴포넌트를 만들어보세요:
- 채팅방 목록과 채팅 메시지를 표시하는 컴포넌트 구조 설계
- Props를 통한 채팅방 정보 전달
- 이벤트 버스를 사용한 실시간 메시지 전달
- 컨텍스트 API를 활용한 사용자 정보 공유
- 슬롯을 활용한 메시지 템플릿 커스터마이징
- $effect를 사용한 새 메시지 알림 구현
- 컴포넌트 생명주기를 고려한 채팅방 입장/퇴장 처리
다음 강의 미리보기
5강: 반응성 최적화와 고급 패턴
다음 강의에서는 Svelte의 반응성 시스템을 더 효율적으로 활용하는 방법을 배웁니다:
- 반응형 스토어 생성 및 사용
- 반응성 최적화 기법
- push-pull 반응성 모델 이해하기
- 의존성 추적 메커니즘
- 클래스와 함께 $state 사용하기