토이프로젝트 발자취(2_3_2.프론트엔드 구현 실전 Svelte 사용기)

0
post-thumbnail

이전 포스팅에 선언(?) 했던 것과 같이 최대한 AI를 사용 하는 것으로 프론트 엔드 구현을 진행했다.
전체적인 기본 틀은 정말 프롬프트 몇번에 완성 되었고, 실제 코드로 구현하기까지도 한 화면 당 10분이면 충분했다. 여기서 AI의 발전에 다시 한번 감탄과 함께, 폭발적인 생산성 증대를 기대했으나,자잘한 문제를 해결에 시간을 오래 잡아먹어서 고생하기도 했다.
그래도 만약 내가 직접 처음부터 구현했다면 아마 몇주나 걸릴 작업이었을 것으로 생각한다. 백엔드 개발자라고는 해도, 웹 개발자라면 어느 정도 풀 스택의 소양을 가지고 있어야 한다고 생각하므로 Svelte로 프론트 엔드를 구현하면서 필요한 기본적인 부분들은 이번글로 정리 해보려고 한다. 기초 문법적인 내용은 아니고, 그냥 상황에 맞는 실전에 가까운 예제로 정리하며, 작업의 순서를 따르도록 한다.

레이아웃 구성

백엔드의 뼈대는 도메인 정의라고 생각한다. 이에 상응하는 개념은 아니지만, 프론트 엔드에서의 뼈대는 레이아웃이 아닐까 생각 해 본다.(극히 주관적인 관점이다...)
레이아웃은 웹 페이지의 전반적인 구조를 정의하는 요소이며, Svelte에서는 가장 기본적인 개념중 하나인 컴포넌트의 요소만으로 이 레이아웃을 쉽고 빠르며, 이해하기 쉽게 구현 할 수 있다.

나는 이전까지는 JSP의 Tiles, Thymeleaf Thymeleaf의 Layout Dialect와 같은 템플릿 엔진에서 별도로 레이아웃을 구성하는 방법만을 써봤었다. 이 구현했던 것과 비교만으로도 놀랍도록 쉬운 구현에 감탄했다.

레이아웃 구현

이전 글에 간단하게 컴포넌트를 만들고 사용하는 예시를 이미 전달했다.
레이아웃은 일반적으로 모든 페이지에 반복적으로 사용되는 구조로, 고정 된 요소를 컴포넌트로 만들고, 이 컴포넌트를 매 페이지에서 사용하면 된다.
즉 레이아웃을 구현하는데, 별도의 특별한 개념과 구현방법이 없이 컴포넌트를 반복 사용하면 레이아웃이 구현되는 것이다!
추가로 Sveltekit 에서는 반복되어 사용할 컴포넌트를 +layout.svelte 에 정의하면 모든 페이지에 반복적, 공통적으로 사용되게 구현해 주며, 이 파일의 이름 그대로 레이아웃을 바로 구현 할 수 있게 한다.

SvelteKit에서의 레이아웃 예제

레이아웃의 가장 단순한 예제를 위한 예시 프로젝트를 만들어서 다음을 구현 해 본다. 프로젝트 생성은 이전 포스팅 내용과 동일하게, DaisyUI까지 적용한 연습용 프로젝트를 만들었다. 우선 바로 Header와 Footer의 컴포넌트를 다음과 같이 만들어 본다. 이 예시 프로젝트의 주제는 쇼핑몰로 가정하고 구현한다.
우선은 src/routes 하위에 컴포넌트를 만들어 본다.

Header.svelte

<header class="bg-white shadow">
	<div class="container mx-auto px-4 py-2 flex justify-between items-center">
		<h1 class="text-2xl font-bold text-gray-800">My Shop</h1>
		<nav>
			<ul class="flex space-x-4">
				<li><a href="/" class="text-gray-700 hover:text-blue-500">Home</a></li>
				<li><a href="/products" class="text-gray-700 hover:text-blue-500">Products</a></li>
				<li><a href="/about" class="text-gray-700 hover:text-blue-500">About</a></li>
				<li><a href="/contact" class="text-gray-700 hover:text-blue-500">Contact</a></li>
			</ul>
		</nav>
	</div>
</header>

Footer.svelte

<footer class="bg-gray-800 text-white py-4 mt-10">
	<div class="container mx-auto px-4">
		<div class="flex justify-between">
			<p>&copy; {new Date().getFullYear()} MyShop. All rights reserved.</p>
			<nav>
				<ul class="flex space-x-4">
					<li><a href="/privacy-policy" class="hover:text-gray-400">Privacy Policy</a></li>
					<li><a href="/terms-of-service" class="hover:text-gray-400">Terms of Service</a></li>
				</ul>
			</nav>
		</div>
	</div>
</footer>

그리고 바로 src/routes 하위에 +layout.svelte를 생성 하고, 다음의 컴포넌트 사용을 정의한다.
+layout.svelte

<script>
	import "tailwindcss/tailwind.css";
	import Header from './Header.svelte';
	import Footer from './Footer.svelte';
</script>

<Header />
<slot />
<Footer />

Header는 상단에, Footer는 하단에 위치하는게 목적이기 때문에 위와 같이 컴포넌트의 사용위치를 정해주면 원하는 의도대로 동작한다. 만약 추가로 다른 레이아웃 요소를 중간에 넣고 싶다면 원하는 위치에 넣어주면 된다. 여기서 중요한 부분은 <slot /> 영역인데 이 곳에 표시되는 내용은 +page.svelte의 내용으로 즉 해당 페이지의 고유영역 (컨텐츠)가 표시되는 부분이다. 레이아웃의 사용의도대로 반복적으로 사용될 부분과, 변경이 될 부분을 나눈것이다.

MyShop 레이아웃 예시 화면

npm run dev 명령어로 웹 애플리케이션을 실행 하고, 직접 페이지에 접속해보면 위와 같은 화면을 실제로 확인 할 수 있다. Header, slot(실제 페이지의 컨텐츠 내용), Footer의 요소가 제대로 표시되었다. 레이아웃은 반복되어서 사용되는 구조이니, 이 형태가 다른 페이지에도 실제 적용되는지 알아보기 위해, /products에 대한 라우팅 페이지를 만들어보자.
/src/routes/products 로 폴더를 만들고, 이 폴더 하위에 +page.svelte 파일을 다음처럼 만들어 보자.

/src/routes/products/+page.svelte

<h1>Welcome to Products Page!</h1>
<p>This Page for Products</p>

이제 웹 브라우저에서 메인 페이지 header영역의 Products 버튼을 누르거나, /products로 요청을 보내보자. /(루트)요청과 동일한 레이아웃을 가지며, +layout.svelte 파일에서 slot에 해당하는 영역만 바뀌는 것을 확인 할 수 있다. 레이아웃은 별도의 추가 작업없이, 하위 모든 페이지에 공통으로 적용되는 것을 확인했다.

패키징으로 레이아웃코드 분리

코드 개선 및 구현 진행에 맞춰 별도 예제 프로젝트의 별도 브랜치를 분리해서 만들어 보았다. 매 단락에 맞춰 Github 링크를 첨부한다.

현재 단계 Github 링크

컴포넌트로 사용한 Header와 Footer는 현재 routes 하위 폴더에 위치한다. SvelteKit에서 routes는 폴더명 그대로 라우팅 규칙을 담당하므로 (폴더 라우팅 방식을 사용) 컴포넌트 코드는 별도의 폴더로 관리하는 것이 더 알맞다. 또한 현재는 레이아웃 파일에서 import 방식으로 사용할 컴포넌트를 불러올 때 상대경로를 사용하기 때문에 파일의 이동의 변경만으로 코드를 수정해야 할 부분이 있음과 동시에, 현재의 파일의 위치자체를 계속 인지하고 바꿔야 하는 불편함이 있다.

<script>
	import "tailwindcss/tailwind.css";
	import Header from './Header.svelte';
	import Footer from './Footer.svelte';
</script>

그래서 이런 불편함을 개선하기 위해 다음과 같이 폴더 구조를 변경 해 보도록 한다. src/lib/components/ui 라는 경로로 폴더를 만들고, 기존의 Header.svelte, Footer.svelte 파일을 이동시킨다.
패키징 변경한 폴더구조

위와 같은 구조로 패키징 변경과 폴더구조를 변경 한 뒤, +layout.svelte 파일의 컴포넌트 import 경로를 아래와 같이 수정 한다.

+layout.svelte

<script>
	import "tailwindcss/tailwind.css";
	import Header from '$lib/components/ui/Header.svelte';
	import Footer from '$lib/components/ui/Footer.svelte';
</script>

<Header />
<slot />
<Footer />

이렇게 변경하면 이제 절대경로로 해당 컴포넌트 파일을 불러오게 된다.참고로 패키징 규칙은 강제된 규칙은 아니며, 이름 또한 마음대로 사용해도 된다. 보편적으로 lib/components 로 컴포넌트를 패키징하는 케이스가 검색 결과에서 보면 많아서 사용했으며, 나는 ui라는 하위패키징도 처리 했는데 이런 규칙은 얼마든지 자유이다. 다만 자신 or 팀 규칙을 따라가면서 일관된 규칙을 지키면 된다.

each문으로 Header 컴포넌트 개선하기

개발자가 코드 Smell 을 가장 쉽게 느낄 수 있는 것은 반복되는 코드이다. 아래 현재의 Header 코드를 보면 nav(네비게이션) 영역에 거의 비슷한 형태로 반복되는 구조를 찾을 수 있다. Svelte 이전의 가장 기본이 되는 Javascript로도 조금 생각 해보면 이런 구조를 반복문으로 해결 할 수 있음을 떠 올릴 수 있는데, Javascript 자체로 DOM 영역을 컨트롤 하는 것이 조금 까다롭다. 이를 간단히 해결 할 수 있는 방법으로 Svelte에서 제공하는 문법을 사용하면 된다.

Header.svelte 기존코드

<header class="bg-white shadow">
	<div class="container mx-auto px-4 py-2 flex justify-between items-center">
		<h1 class="text-2xl font-bold text-gray-800">My Shop</h1>
		<nav>
			<ul class="flex space-x-4">
				<li><a href="/" class="text-gray-700 hover:text-blue-500">Home</a></li>
				<li><a href="/products" class="text-gray-700 hover:text-blue-500">Products</a></li>
				<li><a href="/about" class="text-gray-700 hover:text-blue-500">About</a></li>
				<li><a href="/contact" class="text-gray-700 hover:text-blue-500">Contact</a></li>
			</ul>
		</nav>
	</div>
</header>

Header.svelte 개선코드

<script>
	export let links = [
		{ name: "Home", url: "/" },
		{ name: "Products", url: "/products" },
		{ name: "About", url: "/about" },
		{ name: "Contact", url: "/contact" }
	];
</script>

<header class="bg-white shadow">
	<div class="container mx-auto px-4 py-2 flex justify-between items-center">
		<h1 class="text-2xl font-bold text-gray-800">My Shop</h1>
		<nav>
			<ul class="flex space-x-4">
				{#each links as link}
					<li><a href={link.url} class="text-gray-700 hover:text-blue-500">{link.name}</a></li>
				{/each}
			</ul>
		</nav>
	</div>
</header>

Javascript로 links 배열에 객체를 만들어서 선언했다.
그리고 {#each} 는 each 문이 시작을 알리며, {/each}는 each문의 종료를 알리는 구문이자 문법이다. Javascript의 for / foreach 반복문과 유사한 기능을 제공한다. each문을 통해 모든 요소를 순회하며, 결과는 기존과 동일함을 알 수 있다. HTML 코드와 함께 쓰이기 때문에 더 이해하기 쉽고 직관적인 장점이 있다.

다른 레이아웃 적용

현재 단계 Github 링크

레이아웃은 계속 설명하기를 공통으로 사용되는 HTML의 구조라고 했다. 하지만 경우에 따라서는 특정 페이지에서는 다른 레이아웃은 적용하거나, 레이아웃을 사용하지 않아야 하는 경우도 있다. 특정 페이지에서는 Footer 영역을 사용하지 않는 경우를 예시로 다음의 코드를 작성해 본다.
/about을 라우팅하며, 이 페이지에서는 Footer는 보이지 않도록 하는것이 목적이다.

/src/routes/about/+page.svelte

<h1>Welcome to AboutPage!</h1>
<p>This Page No Footer</p>

나는 모든 것이 공통으로 적용되는 레이아웃과, 일부가 다른 화면이 공통적으로 적용되는 레이아웃의 2가지 다른 경우를 적용하기 위해 생각보다 많은 시행착오를 경험했다. 여러 해결 방법이 존재했는데, 그 중 내가 답으로 찾은 방법은 그룹 레이아웃을 사용했다. routes 하위 폴더에 (그룹화 할 이름) 의 규칙으로 패키징을 하는 방식으로 ()괄호 안의 명칭은 라우팅 규칙에는 전혀 영향을 끼치지 않으며, 다른 그룹폴더와 별개로 관리 할 수 있는 이점이 있다. +layout.svelte 파일은 자동으로 하위 폴더 (하위 라우팅)에 중첩되어서 적용되는데, 이를 관리하기 가장 편리한 방법이라 생각한다.
이제 실제 코드 적용 단계는 글로 작성하기에는 조금 무리인 부분이 있어서 (어렵진 않지만 글로 쓰기에는 너무 단계가 많다) 결과론적인 폴더 구조는 다음과 같다.

그룹 패키징을 처리한 폴더 구조

main과 sub 의 두 그룹으로 나누고 레이아웃을 개별 관리하는 방식이다. 단순히 폴더구조만으로는 더 복잡 해 보일 수도 있다고 생각하는데, 한 번 구조를 구성만 해 놓으면 더 이상 레이아웃 구조는 바꿀 필요가 없는 안정된 구조라고 생각한다.
가장 상단의 layout에는 공통으로 사용하는 CSS 코드를 불러온다. 그리고 main 그룹에서는 Header와 Footer를 공통으로 사용, sub 그룹에서는 Header만 사용하도록 레이아웃을 구성했다.
라우팅 규칙에는 변동이 없기에 /, /products, /about 는 정상적으로 호출된다.
글로는 조금 난해한 것 같지만, 실제 코드로 확인하면 어렵지 않은 내용이다.

라이트 & 다크모드 구현

어느순간부터 웹사이트에서 라이트모드와 다크모드를 제공하는 것은 기본적인 사항이 된 것 같다. 보통 라이트모드는 밝은 배경과 이에 맞도록 잘보이는 글씨 색을 제공하고, 다크모드는 어두운 배경과 이에 맞게 잘보이는 글씨 색상을 제공하는 형태이다. 실제 눈 건강에 라이트 모드 혹은 다크모드가 효과가 있는지는 논외로 하고, 모던한 웹 페이지에서의 기본소양과도 같은 라이트 / 다크모드를 구현 해 보기로 한다.

DayisyUI를 통해 구현

실제 구현 단계는 여러 방법이 있을 것이다. 기본적인 구현의 매커니즘은 대략 다음과 같다.

    1. CSS에 변수로 라이트 모드와 다크모드의 색상을 정의 한다.
    1. JavaScript를 이용하여 사용자가 특정 버튼을 누르면 HTML내 class를 넣거나 빼기, 혹은 이름 변경 등을 통해서 토글 상태를 갖게 한다.
    1. 해당 토글기능으로 바뀐 HTML에 따라서 CSS의 라이트모드/ 다크모드가 적용된다.

위 구현을 실제로 해 보면 좋겠지만, 생산성과 완성도를 위해 이미 누가 개발한 방법을 가져와 사용하기로 하고, 그 방법으로 DayisyUI를 본격적으로 사용 하기로 한다.

테마 설정

현재 단계 Github 링크

DaisyUI의 미리 만들어진 테마를 사용해서 밝은 배경 테마는 라이트모드로, 어두운 배경의 테마는 다크모드로 사용하기로 한다. 이를 적용하기 위해서는 설정파일인 tailwind.config.js를 수정해야 한다. 아래와 같이 적용했다.
tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{html,svelte,js,ts}'],
  theme: {
    extend: {},
  },
  plugins: [require('daisyui')],
  daisyui: {
    themes: ["lofi", "business"],
    darkTheme: "business", // dark 모드에서 사용할 테마
    base: true,
    styled: true,
    utils: true,
    logs: true,
  },
}

themes에는 DaisyUI에서 사용할 테마를 선언하고, 이 테마와 관련된 CSS를 불러오는 방식이다. DaisyUI 테마 공식문서 를 통해 어떤 테마가 있는지 확인 할 수 있으며, 보고 있는 문서의 테마를 바로바로 변경해서 확인 해 볼 수도 있다.

테마 적용

공식문서의 테마를 적용하는 방법은

<html data-theme="cupcake"></html>

이런 형식으로 html 요소에 data-theme의 값을 설정하는 것으로 사용하라고 되어있다. 토글 기능을 하는 버튼을 추가하고, 테마를 상호 변경하는 기능을 적용 해 보면 아래와 같은 코드를 적용 해 본다.

Header.svelte

<script>
	export let links = [
		{ name: "Home", url: "/" },
		{ name: "Products", url: "/products" },
		{ name: "About", url: "/about" },
		{ name: "Contact", url: "/contact" }
	];
    
	let theme = "lofi";

	function toggleTheme() {
		theme = theme === "lofi" ? "business" : "lofi";
		document.documentElement.setAttribute("data-theme", theme);
	}
</script>

<script>
	export let links = [
		{ name: "Home", url: "/" },
		{ name: "Products", url: "/products" },
		{ name: "About", url: "/about" },
		{ name: "Contact", url: "/contact" }
	];

	let theme = "lofi";

	function toggleTheme() {
		theme = theme === "lofi" ? "business" : "lofi";
		document.documentElement.setAttribute("data-theme", theme);
	}
</script>

<header class="shadow">
	<div class="container mx-auto px-4 py-2 flex justify-between items-center">
		<h1 class="text-2xl font-bold">My Shop</h1>
		<nav>
			<ul class="flex space-x-4">
				{#each links as link}
					<li><a href={link.url} class="hover:text-primary">{link.name}</a></li>
				{/each}
			</ul>
		</nav>
		<button on:click={toggleTheme}>
			Toggle Theme
		</button>
	</div>
</header>

설정에서 사용하기로 선언했던 lofi, business를 토글 방식으로 적용하기 위해 메서드 toggleTheme()를 정의 했다. document.documentElement를 통해서 얻어지는 값은 최상위 요소인 html을 반환하기 때문에 의도했던 html 요소의 data-theme의 값을 바꿀 수 있다.
toggleTheme 메서드를 호출하는 역할을 담당하도록 버튼을 만들고, onclick 이벤트를 할당했는데, 여기서 html 내 button 코드에서 바로 on:click={toggleTheme} 라는 문법을 사용해서 onclick 이벤트를 할당했다. Svelte의 고유 문법으로 순수 Javascript 방식으로는 addEventListener 등을 사용해서 적용 해야 하는 방법을 html 코드 내에서 바로 적용 할 수 있게 해주는 편리한 Svelte의 문법이다.

추가적으로 css를 위한 class 속성 중에서 구체적으로 배경이나 텍스트 색상을 지정했던 요소를 모두 삭제했다. 테마의 색상을 따라가야 정상적인 배경과 텍스트를 표시 할 수 있기 때문이다. 따라서 Footer의 코드도 다음과 같이 변경했다. 추가적으로 라이트모드, 다크모드에 따라 일부 CSS에 변화를 주어야 하는 경우는 따로 추가적인 CSS 변수 등을 사용하여 처리 하는 방식을 사용하면 된다.(나의 예제에는 별도로 적용하진 않는다.)

Footer.svelte

<footer class="py-4 mt-10">
	<div class="container mx-auto px-4">
		<div class="flex justify-between">
			<p>&copy; {new Date().getFullYear()} MyShop. All rights reserved.</p>
			<nav>
				<ul class="flex space-x-4">
					<li><a href="/privacy-policy" class="hover:text-primary">Privacy Policy</a></li>
					<li><a href="/terms-of-service" class="hover:text-primary">Terms of Service</a></li>
				</ul>
			</nav>
		</div>
	</div>
</footer>

이제 웹 브라우저에서 화면의 Toggle Theme 버튼을 누르면 테마가 토글 방식으로 변경되어서 라이트모드 <-> 다크모드 간 전환이 되는 것을 확인 할 수 있다.

모드 변경 기능 저장 & 화면 개선

현재 단계 Github 링크

현재는 웹 페이지를 새로고침하는 것 만으로도, 테마의 상태가 유지되지 않는다. 웹 페이지를 새로고침하거나, 혹은 브라우저를 종료했다가 다시 페이지에 접속 했을 때에도 자신이 사용했던 테마를 저장하려면 이 값을 반영구히 저장할 방법이 필요하다. 이 테마 값은 민감한 데이터 & 변조 위험성 이 전혀 없는 데이터이므로 로컬 스토리지에 저장해서 구현하기로 한다.
또 Toggle Theme의 텍스트 버튼이 아닌, 이미지를 통한 버튼과 현재 상태에 따라 달리보이는 아이콘을 적용 시켜본다. 구현한 코드는 아래와 같다.

<script>
	import { browser } from '$app/environment';
	import { onMount } from "svelte";
    
	export let links = [
		{ name: "Home", url: "/" },
		{ name: "Products", url: "/products" },
		{ name: "About", url: "/about" },
		{ name: "Contact", url: "/contact" }
	];

	let theme = "lofi";

	if (browser) { // 웹 브라우저환경에서만 로컬스토리지 접근
		onMount(() => { // 페이지가 로딩이 완료되면 실행
			const savedTheme = localStorage.getItem("theme");
			if (savedTheme) {
				theme = savedTheme;
				document.documentElement.setAttribute("data-theme", theme);
			}
		});
	}

	function toggleTheme() {
		theme = theme === "lofi" ? "business" : "lofi";
		document.documentElement.setAttribute("data-theme", theme);
		localStorage.setItem("theme", theme);
	}
</script>

<header class="shadow">
	<div class="container mx-auto px-4 py-2 flex justify-between items-center">
		<h1 class="text-2xl font-bold">My Shop</h1>
		<nav>
			<ul class="flex space-x-4">
				{#each links as link}
					<li><a href={link.url} class="hover:text-primary">{link.name}</a></li>
				{/each}
			</ul>
		</nav>
		<button on:click={toggleTheme} aria-label="Toggle Theme">
			{#if theme === "lofi"}
				<!-- 태양 아이콘 (라이트 모드) -->
				<svg
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
					class="w-6 h-6"
				>
					<circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2"/>
					<path
						stroke="currentColor"
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
					/>
				</svg>
			{:else}
				<!-- 초승달 아이콘 (다크 모드) -->
				<svg
					xmlns="http://www.w3.org/2000/svg"
					fill="none"
					viewBox="0 0 24 24"
					stroke="currentColor"
					class="w-6 h-6"
				>
					<path
						stroke="currentColor"
						stroke-linecap="round"
						stroke-linejoin="round"
						stroke-width="2"
						d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"
					/>
				</svg>
			{/if}
		</button>
	</div>
</header>

주석으로 간단히 코멘트를 달았지만, 다시 설명하자면 browser는 Sveltekit에서 사용할 수 있는 환경 값으로 이는 Svelte가 서버 측에서 렌더링될 때에 로컬스토리지에 접근하는 동작이 실행될 경우 에러를 발생시키기 때문에 안전한 처리를 위해서 넣어야 하는 코드라고 한다.

onMount의 경우는 Javascript에서 페이지가 준비되었을 때 (정확히는 DOM) 동작인 다음과 비슷한 역할을 한다.

document.addEventListener("DOMContentLoaded", function() {
});

페이지가 로딩 되었을 때로 생각하면 되는데, Svelte에서 정확히는 컴포넌트가 삽입되었을 때(DOM에 변동이 있을 때) 동작하는 코드라고 한다.

라이트모드와 다크모드의 버튼은 특별한 설명은 필요없을 것으로 생각한다. svg 이미지의 경우 그대로 사용하면 그만이고, Svelte 문법으로는 if ~ else 구문이 사용되었는데, 별다른 설명이 없어도 직관적으로 이해하기 쉬운 문법이라 생각한다.
이렇게 라이트모드와 다크모드의 개선을 한 페이지는 아래와 같이 동작한다.
라이트모드 다크모드 전환

추가적인 개선을 한다면 라이트모드와 다크모드의 이미지 전환간 애니메이션 효과를 넣는 것을 고려 해 볼만 할 것 같다.

Svelte 값의 변경과 공유

Svelte를 처음 사용하면서 쉬운듯 보였으나, 실제로 겪었을 때 바로 해결 하지 못했던 문제를 예시를 들며, 이것을 해결하는 방법을 정리하고자 한다. 우선 Svelte에서는 어떤 값이 변경 되었을 때, 그 변경을 인지 할 수 있어야 정상적으로 화면에도 적용이 된다. 이것과 관련된 개념은 상태인데, 우선 간단한 예제로 아래와 같이 컴포넌트를 만들고 사용해 보기로 한다.

CountBinding.svelte

<script>
	export let count = 0;

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

외부에서 count 값을 받아서 (프로퍼티) 사용 할 수 있는 컴포넌트로, 버튼을 누르면 클릭 횟수를 올리고, 저장하는 간단한 컴포넌트다.

이것을 메인 페이지에서 직접 사용하기로 한다.

+page.svelte

<script>
	import CountBinding from '$lib/components/CountBinding.svelte';

	let count = 10

	function increment() {
		count += 1;
	}
</script>

<h1>Welcome to SvelteKit</h1>
<div class="card-body">
	<CountBinding {count}/>
	<button on:click={increment}>Page Increment Click</button>
</div>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

새로만든 CountBound를 불러와서 사용하면서, 프로퍼티 값으로 count를 전달 했다. (부모 페이지에서 전달). 또 현재(부모)의 페이지에 CountBinding 컴포넌트에서 사용하고 있는 것과 동일한 함수를 만들고 count 값을 변경하도록 하는 별개의 button도 만들고, onClick 이벤트를 할당했다.

이제 웹 브라우저에서 화면을 확인하고, 컴포넌트에서 만들어진 버튼과, 메인 페이지에서 만들어진 버튼을 번갈아가며 눌러보자. 우선 부모페이지에서 전달한 프로퍼티 값은 제대로 적용되는 것을 확인 했다. 그런데 자세히 동작을 살펴보면 두 다른 버튼의 값이 제대로 동기화 되고 있지 않음을 확인 할 수 있다.
이유는 부모 페이지에서는 count값을 변경하고, 이것을 자식인 CountBinding 컴포넌트에게 전달하고 있으며, CountBinding에서 count 값을 변경 할 경우 직접 내부에서 count 값을 변경하므로, 부모에게는 이 전달사항이 전달되고 있지 않는 문제로 발생한다. 즉 Svelte의 전달된 프로퍼티 값은 부모가 자식에게 값을 전달하는 단방향 데이터 흐름이기 때문에, 자식이 해당 값을 직접 변경해도 부모에게는 반영되지 않기 때문이다.
이것을 해결하는 방법으로는 다음의 방법을 사용 할 수 있다.

데이터의 흐름을 일관되게 사용

현재 단계 Github 링크

위 문제를 해결하기 위해 어느 한쪽에서만 데이터의 값을 가지고 있고, 변경 하도록 하면 된다. 해결 방법에는 여러가지가 있을 수 있는데 모든 값을 부모에게서 전달 받는 형식으로 처리해서, 결과적으로 데이터를 한 곳에서만 관리하는 것으로 구현 해 본다.

CountBinding.svelte

<script>
	export let count = 0;

	export let onCountClick;
</script>

<button on:click={onCountClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

+page.svelte

<script>
	import CountBinding from '$lib/components/CountBinding.svelte';

	let count = 10

	function onCountClick() {
		count += 1;
	}
</script>

<h1>Welcome to SvelteKit</h1>
<div class="card-body">
	<CountBinding {count} {onCountClick}/>
	<button on:click={onCountClick}>Page Increment Click</button>
</div>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

부모 페이지로부터 값과 값을 변경하는 로직을 모두 전달 받고, 결과적으로 한쪽에서만 데이터를 일관되게 처리 하므로 동기화 문제가 발생하지 않는다. 값이 변경되었음에도, 이 값의 변경이 제대로 화면에 반영되지 않는 경우는 대부분 이런 부모와 자식 컴포넌트간 데이터의 흐름과 관리 문제로 발생한다. 나는 의도치 않은 동작을 수정하기 위해 꽤 많은 시간을 낭비했는데, 실제로 화면을 구성하면서 컴포넌트를 잘게 나눠서 관리하고, 이것을 사용하면서 부모와 자식 관계가 복잡해진 결과에서는 쉽게 해결하기가 어려웠기 때문이다.

store와 구독을 통한 해결

현재 단계 Github 링크

웹 애플리케이션의 구조가 복잡해지면 데이터의 흐름을 한쪽으로 제어하는 것 자체가 이해하기 어렵고 복잡 해 질수도 있다, 만약 컴포넌트의 자식 레벨이 계속 추가가 되는 경우, 부모에서 자식으로, 자식으로, 또 자식으로의 데이터 전달 구조를 위해 컴포넌트를 설계해야하며, 문제가 생겼을 때 어느 시점에 문제가 발생했는지 찾기가 쉽지 않다. 이때 해결책으로 사용 할 수 있는 개념은 Svelte에서 제공하는 store 객체이다.

src/lib/stores.js

import { writable } from 'svelte/store';

// count의 초기값을 10으로 설정
export const countStore = writable(10);

별도의 파일을 만들고, 공통으로 관리할 store 객체를 사용하기 위해 위 처럼 코드를 만든다. writable 은 쓰기가 가능한 store객체로, 내부, 외부에서 모두 변경이 가능하다. readable의 경우는 내부에서만 변경이 가능한 값이다. 이 외에도 여러 값이 존재하나 주로 사용되는 값은 writable이다.

CountBinding.svelte

<script>
	import { countStore } from '$lib/stores';

	let count;

	// 스토어를 구독하여 count 변수를 업데이트
	countStore.subscribe(value => {
		count = value;
	});

	function increment() {
		countStore.update(n => n + 1);
	}
</script>

<button on:click={increment}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

+page.svelte

<script>
	import CountBinding from '$lib/components/CountBinding.svelte';
	import { countStore } from '$lib/stores';

	let count;

	// 스토어를 구독하여 count 변수를 업데이트
	countStore.subscribe(value => {
		count = value;
	});

	function increment() {
		countStore.update(n => n + 1);
	}
</script>

<h1>Welcome to SvelteKit</h1>
<div class="card-body">
	<CountBinding />
	<button on:click={increment}>Page Increment Click</button>
</div>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

메인 페이지에서 count 숫자를 올리는 함수의 호출과 할당방식은 이전의 동기화에 문제가 있던 형태처럼 돌아왔다. 하지만 이 count 값은 제대로 동기화 되어서 처리 된다.
store객체를 통해 공통된 값을 처리하는데, subscribe는 값의 변경을 감지하며, update는 해당 값을 변경하는 동작을 한다. 여기서 store를 사용하면 부모와 자식 서로 양방향으로 값의 변경을 인식하고, 공유하게 된다. 즉 이전까지 일관된 데이터의 흐름과 처리를 고민해야했던 문제를 쉽게 해결 해 주었다.

이 상태에 대한 개념이 React에 비하면 굉장히 쉬운편이다. 아주 예전에 React를 잠깐 공부 해 본적이 있는데 React는 이 상태 문제에 대한 관리를 위한 여러 라이브러리가 있으며, 사용법도 처음에 쉽게 이해하기는 힘든 구조였던 것으로 기억한다.

Svelte 실전 정리를 마치며

몇 가지 더 정리하고 싶은 요소도 있었지만, 분량상 문제와 엄청 중요한 사항은 아니라서 이 정도로 정리를 마치려고 한다.
Svelte에 대한 현재 감상으로는 굉장히 매력적인 언어이다. 백엔드 개발자도 충분히 쉽게 이해 할 수 있으며, 사용하기에 쉬운 언어였다. 실제로는 토이프로젝트의 페이지를 구현하면서 AI에게 모든 것을 위임했기에, 만들어진 코드를 보고 이해하는 것 자체는 어렵지 않았고, Svelte 문법도 그냥 보고 바로 무엇을 의도하는 문법인지 바로 알 수 있을 정도였다. 다만 레이아웃 처리나, 상태 값에 대한 관리에 있어서 좀 더 나은 구조를 찾으려다가 시간을 조금 썼고, 그래서 이번글의 정리는 내가 약간 시간을 잡아먹게 되었던 요소를 위주로 정리했다. 막상 정리 해 보니 그리 어려운 개념은 아니였는데, 실제 작업은 복잡도가 이미 생긴 상태에서 작업을 하려고 하다보니, 코드 수정에 애를 좀 먹었던 것 같다. 결론적으로는 나의 코드에 대한 무지에 의한 문제였다. 그럼에도 불구하고 코드 자체의 이해 난이도는 낮았던 정말 좋은 언어라고 생각했다.
예상보다 본 글의 분량이 길어져서, 다음 포스팅에 이제 다시 원래의 작업물인 토이프로젝트의 프론트엔드 결과물에 대한 내용을 포스팅하겠다.

profile
오늘도 머릿속에 인덱스를 새겨넣는 개발자

0개의 댓글

관련 채용 정보