프로젝트/Fullstack

[Svelte] 전역에서 사용 가능한 모달 모듈 구현

bingual 2024. 11. 27. 15:14
반응형

소개

 

 

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`를 사용하는 방법과 컴포넌트에서 유지보수에 용이하게끔 객체들을 다루는 방법을 다룹니다.