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
과footer
props로 함수 전달 - 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 사용하기