우선 완성된 컴포넌트는 나의 Github 리포지토리에 올려뒀다: https://github.com/hangminlee/ImageViewer-Svelte
이번 글은 어떻게 이미지 뷰어 컴포넌트가 작동하는지에 대한 기록이다.
배경
우선 이 컴포넌트를 만든 이유는 내 블로그 글의 이미지를 보다 심도 있게 볼 수 있는 장치가 필요할 것 같아서였다.
사실 이미지를 이렇게 크게 보여줄 필요까지는 없긴 한데, 있으면 좋을 것 같다는 마음에 적용을 시킨 것이다.
내 블로그 게시글은 작성 시 마크다운 파일로 올라가고 글을 읽을 때는 그것을 HTML로 렌더링해주는 @humanspeak/svelte-markdown 이라는 패키지를 사용한다. 그리고 저속 네트워크에서 빠른 이미지 로딩을 위해 저화질 이미지를 렌더링해주기 위해 커스텀 렌더러를 적용시켰다.
커스텀 렌더러 - 이미지에 동작을 추가하고 렌더링된 모든 이미지에 대한 상태 초기화
커스텀 렌더러 전체 코드:
<script lang="ts">
import { imgViewerStatusStore } from "@routes/client-config.svelte";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import { page } from "$app/state";
const { src, alt, width, height } = $props();
const containerWidth = $derived(width !== 'auto' ? `max-width: min(${width}px, 100%);` : '');
const aspectRatio = $derived(width !== 'auto' && height !== 'auto' ? `aspect-ratio: ${width} / ${height};` : '');
let newSearchParams = $derived.by(()=>{
const url = new URL(page.url);
const searchParams = url.searchParams;
searchParams.set('imgview', src);
return searchParams.toString();
});
async function imgPreview(event: Event) {
event.preventDefault();
const url = new URL((event.currentTarget as HTMLAnchorElement).href);
if (!src) return;
imgViewerStatusStore.update((status)=>({
...status,
isOpen: true,
currentSrc: src,
}));
}
onMount(() => {
if (src) {
imgViewerStatusStore.update((status) => ({
...status,
images: [...status.images, { src, alt, uuid: crypto.randomUUID() }]
}));
}
});
onDestroy(() => {
get(imgViewerStatusStore).images.length && imgViewerStatusStore.update((status) => ({
...status,
images: []
}));
});
</script>
<a class="img-container" style="background-image: url({src}/?res=low);{containerWidth}{aspectRatio}" href={`?${newSearchParams}`} onclick={imgPreview}>
<img {src} {alt} {width} {height} loading="lazy" fetchpriority="low" />
</a>이 커스텀 렌더러 컴포넌트에 하나하나 이미지 뷰어를 적용시킬 수도 있었겠지만 그러면 너무 비효율적이고, 뷰 페이지에서 모든 이미지 정보를 확인하고 한번에 보여주는 편이 훨씬 효율적이기 때문에 렌더링 시에 이미지 정보를 컨텍스트 스토어에 저장하고 이미지 뷰어 컴포넌트에서 해당 스토어로부터 이미지 목록을 받아서 렌더링할 수 있도록 구성했다.
페이지가 이동하여 이미지 컴포넌트가 파괴될 경우 스토어에서도 이미지 배열 목록을 초기화해 다른 페이지의 이미지 목록과 섞이지 않도록 구성했다. 모든 컴포넌트에 대해 onDestroy가 실행되므로 다소 효율이 떨어지고 더 현명한 방법이 있었을 것도 같지만, 이렇게 구성해야 책임소지가 더 직관적으로 파악될 것 같았다.
또한, Javascript를 사용할 수 없는 환경에서 SSR 렌더링만으로도 컨트롤이 간소화된 이미지 뷰어를 사용할 수 있도록 현재 페이지 상태를 구독하여 이미지 클릭 시 뒤에 searchParams를 붙여 페이지 위에 이미지 뷰어가 떠있는 것처럼 보일 수 있도록 구성했다.
이것이 나는 SvelteKit의 장점이라고 생각한다. 생각보다 간단하게 SSR로 모달을 흉내낼 수 있기 때문이다. 엄밀히 따지면 모달은 아니지만, 같은 페이지 뷰라도 URL의 정보를 가지고 서버에서 미리 렌더링을 추가로 넣어줄 수 있기 때문이다.
그리고 그 과정이 매우 간편하다. 물론 다른 SSR 지원 프레임워크도 이것이 가능하지만, 그 직관성은 Svelte가 매우 편리하다고 생각한다.