FE
[SvelteKIt] Svelte의 prop과 store로 유연한 UI 만들기
| 서론: “이 화면에서는 버튼이 비활성화되어야 해요.”
안녕하세요 팡일입니다.
웹 애플리케이션을 만들다 보면, 같은 UI 컴포넌트를 여러 맥락에서 사용해야 하는 경우가 정말 많습니다. 제 회고 기록 서비스 re-log의 감정 선택(Emotion Section) 컴포넌트가 바로 그 대표적인 예였습니다.
글 작성 / 수정 페이지 → 사용자가 여러 감정을 자유롭게 선택하고 취소할 수 있어야 함
글 상세 페이지 → 사용자가 선택했던 감정 목록을 “보기만” 하고, 더 이상 수정할 수는 없어야 함
이 두 요구사항을 만족시키기 위해 컴포넌트를 두 개 만들 수도 있습니다. 하지만 그것은 코드 중복과 유지보수 복잡성을 키우는 좋지 않은 선택이죠.
그래서 저는 하나의 컴포넌트가 외부 상태에 따라 두 모습을 모두 수행하도록 Svelte의 prop과 store를 활용하여 유연하게 설계하는 방식을 선택했습니다.
| 본론: prop과 store로 만드는 똑똑한 컴포넌트 설계
핵심 아이디어는 단순합니다.
“컴포넌트의 행동을 외부에서 결정하도록 만들자.”
즉, 컴포넌트 내부에서 모드를 판단하지 않고, prop으로 컴포넌트의 행동을 제어하고 store로 데이터 상태를 중앙에서 관리하는 구조입니다.
1) 1단계: prop 으로 컴포넌트의 “행동 방식” 결정하기
EmotionSection.svelte 컴포넌트에 isDetailMode라는 boolean 타입 prop을 추가했습니다. 이 값은 컴포넌트가 선택 가능한 상태인지, 아니면 읽기 전용 모드인지를 결정하는 스위치 역할을 합니다.
// EmotionSection.svelte
<script lang="ts">
export let isDetailMode = false;
// ... (기타 로직)
</script>이제 prop 값에 따라 UI 요소의 동작을 분기할 수 있습니다.
<section>
{#if !isDetailMode}
<button on:click={handleReset}>전체 취소</button>
{/if}
</section>
<div>
{#each emotions as emotion}
<Chip
{emotion}
selected={isSelected(emotion.name)}
on:click={() => !isDetailMode && handleSelectEmotion(emotion.name)}
class={isDetailMode ? 'cursor-default' : 'cursor-pointer'}
/>
{/each}
</div>효과는 명확했습니다.
작성/수정 페이지에서는 감정 선택 가능
상세 페이지에서는 클릭이 차단되고, 커서도 default로 변경
불필요한 UI 요소(전체 취소 버튼)도 자동으로 숨김
이제 부모 컴포넌트에서는 prop만 주입하면 됩니다.
<!-- 글 작성 -->
<EmotionSection />
<!-- 글 상세 -->
<EmotionSection isDetailMode={true} />컴포넌트 복사 없이, 하나의 컴포넌트가 두 역할을 자연스럽게 수행하게 된 것입니다.
2) 2단계: store 로 데이터 상태 중앙 관리하기
prop은 “컴포넌트의 행동”을 제어했지만, 실제 선택된 감정 데이터는 어디에서 관리해야 할까요?
특히 수정 페이지에서는 기존 감정을 불러와 EmotionSection에 반영해야 합니다. 이를 위해 Svelte의 writable store를 도입하여
감정 선택 상태를 중앙에서 관리하도록 설계했습니다.
(1) store와 액션 정의 (writeStore.ts, writeActions.ts)
// writeActions.ts
import { selectedEmotions } from './writeStore';
export const setSelectedEmotions = (emotions: string[]) => {
selectedEmotions.set(emotions);
};스토어는 “감정 목록의 단일 진실 소스” 역할을 합니다.
(2) 데이터 로딩 시 store에 주입 (detailActions.ts)
Firestore에서 데이터를 불러오는 순간, 그 감정 목록을 store에 주입합니다.
// detailActions.ts
import { setSelectedEmotions } from '$lib/stores/write/writeActions';
export const loadRetrospect = async (id: string) => {
const retrospect = await getRetrospect(id);
if (retrospect?.emotions) {
setSelectedEmotions(retrospect.emotions);
}
};EmotionSection 컴포넌트는 항상 selectedEmotions를 구독하고 있기 때문에, store 값이 업데이트되면 화면은 자동으로 반영됩니다.
| 결론: 재사용성을 극대화하는 설계
prop으로 컴포넌트의 행동을 제어하고 store로 데이터 흐름을 중앙에서 관리하는 방식은 EmotionSection 컴포넌트를 여러 페이지에서 반복 사용하도록 만드는 데 큰 도움이 되었습니다.
이 방식의 장점은 매우 명확합니다.
단일 책임 원칙 준수
→ EmotionSection은 UI 렌더링에만 집중높은 재사용성
→ 어떤 페이지에서든 prop과 store 상태만 바꿔 주면 유연하게 동작유지보수 용이성
→ 감정 선택 관련 버그·개선 작업 시 수정 범위가 명확함
Svelte로 컴포넌트를 만들 때 “이 컴포넌트가 다른 화면에서도 쓰일 수 있을까?”를 한 번 더 생각해 본다면, 더 견고하고 확장성 있는 UI를 만들 수 있습니다. prop과 store를 적절히 활용하는 패턴은 복잡한 UI에서도 코드 중복 없이 깨끗하고 유지보수성 높은 컴포넌트를 만들 수 있는 강력한 도구입니다.