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 스니펫의 주요 특징:

  • 함수를 통한 콘텐츠 전달: childrenfooter props로 함수 전달
  • HTML 문자열 반환: 스니펫 함수는 HTML 문자열을 반환
  • 동적 콘텐츠 생성: 함수 내에서 동적으로 콘텐츠 생성 가능
  • 이벤트 핸들링: ID와 핸들러 함수를 통한 이벤트 처리

컨텍스트 API

컨텍스트 API는 props 드릴링(여러 계층을 통해 props를 전달하는 것) 없이 컴포넌트 트리 전체에 데이터를 공유할 수 있는 방법을 제공합니다. setContextgetContext 함수를 사용합니다.

공유 상수:

// 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 사용하기

© 2024 Coding Stairs. All rights reserved.