[Svelte] 전역에서 사용 가능한 모달 모듈 구현
소개
GitHub - bingual/sveltekit5: svelte5, 기타 라이브러리 테스트
svelte5, 기타 라이브러리 테스트. Contribute to bingual/sveltekit5 development by creating an account on GitHub.
github.com
해당 프로젝트는 최신 `Svelte 5`와 `SvelteKit`을 기반으로 구축된 프로젝트로, 클린 코드와 유지 보수를 중점적으로 고려한 웹 애플리케이션입니다.
성능 최적화, 재사용 가능한 컴포넌트 설계, 모듈화, 메모리 누수 관리, 중복 제거, 테스트 코드 작성을 목표로 하여 효율적인 개발 환경을 구성하는데 초점을 맞춘 기능 테스트가 목표입니다.
`Svelte 5` 는 2024년 10월 19일 정식으로 릴리즈 되었습니다.
한국에서는 `React`, `Vue`, `Next`와는 다르게 인지도가 낮지만 빠르고 편리하며 프레임워크 내에서 자체적으로 제공하는 기능이 많아 개발 경험이 좋기에 해외에서는 핫한 프레임워크 중 하나입니다.
현재 사용하는 `Svelte 5` 및 기타 라이브러리들은 베타 버전도 포함하고 있기에 버그가 있을 수 있습니다.
앞으로 연재될 Svelte 5 포스팅들은 단순히 기능을 구현하는 것만이 아니라 어떻게 해야 효율적으로 기능들을 구현하고 사용 및 관리하는지에 대해 설명을 진행할 예정입니다.
Sveltekit, Prisma, Tailwind, Auth 등등 해당 프로젝트에 사용되는 라이브러리에 대한 설명은 없으므로 공식문서를 참고 바랍니다.
프로젝트 구조
prisma/ # Prisma 관련 설정 및 파일 폴더
e2e/ # E2E 테스트 코드 폴더
src/ # 소스 코드 루트 폴더
├── lib/ # 재사용 가능한 라이브러리 코드 폴더
│ ├── auth.ts # Oauth 코드
│ ├── prisma.ts # Prisma 인스턴스 코드
│ ├── supabaseClient.ts # Supabase 관련 코드
│ ├── utils/ # 유틸리티 코드 폴더
│ └── components/ # Svelte 컴포넌트 폴더
├── routes/ # SvelteKit 라우팅 폴더
└── tests/ # 유닛 테스트 코드 폴더
명시되지 않은 코드 구조가 있을 수 있습니다.
내용 요약
설명은 간단하게 진행되니 자세한 설명은 참조하는 링크를 참고해 주세요.
- 모달의 상태를 관리하는 모달 스토어 모듈 구현
- `index.layout.svelte`에서 동적으로 컴포넌트를 불러와 생명 주기 관리
- 모달 스토어 사용 예제
모달 스토어 모듈 구현
export const modalStore = () => {
const modalNames = {
SetMemo: 'SetMemo',
} as const;
type ModalNameType = (typeof modalNames)[keyof typeof modalNames] | '';
type DefaultValueType = {
currentModalName: ModalNameType;
modalTitle: string;
modalButtonLabels: {
confirm?: string;
cancel?: string;
};
modalProps?: { data?: any; action?: ActionType };
};
const defaultValue: DefaultValueType = {
currentModalName: '',
modalTitle: '',
modalButtonLabels: { confirm: '확인', cancel: '취소' },
modalProps: undefined,
};
const currentModalName = writable(defaultValue.currentModalName);
const modalUi = uiHelpers();
const modalTitle = writable(defaultValue.modalTitle);
const modalButtonLabels = writable(defaultValue.modalButtonLabels);
const modalProps = writable<DefaultValueType['modalProps']>(defaultValue.modalProps);
const modalState = () => {
return {
currentModalName,
modalNames,
modalUi,
modalTitle,
modalButtonLabels,
modalProps,
};
};
const setModal = (
name: DefaultValueType['currentModalName'],
title?: DefaultValueType['modalTitle'],
buttons?: DefaultValueType['modalButtonLabels'],
props?: any,
) => {
currentModalName.set(name);
modalUi.toggle();
if (title) modalTitle.set(title);
if (buttons) modalButtonLabels.set(buttons);
if (props) modalProps.set(props);
};
const resetModal = () => {
currentModalName.set(defaultValue.currentModalName);
modalTitle.set(defaultValue.modalTitle);
modalButtonLabels.set(defaultValue.modalButtonLabels);
modalProps.set(defaultValue.modalProps);
};
return {
modalState,
setModal,
resetModal,
};
};
모듈이란?
프로그래밍에서 모듈이란 재사용 가능한 코드의 단위로, 프로그램을 구성하는 독립적이고 캡슐화된 코드 블록을 의미합니다. 모듈은 특정한 기능을 수행하며, 이를 통해 코드의 가독성, 유지보수성, 재사용성을 높일 수 있습니다.
우리가 프로그래밍을 할 때 기능을 확장하거나 수정해야 하고 디버깅을 해야 할 일이 많을 텐데 중앙관리하지 못하게 된다면 굉장히 번거롭겠죠? 그래서 모듈을 만들어 관리하게 된다면 편리하게 작업할 수 있습니다. 재사용 가능한 컴포넌트 또한 모듈이라고 할 수 있죠.
`modalUi`: 모달의 상태 값을 관리하는 `svelte-5-ui-lib` 라이브러리의 함수입니다.
<script lang="ts">
import { Modal, Button, uiHelpers } from 'svelte-5-ui-lib';
const modalExample = uiHelpers();
let modalStatus = $state(false);
const closeModal = modalExample.close;
$effect(() => {
modalStatus = modalExample.isOpen;
});
</script>
<div class="flex justify-center">
<Button onclick={modalExample.toggle}>Default modal</Button>
</div>
이러한 느낌으로 사용할 수 있습니다.
`defaultValue`: 초기 값을 설정하는 객체입니다.
`modalState`: 정의한 스토어들을 내보내기 하는 함수입니다.
`setModal`: 정의한 스토어들의 값을 설정하는 함수입니다.
`resetModal`: 정의한 스토어들의 값을 초기화하는 함수입니다.
이렇게 정의한 함수들을 관리하는 모듈을 사용하고자 하는 곳에 내보내서 사용하게 된다면 내부 함수에서 외부 함수의 변수 값들을 참조하고 변경할 수 있게 됩니다. 클로저란 이름이 붙은 것은 외부 함수가 종료되어도 내부 함수는 외부 함수의 참조 값을 계속 유지할 수 있기 때문이죠.
type ToastStore = ReturnType<typeof toastStore>;
type ModalStore = ReturnType<typeof modalStore>;
export const useContext = () => {
const toastStore = getContext<ToastStore>('toastStore');
const modalStore = getContext<ModalStore>('modalStore');
return {
toastStore,
modalStore,
};
};
토스트 메시지와 마찬가지로 타입 추론을 자동으로 해주지 않으니 관리해 주도록 합시다.
모달 스토어 모듈 사용
`index.layout.svelte`
<script lang="ts">
...
let { children } = $props();
setContext('modalStore', modalStore());
const {
modalStore: ContextModalStore,
} = useContext();
const { modalState, resetModal } = ContextModalStore;
const { modalUi, currentModalName, modalNames } = modalState();
let ModalComponent: any = $state();
const loadComponent = async (name: string) => {
switch (name) {
case modalNames.SetMemo:
ModalComponent = (await import('$lib/components/modals/SetMemo.svelte')).default;
break;
default:
return undefined;
}
};
$effect(() => {
if ($currentModalName) {
loadComponent($currentModalName);
}
if (!modalUi.isOpen) {
resetModal();
}
});
...
</script>
...
{#if ModalComponent}
{@render ModalComponent()}
{/if}
...
정의한 모달 스토어 모듈을 사용하기 위해 세팅을 해줍니다.
https://svelte.dev/docs/svelte/@render
`render`는 svelte 5에서 `snippet`을 렌더링 하기 위해 추가된 부분인데요 컴포넌트 또한 `snippet`이니 렌더링이 됩니다.
이걸 이용한다면 필요에 따라 컴포넌트를 동적으로 렌더링 할 수 있습니다. 컴포넌트가 많아지면 많아질 수 록 페이지가 마운트 될 때 전부 불러와야 하니 성능상 좋지 않겠죠? 그래서 필요한 것들만 그때마다 불러와서 사용하면 최적화할 수 있겠습니다.
사용 예제
`SetMemo.svelte`
<script lang="ts">
...
const {
modalStore: { modalState },
} = useContext();
const { modalUi, modalTitle, modalButtonLabels } = modalState();
const closeModal = modalUi.close;
let modalStatus = $state(false);
$effect(() => {
modalStatus = modalUi.isOpen;
});
...
</script>
...
<Modal title={$modalTitle} {modalStatus} {closeModal}>
...
<div class="flex space-x-2">
<Button type="submit" color="primary">{$modalButtonLabels.confirm}</Button>
<Button color="alternative" onclick={closeModal}>{$modalButtonLabels.cancel}</Button>
</div>
...
</Modal>
...
`memo.svelte`
<script lang="ts">
...
const {
modalStore: { setModal },
toastStore: { addToast },
} = useContext();
let { data, form }: { data: PageData; form: ActionData } = $props();
const handleModal = (action: ActionType, data?: Memo) => {
const actionMap = {
create: {
modalTitle: '메모 생성',
modalButtonLabels: { confirm: '생성', cancel: '취소' },
props: { action },
},
update: {
modalTitle: '메모 수정',
modalButtonLabels: { confirm: '수정', cancel: '취소' },
props: { data, action },
},
};
const modalConfig = actionMap[action];
setModal('SetMemo', modalConfig.modalTitle, modalConfig.modalButtonLabels, modalConfig.props);
};
...
</script>
...
<Button color="green" onclick={() => handleModal('update', memo)}
><EditOutline /></Button
...
코드가 길기 때문에 필요한 부분들만 가져와봤습니다.
`svelte 5` 에서는 룬 시스템이 추가가 되었습니다.
https://svelte.dev/docs/svelte/$state
`$state`: 단순히 변수를 선언하는 것과 동일하다 보면 됩니다.
https://svelte.dev/docs/svelte/$effect
`$effect`: 페이지의 마운트, 언마운트, 클린업, 변화를 추적해서 동적 할당 하는 등등 모든 경우에 대해 페이지 상태를 처리할 수 있는 룬입니다.
하지만 너무 많은 코드를 `$effect`에 낭비하게 되면 유지보수 하기 굉장히 힘들어지죠. 그래서 나온 것 중 하나가 `$derived` 이지만 이거는 이후에서 다루게 될 겁니다.
이것들을 잘만 이용하면 상태들을 손쉽고 자연스럽게 관리할 수 있게 됩니다.
마무리
다음 포스팅에선 `$derived`를 사용하는 방법과 컴포넌트에서 유지보수에 용이하게끔 객체들을 다루는 방법을 다룹니다.