SvelteKit Tailwind 튜토리얼 - 4일차
포스트
취소

SvelteKit Tailwind 튜토리얼 - 4일차

Tailwind CSS 튜토리얼들을 공부합니다. 같은 내용이 반복되는 것 같지만 익숙해질 때까지 다른 방법은 없다고 생각합니다. 유튜브의 좋은 강좌들을 하나씩 정복하려고 합니다.

0. 개요

1. 프로젝트 생성

참고 : 2024-02-26-svelte5-runes-example1 - 1. 프로젝트 생성

svelte.config.js

  • a11y 관련 메시지 무시하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
  },
  onwarn: (warning, handler) => {
    // console.log('** onwarn code:', warning.code);
    if (warning.code.startsWith('a11y-')) {
      return;
    }
    handler(warning);
  },
};

export default config;

tailwind.config.js

  • theme
    • screens : 스크린 사이즈 사용자 정의
    • container : center 적용과 padding 정의
    • fontFamily : 폰트 적용 순서
    • extend : 확장 설정
      • colors : 색상 변수
      • boxShadow : 박스 그림자 변수
      • dropShadow : (아이콘 같은) 박스가 아닌 형태에 대한 그림자 변수
    • plugins : typography, daisyui
    • daisyui
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const defaultTheme = require('tailwindcss/defaultTheme');

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{html,js,svelte,ts}'],
  theme: {
    extend: {
      colors: {
        dark: '#090E34',
        ...
      },
      boxShadow: {
        two: '0px 1px 4px rgba(0, 0, 0, 0.12)',
        three: '0px 1px 5px rgba(0, 0, 0, 0.14)',
        ...
      },
      dropShadow: {
        tooltip: '0px 0px 2px rgba(0, 0, 0, 0.14)',
        ...
      },
    },
    container: (theme) => ({
      center: true,
      padding: {
        DEFAULT: theme('spacing.4'),
        sm: theme('spacing.5'),
        lg: theme('spacing.6'),
        xl: theme('spacing.8'),
      },
    }),
    screens: {
      xs: '400px',
      sm: '540px',
      md: '720px',
      lg: '960px',
      xl: '1140px',
      '2xl': '1320px',
    },
    fontFamily: {
      sans: ['"Noto Sans KR"', ...defaultTheme.fontFamily.sans],
      serif: ['"Noto Serif KR"', ...defaultTheme.fontFamily.serif],
      mono: ['D2Coding', ...defaultTheme.fontFamily.mono],
    },
  },
  plugins: [require('@tailwindcss/typography'), require('daisyui')],
  daisyui: {
    logs: false,
    themes: false,
  },
};

app.pcss

  • 폰트 설치 : Noto Sans KR, Noto Serif KR, D2Coding
  • splidejs 기본 css 설치 : slider/carousel 라이브러리
    • 자동으로 node_modules 위치에서 읽어온다 (~ 필요없음)
  • html 기본 스타일
    • 부드러운 scroll
    • 기본 폰트 스타일
    • 기본 폰트 크기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* fonts: Noto Color Emoji, Noto Sans KR, Noto Serif KR */
@import url('https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Noto+Sans+KR:wght@300;400;500;700&family=Noto+Serif+KR:wght@400;700&display=swap');
@import url('//cdn.jsdelivr.net/gh/wan2land/d2coding/d2coding-ligature-full.css');
@import '@splidejs/splide/css';

@tailwind base;
@tailwind components;
@tailwind utilities;

html {
  scroll-behavior: smooth; /* 부드러운 스크롤 */
  font-family: font-sans;
  font-size: clamp(1rem, 2.2vh, 1.5rem);
}

2. 템플릿 구성요소 만들기

Svelte & Kakao Map 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<script>
  /**
   *  @type { {
   *    addControl: (arg0: any, arg1: any) => void;
   *    setDraggable: (arg0: boolean) => void;
   *  } }
   */
  let map;

  /** @type {import('svelte/action').Action<HTMLElement, any>}  */
  function setupKakaoMap(node, kakao) {
    let options = {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
      level: 3, // 지도의 레벨 (확대, 축소 정도)
    };
    // 지도 생성
    map = new kakao.maps.Map(node, options);

    // 일반 지도와 스카이뷰로 전환할 수 있는 컨트롤 생성
    let mapTypeControl = new kakao.maps.MapTypeControl();
    map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);

    let markerPosition = new kakao.maps.LatLng(33.450701, 126.570667);
    let marker = new kakao.maps.Marker({
      position: markerPosition,
    });
    marker.setMap(map);

    // 사용자 이벤트
    node.dispatchEvent(new CustomEvent('emit', { detail: 'hello' }));

    return {
      destroy() {
        if (map) {
          mapTypeControl = null;
          markerPosition = null;
          marker = null;
          // @ts-ignore
          map = null;
        }
      },
    };
  }

  /** @type { (event:{detail:string}) => void } */
  function handleEmit(event) {
    console.log('Map.onEmit:', event.detail);
  }
</script>

<div on:emit={handleEmit} use:setupKakaoMap={window.kakao} style="width:500px;height:400px;"></div>
<div class="mt-4">
  <button
    class="btn"
    onclick={() => {
      map.setDraggable(false);
    } } >지도 드래그 이동 끄기</button
  >
  <button
    class="btn"
    onclick={() => {
      map.setDraggable(true);
    } } >지도 드래그 이동 켜기</button
  >
</div>

svelte-kakaomap-example

window 전역변수에 kakao 등록하기

  • window.kakao 사용시 미등록 타입으로 인한 컴파일 오류 해결 방법
  • Window 인터페이스 타입에 kakao 를 등록하고 기본값 {} 설정
1
2
3
4
5
6
7
8
9
10
11
declare global {
  interface Window {
    kakao: any;
  }
  namespace App {
  }
}

window.kakao = window.kakao || {};

export {};

Svelte & Splider 예제

  • Slide root 아래에 track > list > slide 순서로 작성
    • 중간에 다른 요소가 끼워져 있으면 안됨
  • splide options
    • type : 슬라이딩 동작
    • arrows : 좌우의 기본 컨트롤러 없애기
      • 대신에 하단에 별도의 슬라이드 이동 버튼을 만들어 API 연결
    • perPage : 페이지 당 슬라이드 출력 개수
    • 슬라이더 전체 width 와 height
      • 슬라이더 개별 fixedWidth, fixedHeight 지정시 auto 크기는 무효화 됨
    • 클래스
      • pagenation : 원형 page 를 감싸는 container
      • page : 슬라이드 선택용 원형 버튼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<script>
  import { onMount } from 'svelte';
  import { Splide } from '@splidejs/splide';

  /** @type { import('@splidejs/splide').Splide } */
  let splide;

  /** @type {import('svelte/action').Action<HTMLElement>}  */
  function setupSplide(node) {
    /** @type { import('@splidejs/splide').Options } */
    let options = {
      type: 'loop',
      arrows: false, // hide
      padding: '2rem',
      perPage: 1,
      width: '40em',
      // fixedWidth: '20rem', // width of slide
      height: '10rem',
      classes: {
        pagination: 'splide__pagination bg-red-100 -bottom-2',
        page: 'splide__pagination__page bg-blue-400',
      },
    };

    splide = new Splide(node, options);
    splide.mount();

    return {
      destroy() {
        splide.destroy();
      },
    };
  }
</script>

<div class="container mx-auto">
  <!-- Slide root : track > list > slide -->
  <section class="splide" use:setupSplide>
    <h2 id="carousel-heading">Splide Basic HTML Example</h2>
    <div class="splide__arrows">
      <button class="splide__arrow splide__arrow--prev">Prev</button>
      <button class="splide__arrow splide__arrow--next">Next</button>
    </div>

    <div class="splide__track">
      <ul class="splide__list">
        <li class="splide__slide">Slide 01</li>
        <li class="splide__slide">Slide 02</li>
        <li class="splide__slide">Slide 03</li>
      </ul>
    </div>
  </section>

  <!-- controller -->
  <div class="mt-4 flex w-1/2 justify-center gap-8 border">
    <button class="btn btn-accent px-8"
      onclick={() => splide.go('<')}>Prev</button>
    <button class="btn btn-accent px-8"
      onclick={() => splide.go('>')}>Next</button>
  </div>
</div>

svelte-splide-example

Flex 로 반응형 컬럼 Grid 구현하기

  • wrapper 의 flex flex-wrap 이 핵심 포인트
    • wrapper 에서 -mx-4 한 후에, column 에서 p-4 으로 채운다.
  • column 에서 콘텐츠의 너비를 지정 : 1/2 또는 1/3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- Container -->
<div class="max-w-screen-xl mx-auto px-4">
  <!-- Grid wrapper -->
  <div class="-mx-4 flex flex-wrap">

    <!-- Grid column 1 -->
    <div class="w-full p-4 sm:w-1/2 lg:w-1/3">
      <!-- Column contents -->
      <div class="px-10 py-12 bg-white rounded-lg shadow-lg">
        <!-- Card contents -->
      </div>
    </div>

    <!-- Grid column 2 -->
    <div class="w-full p-4 sm:w-1/2 lg:w-1/3">
      <!-- Column contents -->
      <div class="px-10 py-12 bg-white rounded-lg shadow-lg">
        <!-- Card contents -->
      </div>
    </div>

    <!-- Grid column 3 -->
    <div class="w-full p-4 sm:w-1/2 lg:w-1/3">
      <!-- Column contents -->
      <div class="px-10 py-12 bg-white rounded-lg shadow-lg">
        <!-- Card contents -->
      </div>
    </div>

  </div>
</div>

Bootstrap-like responsive column grid with Tailwind CSS and flexbox

3. TailGrid - Agency Site Template

  • 섹션별로 svelte 파일 분리
  • 풀다운 메뉴 컨트롤러 navController 를 하위 요소에 전달
  • window.scrollY 이벤트를 연결하여 scrolledFromTop 반응형 변수 변경
  • 팀 구성원 정보를 PageData.members 로 받아서 하위 요소에 전달

  • 개발 편의를 위해 페이지 로딩시 자동으로 최하단으로 window.scrollTo 이동
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<script>
  import { onMount } from 'svelte';
  import TeamSection from './team-section.svelte';
  import AboutSection from './about-section.svelte';
  import ServiceSection from './service-section.svelte';
  import BrandSection from './brand-section.svelte';
  import HeroSection from './hero-section.svelte';
  import NavbarSection from './navbar-section.svelte';
  import TestimonialsSection from './testimonials-section.svelte';
  import PricingSection from './pricing-section.svelte';
  import CtaSection from './cta-section.svelte';
  import ContactSection from './contact-section.svelte';
  import BlogSection from './blog-section.svelte';
  import FooterSection from './footer-section.svelte';

  // Child 의 변경사항이 Parent 로 전달되기 위해서는 Object 형태이어야 한다
  // - scrolledFromTop 처럼 state 변수를 단순 전달하면 안올라온다
  const navController = (() => {
    /** @type {boolean} */
    let isOpen = $state(false);
    return {
      toggle: () => {
        isOpen = !isOpen;
      },
      get open() {
        return isOpen;
      },
    };
  })();

  // Child 로의 변경사항은 실시간으로 전달된다
  /** @type {boolean} */
  let scrolledFromTop = $state(false);

  // window 스크롤 세로 위치
  /** @type {number} */
  let y = $state(0);

  $effect(() => {
    // 사용자 스크롤에 의해 scrollY 가 변경되어야 갱신된다
    scrolledFromTop = y >= 50 ? true : false;
  });

  onMount(() => {
    $inspect(data);

    //** DEBUG: 새로고침 이후 아래부분 작성 내용을 즉시 확인
    window.scrollTo({
      top: document.body.scrollHeight,
      behavior: 'instant',
    });
  });

  // Smooth scroll to top
  function scrollToTop() {
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  }

  /** @type { {
   *     data: import('./$types').PageData
   *   } } */
  let { data } = $props();
</script>

<!-- || 윈도우 이벤트 연결 -->
<svelte:window bind:scrollY={y} />

<NavbarSection {scrolledFromTop} {navController} />
<HeroSection />
<BrandSection />
<ServiceSection />
<AboutSection />
<TeamSection members={data.members ?? []} />
<TestimonialsSection />
<PricingSection />
<CtaSection />
<ContactSection />
<BlogSection />
<FooterSection />

<!-- || Back to top button -->
<button
  onclick={scrollToTop}
  type="button"
  class="{!scrolledFromTop &&
    'hidden'} !fixed bottom-6 end-6 z-30 rounded-full bg-red-600 p-3 text-xs font-medium uppercase leading-tight text-white shadow-md transition duration-150 ease-in-out hover:bg-red-700 hover:shadow-lg focus:bg-red-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-800 active:shadow-lg"
  id="btn-back-to-top"
>
  <span class="[&>svg]:w-4">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
      stroke-width="3"
      stroke="currentColor"
    >
      <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18" />
    </svg>
  </span>
</button>

top 화면

Agency - Tailwind CSS Agency Site Template

전체 화면

Agency - Tailwind CSS Agency Site Template

Template Section and Pages:

  • Home Page
  • About Us
  • Our Team
  • Features
  • Pricing
  • Support
  • Blog
  • 404 Page

9. Review

  • TailGrid 유료 템플릿들을 모두 Svelte 버전으로 바꿔가며 연습하려고 한다.
    • 이제 겨우 1개 했는데, 엄청 길어서 힘들었다.
  • Svelte Action 을 이용해 splidejs 와 kakaomap 을 연결했다.
    • Agency 템플릿에 지도와 carousel 기능이 구현되어 있지 않아서 채워 넣었다.
  • window 전역객체에 property 를 정의해서 빨간줄을 안보게 되니 속이 시원하다.
    • 인터페이스로 Window 선언 후 window.kakao = window.kakao || {}; 초기화

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.