Ben Davis 유투버의 SvelteKit + Supabase 심화학습을 따라한 클론 프로젝트입니다. 소스는 깃허브 에 있습니다.
0. 개요
화면 캡쳐
users 리스트 출력
참고문서
1. 프로젝트 생성
1
2
3
4
5
6
7
8
| pnpm create svelte@latest svltk-drizzle-app
- Skeleton project
- TypeScript
cd svltk-drizzle-app
pnpm install
pnpm run dev
|
- Install TailwindCSS, tailwind-merge
tailwind.config.js
에 template paths 추가- noto sans, d2coding 한글 폰트 추가
app.css
에 Tailwind directives 추가- 최상위
+layout.svelte
에 app.css
import +page.svelte
에서 TailwindCSS classes 를 사용해 작동 확인
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
| pnpm add -D tailwindcss postcss autoprefixer tailwind-merge
pnpx tailwindcss init -p
# default font 설정
cat <<EOF > tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
fontFamily: {
sans: ['"Noto Sans KR"', ...defaultTheme.fontFamily.sans],
serif: ['"Noto Serif KR"', ...defaultTheme.fontFamily.serif],
mono: ['D2Coding', ...defaultTheme.fontFamily.mono],
},
},
plugins: [],
};
EOF
cat <<EOF > src/app.css
/* 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");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-white dark:bg-gray-800;
}
}
EOF
cat <<EOF > src/routes/+layout.svelte
<script lang="ts">
import '../app.css';
</script>
<slot />
EOF
cat <<EOF > src/routes/+page.svelte
<h1 class="text-3xl font-bold underline">Hello, SvelteKit!</h1>
EOF
pnpm run dev
|
참고
- 설치 :
pnpm add -D daisyui@latest
- 설정 :
tailwind.config.js
에 플러그인 추가- 로그 출력 설정
- themes 설정 : 설정된 테마만 포함
- 첫번째 테마가 light Mode
- 두번째 테마가 dark Mode
- 그 외의 테마는
[data-theme]
설정시 반영됨
1
2
3
4
5
6
7
8
9
| // tailwind.config.js
module.exports = {
//...
plugins: [require('daisyui')],
daisyui: {
logs: false,
themes: ['cmyk', 'dark', 'lofi'], // HTML[data-theme]
},
};
|
1
2
3
4
5
6
7
8
9
10
11
12
| <div class="card m-10 w-96 bg-base-100 shadow-xl" data-theme="lofi">
<figure>
<img src="https://picsum.photos/200/300" alt="Shoes" />
</figure>
<div class="card-body">
<h2 class="card-title">Shoes!</h2>
<p>If a dog chews shoes whose shoes does he choose?</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">Buy Now</button>
</div>
</div>
</div>
|
랜덤 이미지 https://picsum.photos/200/300
- 설치 :
pnpm add -D @tailwindcss/typography
- 설정 :
tailwind.config.js
에 플러그인 추가 (daisyui 앞에 위치)require('@tailwindcss/typography'),
- 설치 :
pnpm add theme-change
1
2
3
4
5
6
7
| import { onMount } from 'svelte';
import { themeChange } from 'theme-change';
onMount(() => {
themeChange(false);
// 👆 false parameter is required for svelte
});
|
프로젝트에 필요한 DB 및 설정들을 개별적으로 관리할 수 있다.
- supabase login : access token 입력 (클라우드와 link 할 때 필요)
- supabase init :
supabase/config.toml
설정 파일 생성됨 supabase/seed.sql
작성- supabase start : docker 컨테이너 실행
- supabase studio : http://localhost:54323
1
2
3
4
| # .env
DATABASE_URL="postgresql://postgres:postgres@localhost:54322/postgres"
SUPABASE_ANON_KEY="..."
SUPABASE_URL="http://localhost:54321"
|
- db reset 할 때, migrations sql 파일들은 이름 순서대로 실행된다.
- migrations 의 sql 파일들이 먼저 실행된 후, seed.sql 이 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| supabase migration new tables
# `supabase/migrations/{timestamp}_tables.sql` 파일 생성
# cat 명령어로 파일 생성해도 마찬가지로 동작함
cat <<EOF > supabase/migrations/$(date '+%Y%m%d%H%M%S')_tables.sql
-- Create the table
CREATE TABLE countries (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL COLLATE "ko-x-icu"
);
EOF
cat <<EOF > supabase/seed.sql
-- Insert some sample data into the table
INSERT INTO countries (name) VALUES
('미국'),('캐나다'),('러시아'),('중국'),('일본'),('한국');
EOF
# DB 컨테이너가 재시작 되며 초기화 된다
supabase db reset
|
Supabase Studio 의 SQL Editor 로 데이터 확인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| -- # 사용 가능한 collation 리스트
SELECT collname FROM pg_collation where collname like 'ko%';
-- => ko-x-icu
-- => ko-KR-x-icu
-- => ...
-- # collation 적용된 테이블과 컬럼 조회
select table_schema, table_name, column_name, collation_name
from information_schema.columns
where table_schema = 'public' and collation_name is not null
order by table_schema, table_name, ordinal_position;
-- query
select * from countries;
|
Drizzle Kit 설치 및 설정
drizzle-kit 은 npm 또는 pnpm 으로 실행해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| pnpm add drizzle-orm postgres dotenv
pnpm add -D drizzle-kit
# drizzle config 파일 생성
touch drizzle.config.ts
# db 를 읽어서 schema.ts 파일 자동 생성 (필요한 코드만 가져다 쓴다)
pnpm drizzle-kit introspect:pg
# schema 반영 (주의: drizzle.config.ts 파일이 있어야 함)
pnpm drizzle-kit push:pg
# typescript 로부터 마이그레이션 sql 생성 (자동)
pnpm drizzle-kit generate:pg --schema=./src/lib/db/schema.ts
|
1
2
3
4
5
6
7
8
| // src/lib/db/schema.ts
import { pgTable, serial, text, varchar } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 }),
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // drizzle.config.ts
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config();
export default {
schema: 'src/lib/db/schema.ts',
out: 'drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
verbose: true, // Print all statements
// strict: true, // Always ask for my confirmation
} satisfies Config;
|
1
2
3
4
5
6
7
| // package.json
{
"scripts": {
"generate": "drizzle-kit generate:pg",
"push": "drizzle-kit push:pg"
}
}
|
SvelteKit 에서 Drizzle ORM 사용하기
- 테스트용 데이터를 넣고
src/lib/db/index.ts
에서 drizzle 설정src/routes/+page.server.ts
에서 select 문 실행src/routes/+page.svelte
에서 데이터 출력
1
2
3
4
5
6
7
| insert into users (full_name, phone) values
('John Doe', '1234567890'),
('Talon Gison', '677267890'),
('Babara Dov', '33267890'),
('Emi Karl', '7774567890');
select * from users;
|
1
2
3
4
5
6
7
8
| // src/lib/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { DATABASE_URL } from '$env/static/private';
import * as schema from './schema';
const client = postgres(DATABASE_URL);
export const db = drizzle(client, { schema });
|
1
2
3
4
5
6
7
8
9
10
11
12
| // src/routes/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/db';
import { users } from '$lib/db/schema';
export const load: PageServerLoad = async () => {
const allUsers = await db.select().from(users);
return {
users: allUsers ?? [],
};
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div data-theme="cupcake" class="mt-4">
<h2>Users</h2>
<ul class="list-disc ml-4 mt-4">
{#each data.users as user (user.id)}
<li>{user.fullName} ({user.phone})</li>
{/each}
</ul>
</div>
|
Drizzle Migrate 실행
- drizzle 아래 migration SQL 파일이 있어야 하고
.env
에 DATABASE_URL 환경변수가 있어야 한다
1
2
3
4
5
6
7
8
9
10
11
12
| pnpx vite-node migrate.ts
# Running migrations
# {
# severity_local: 'NOTICE',
# severity: 'NOTICE',
# code: '42P07',
# message: 'relation "users" already exists, skipping',
# file: 'parse_utilcmd.c',
# line: '209',
# routine: 'transformCreateStmt'
# }
# Migrated successfully
|
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
| // src/migrate.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
async function main() {
require('dotenv').config();
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
throw new Error('DATABASE_URL is not set');
}
const sql = postgres(DATABASE_URL, { max: 1 });
const db = drizzle(sql);
console.log('Running migrations');
await migrate(db, { migrationsFolder: 'drizzle' });
console.log('Migrated successfully');
process.exit(0);
}
main().catch((e) => {
console.error('Migration failed');
console.error('➜', e);
process.exit(1);
});
|
9. Review
- 아직도 본론은 안들어 갔다. (이제 시작 2)
- daisyUI 는 색감이 이쁘다. 설치도 아주 간편하다.
- drizzle ORM 은 typescript 로 schema 를 정의하는 가벼운 ORM 이다.
Javascript with JSDoc
최근 추세가 Typescript 로부터 탈출하는 경향이 있어서 관련 이슈를 살펴보았다. Typescript 는 초기 JS 의 알 수 없는 오류로부터 안전을 보장받기 위해서 시작된 보조 도구였는데, 요즈음은 형세가 역전되어 Type 가드를 위한 잡다한 코드를 발생시키고 빨간줄을 없애기 위해 생산성을 떨어뜨리는 지경에 이르렀다고 느끼는 사람들이 늘어난 모양이다. 또 다른 개발도구와 프레임워크의 도움으로 타입 검사가 꼭 필요한 케이스가 줄은 탓도 크다.
1
2
3
4
5
6
7
8
9
| const checkIfExist = (
/** @type {any} */ objectName,
/** @type {string} */ keyName
) => {
let keyExists = Object.keys(objectName).some((key) => key === keyName);
return keyExists && objectName[keyName] ? true : false;
};
console.log(checkIfExist(data, 'tag')); // Returns true|false
|
이번에는 Javascript 로 진행해보려 했다가 TS 로 다시 바꿨다.
- drizzle 이 ts 용 ORM 라이브러리라서 TS 필요
- prisma 보다는 경량이라는 점이 확실히 이점이라
- JSDoc 을 사용해 보았지만, 줄곳 Typescript 만 사용하던 관행이 있어서 Javascript 가 낯설게 느껴졌다. 컴파일 과정이 필요하지 않을 정도로 고수이거나, 추가 개발에 부담을 느끼는 라이브러리 개발자들, 빠른 실행을 위해 단계를 단축하고 싶은 사람들이나 사용할 법하다고 느꼈다.
- 나 같은 어설픈 개발자에게는 vscode 와 typescript 조합이 최고다.
- Rich Harris : 라이브러리 개발에는 JSDoc 을 쓰자! (Typescript 과열)
앱 개발시에는 어차피 컴파일 과정이 있어서 JSDoc 사용의 효용이 크지 않다.