React
에Next.js
가 있다면
Svelte
에는SvelteKit
이 있다.
SvelteKit 은 고성능 웹 앱을 구축하기 위한 프레임워크 이다.
SSR
/ SSG
load
hook
endpoint
Svelte 는 UI를 쉽게 만들게 해줄 수 있는 컴파일러 or 라이브러리의 느낌이 있고, (like React)
SvelteKit 은 Svelete 를 이용해서 SSR
웹앱을 구축한다거나 Routing
을 한다거나 등을 손쉽게 핸들링 할 수 있게 해준다. (like NextJS)
또한, Svelte Kit 에서 정제를 위해 client 에서 해줄 것인지, server에서 해줄 것인지 알아서 판단 해준다. (load
)
SvelteKit 은 기본적으로 src/routes 폴더 안에 들어가는 컴포넌트가 페이지가 되고 자동으로 routing 이 되도록 지원한다.
route 에는 2가지 type 이 있는데, pages
와 endpoints
로 나뉘어 진다.
pages
💡 우리가 일반적으로 생각하는 routing 된 page
page 는 .svelte
파일이다.
routes
├── route_a
│ ├── index.svelte ## 자동으로 index를 찾으므로 path는 다음과 같다 (url/route_a)
│ └── c.svelte ## path: /route_a/c
├── index.svelte ## path: /
└── route_b.svelte ## path: /route_b
동적 라우팅은 []
기호를 사용하면 된다.
예를 들어, src/routes/blog/[id].svelte
이런식으로 하면 동적라우팅이 가능해진다.
이렇게 동적 라우팅을 하면 react useParam
처럼 id 값에 대한 접근도 load
function 에 의해 접근 가능하다.
endpoint
💡 server 에서만 실행되는 무언가들이 있다.
database에 접근하거나, API 작업을 수행 하는 등의 작업을 수행하는 곳이다.
endpoint 는 .js
or .ts
파일이다.
HTTP method 와 통신하는 함수이고, 서버단 에서만 사용 할 수 있는 데이터 읽고 쓰기도 가능하게 한다.
pages ~ endpoint
💡 page는 endpoint 에게 data를 요청할 수 있고, endpoint는 JSON 으로 반환하여 준다.
src/routes/items/[id].svelte
페이지는 src/routes/items/[id].js
에서 데이터를 가져올 수 있다.
또한, src/routes/api/items/[id].js
에서도 가져올 수 있다.
import db from '$lib/database';
export const get = async ({ params }) => {
// 'params.id' comes from [id].js
const item = await db.get(params.id);
if (item) {
return { body: { item } };
}
return { status: 404 };
}
endpoint
를 포함한 모든 server-side코드
들은 외부 API 에서 데이터를 요청 해야 할 경우에도 fetch
에 access 할 수 있다.
💡
endpoint
의 함수들은 return 값으로response
를 나타내는{ status, headers, body }
객체를 반환한다.
또한, return 된 body 는 page 에서 prop으로 사용 할 수 있다.
<script>
// populated with data from the endpoint
export let item;
</script>
<h1>{item.title}</h1>
EndPoint
에서의 POST, PUT, PATCH, DELETEEndpoint
에서는 get 말고도 모든 HTTP 메서드를 처리할 수 있다.
export function post(event) {...}
export function put(event) {...}
export function patch(event) {...}
export function del(event) {...} // `delete` is a reserved word
위의 get 예제 처럼 body 를 리턴하고 그 body를 page의 props 로 사용 할 수도 있다.
// src/routes/items/js
import * as db from '$lib/database';
export async function get() {
const items = await db.list();
return {
body: { items }
};
}
export async function post({ request }) {
const [errors, item] = await db.create(request);
if (errors) {
// return validation errors
return {
status: 400,
body: { errors }
};
}
// redirect to the newly created item
return {
status: 303,
headers: {
location: `/items/${item.id}`
}
};
}
<!-- src/routes/items.svelte -->
<script>
// The page always has access to props from `get`...
export let items;
// ...plus props from `post` when the page is rendered
// in response to a POST request, for example after
// submitting the form below
export let errors;
</script>
{#each items as item}
<Preview item={item}/>
{/each}
<form method="post">
<input name="title">
{#if errors?.title}
<p class="error">{errors.title}</p>
{/if}
<button type="submit">Create item</button>
</form>
header
에 accept: application/json
으로 보낸다면, data 를 HTML 이 아닌 JSON 으로 받을 수도 있다.
Navigation bar 이나 전체 reset.css
등 앱 전체에 공통으로 들어가는 요소들을
src/routes/__layout.svelte
파일을 만들어서 넣어준다
React 에서의 Router.tsx 와 반쯤? 비슷하다.
// React 에서의 느낌
function Router() {
return (
<BrowserRouter>
<GNB />
<Layout>
<Routes>
<Route path="/users/login" element={<Login />} />
<Route path="/oauth" element={<KakaoRequest />} />
<Route path="/" element={<Main />} />
<Route path="/postings/:id" element={<Detail />} />
<Route path="/postings/:id/comments" element={<Detail />} />
<Route path="/contents/new" element={<NewPost />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
// Svelte 에서의 __layout.svelte
<script>
import GNB from '$lib/GNB';
import Layout from '$lib/Layout';
import "../app.css";
</script>
<GNB />
<Layout>
<slot />
</Layout>
이렇게 하면 모든 페이지에 app.css
에 있는 style 내용과
<GNB/>
가 들어가게 된다.
<slot />
은 필수로 들어가야 한다.
만약 settings
페이지안에 /setting/profile
/settings/notifications
같이 공유 하위 메뉴가 있다고 가정했을 때,
/settings
아래 페이지에만 적용되는 레이아웃을 만들 수도 있다.
<!-- src/routes/settings/__layout.svelte -->
<h1>Settings</h1>
<div class="submenu">
<a href="/settings/profile">Profile</a>
<a href="/settings/notifications">Notifications</a>
</div>
<slot></slot>
만약 admin
페이지를 만든다고 가정해보면, admin 페이지(back-office)는 client page 의 레이아웃을 가져갈 필요가 없다. 오히려 새로 짜야 하는데 이때 reset layout 을 사용하면 된다.
예를 들어서 /admin/*
페이지가 root layout 을 상속받지 않으려면
src/routes/admin/_layout.reset.svelte
라는 파일을 만들면 된다.
그 파일 내에서 새로운 layout 을 짜주면 된다.
💡 svelte 에서 전역상태 관리를 하기 위함이다.
react 에서 recoil, redux 와 비슷하다.
/src/lib/mystore.ts
import { writable } from 'svelte/store';
export const menuOpen = new writable(true);
import { menuOpen } from '$lib/mystore';
import { menuOpen } from '$lib/mystore';
const handleToggle = () => {
$menuOpen = !$menuOpen;
}
<div on:click={handleToggle}>
hihi
</div>
{$menuOpen} // 이렇게 하면 전역관리 변수에 접근 할 수 있다.
만약 전역에서 관리해야 하는 데이터가 백엔드와 통신해서 가져와야 한다면 다음과 같이 사용 할 수 있다.
/src/lib/mystore.ts
import { writable } from 'svelte/store';
export const products = new writable([]);
const fetchProduct = async ():void => {
const data = await fetch(`${BASE_URL}/product`)
.then(res => res.json())
.then(data => data.slice(0, 100));
products.set(data);
};
fetchProduct();
<script>
import { products } from '$lib/mystore';
console.log($products);
</script>
{#each $products as product}
<p>{product.name}</p>
{/each}
load
💡 load function은
url
,params
,props
,fetch
,session
,stuff
등 이 포함된 object 를 받는다.그리고 꼭
return
이 있어야 한다!!보통 페이지의 데이터가
endpoint
에서 나오는 경우는 필요 하지 않을 수 있다.
endpoint 와 비슷하게 페이지가 렌더링되기 전에 데이터를 fetch하고 조작 할 수 있는 함수인데,
외부 API에서 데이터를 로드하는 등 client 단에서 처리 할 수 있는 일을 할 때 더 유용하다.
외부 API 를 통해 data 를 받아오는 예시
<!-- src/routes/blog/[slug].svelte -->
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch, session, stuff }) {
const response = await fetch(`https://cms.example.com/article/${params.slug}.json`);
return {
status: response.status,
props: {
article: response.ok && (await response.json())
}
};
}
</script>
<script context=”module”>
은<script>
보다 위에 있어야 한다.
component 를 render 하기 전에 load 를 실행 시키기 때문
native fetch 를 쓰는게 아니라 SvelteKit-provided fetch 를 사용해야 한다.
window 나 document 같은 browser 객체를 건드리면 안된다.
API 키나 암호를 직접 쓰지 말고 쓰려면 endpoint 를 통해 호출해야 한다.
url
origin
, hostname
, pathname
, searchParams
<script context="module">
export const load = ({ url, params }) => {
console.log('url: ', url);
console.log(params.index);
return {
props: {
param: params.index
}
};
};
</script>
params
url.pathname
에서 /
빼고 나옴
동적라우팅 한 페이지에서 사용 할 것! (아니면 어차피 빈 값 뱉어냄)
만약 src/routes/a/[b]/[...c]
이렇게 생겨먹은 주소가 있다면
/a/x/y/z
의 params
는 다음과 같다.
{
"b": "x",
"c": "y/z"
}
fetch
load function 에서의 fetch 함수는 우리가 원래 알고 있던 fetch함수와 동일하다.
session
session 은 hooks
의 getSession
과 맞물려서 사용되는 load function 이다.
hooks
💡 HOOK :: 갈고리
무언가 중간에 참견 하는 느낌
src/hooks.ts js
나 src/hooks/index.ts
로 만들어진 파일
이 파일은 서버에서 run 되는 4가지 함수를 가지고 있다. (export)
handle
, handleError
, getSession
, externalFetch
getSession
// src/hooks.js
export const getSession = (event) => {
return {
user: {
id: 'yunkukPark',
name: 'yunkukPark',
access: 'admin'
}
};
};
// src/routes/index.svelte
<script>
import { session } from '$app/stores';
console.log($session);
</script>
hooks 의 getSession에 request response 를 설정해놓으면
Session을 가져오는 방법이 여러가지 있는데 그 중에서
session store 를 사용할 수 있다. 그걸 가지고 사용자의 정보 같은 것을 가져 올 수 있다.
handle function 에서 request 를 변경한 다음 getSession 의 req 에 보낼 수도 있고,
사용자가 인증되어있는지 등을 이용하여 인증 할 수 있다.
또한, 이런 식으로 load 기능을 통해 세션을 가져오는 방법도 있다.
<script context="module">
export const load = ({ session }) => {
console.log(session);
return {};
};
</script>
유저의 avatar나 nickname 을 가지고 와야 하는 경우
<script context="module">
export const load = ({ session }) => {
console.log(session);
return {
props: {
session
}
};
};
</script>
<script>
export let session;
</script>
<h2>{JSON.stringify(session)}</h2>
다음과 같이 module 쪽에서 props 로 session을 return 해주고 받아 쓰면 된다.
💡 사용자의 인증 유무를 session 자체에서 확인해서 좋다!
handle
💡 handle function은 Svelte Kit이 request 를 보낼 떄 마다 실행되고 request 를 보내준다.
return 되는 값은event
Object 이다.
⇒ 사용자를 인증하기 더욱 쉬워짐
기본적인 사용 방법은 아래와 같다.
export const handle = async ({ event, resolve }) => {
const response = await resolve(event);
return response;
};
event 와 resolve(event) 안에는 다음과 같은게 있다.
event: {
request: Request {
size: 0,
follow: 20,
compress: true,
counter: 0,
agent: undefined,
highWaterMark: 16384,
insecureHTTPParser: false,
[Symbol(Body internals)]: {
body: null,
stream: null,
boundary: null,
disturbed: false,
error: null
},
[Symbol(Request internals)]: {
method: 'GET',
redirect: 'follow',
headers: [Object],
parsedURL: [URL],
signal: null,
referrer: undefined,
referrerPolicy: ''
}
},
url: URL {
href: 'http://localhost:3000/',
origin: 'http://localhost:3000',
protocol: 'http:',
username: '',
password: '',
host: 'localhost:3000',
hostname: 'localhost',
port: '3000',
pathname: '/',
search: '',
searchParams: URLSearchParams {},
hash: ''
},
params: {},
locals: {},
platform: undefined
}
response: Response {
size: 0,
[Symbol(Body internals)]: {
body: null,
stream: null,
boundary: null,
disturbed: false,
error: null
},
[Symbol(Response internals)]: {
type: 'default',
url: undefined,
status: 304,
statusText: '',
headers: { etag: '"zk3ik8"' },
counter: undefined,
highWaterMark: undefined
}
}
그럼 event.local 을 이용해서 user정보를 getSession 에게 보낼 수 있다.
쿠키를 설정하고 쿠키사용하는 경우에 유용 할 것이다.
FLOW
Login을 하면 User 인증을 하고 Cookie 를 심는다.
어떤 페이지에서 요청을 할 때 Cookie 와 함께 보내줘서 User 인증을 자동으로 하게 한다.
login.js
(endpoint
) 파일 만들기
export const post = async ({ request }) => {
const body = await request.json();
const user = body;
if (!(user.username === 'Yunkuk' && user.password === '!@#$@^#$')) {
return { body: { message: 'NOT AUTH USER' } };
}
return { body: { message: 'SUCCESS' } };
};
<script>
let message = '';
const login = async () => {
const res = await fetch('api/login', {
method: 'POST',
body: JSON.stringify({
username: 'Yunkuk',
password: '!@#$@^#$'
})
});
const data = await res.json();
message = data.message;
};
</script>
<h1>Login Page</h1>
<p>{message}</p>
<button on:click={login}>Login</button>
<style>
p {
color: tomato;
}
</style>
npm i cookie uuid
uuid :: 랜덤 스트링 생성을 위한 dependency
import * as cookie from 'cookie';
import { v4 as uuidv4 } from 'uuid';
export const post = async ({ request }) => {
const body = await request.json();
const user = body;
if (!(user.username === 'Yunkuk' && user.password === '!@#$@^#$')) {
return { body: { message: 'NOT AUTH USER' } };
}
const sessionId = uuidv4();
const setCookie = cookie.serialize('session_id', sessionId, {
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
path: '/'
});
const headers = {
'set-cookie': setCookie
};
return {
status: 200,
headers,
body: {
message: 'SUCCESS'
}
};
};
export const handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
const response = await resolve(event);
return response;
};