출처 : SvelteKit & Supabase Project Build #1 - Intro & Setup
설치 및 실행
cd svlt5-pokepage
bun install
bun --bun run dev --host 0.0.0.0
pokeapi.co 에서 제공하는 포켓몬 API 를 사용하여 사용자별 MyPage 에서 자신의 포켓몬 카드 3장과 코멘트를 조회하고 수정할 수 있다.
마이페이지
마이페이지 : edit 다이얼로그
홈페이지
로그인페이지
출처에서는 hooks 을 사용하지 않고 client 의 LayoutData 를 이용하여 supabase.auth 와 session 을 사용하고 있다.
ID 를 생성할 수 있는 수단 (ID 타입) : 사용자는 여러 ID를 가질 수 있음
SSR 을 사용하는 이유
SSR 프레임워크는 렌더링 및 데이터 가져오기를 서버로 이동하여 클라이언트 번들 크기와 실행 시간을 줄입니다.
+layout.js
로부터 생성된 LayoutData 는 $props
를 통해 전계층으로 전달된다.
depends('supabase:auth')
선언된 부분만 invalidate 할 수 있다.$effect
로 변경 처리를 할 수 있다.+layout.svelte
에서 했는데, +layout.js
에서 해도 된다.첫페이지
HOME 상단메뉴에 login 버튼이 나타나고, 첫페이지 출력
+layout.js
: createBrowserClient 호출하여 data 전달 (access_token)+layout.svelte
: onAuthStateChange 콜백 설정+layout.svelte
: data 로부터 session 갱신+page.svelte
: Login 버튼 출력 (session == null)로그인
로그인 입력폼에 ID/PWD 입력하면 user.email 이 표시된 첫페이지로 이동
+page.svelte
: ID/PWD 입력 => AuthState 상태 변경+layout.js
: createBrowserClient 호출하여 data 전달 (access_token)+page.svelte
: 변경된 data.session 로 인해 첫페이지 이동+layout.svelte
: data 로부터 session, user.email 갱신+page.svelte
: Login 버튼 없이 첫페이지 출력로그아웃
auth.signOut() 호출시 HOME 상단메뉴의 user.email 표시가 없어지고 Login 버튼 출력
+layout.svelte
상단의 logout 클릭 => AuthState 상태 변경+layout.svelte
: data 로부터 session 갱신 (session == null)사용자 경험을 높이기 위해 button 의 DOM 렌더링 전에 session 을 갱신했다.
$effect.pre
: DOM 의 beforeUpdate (ex: autoscroll 상태값)onMount
: 컴포넌트가 DOM 에 mount 될 때 한번만 실행$effect
: DOM 의 afterUpdate (ex: autoscroll 상태 변경에 대한 반영)<script>
let { children, data } = $props();
let supabase = $state(data.supabase);
let session = $state(data.session);
// DOM 업데이트 이전에 실행 ($effect 는 이후에)
$effect.pre(() => {
console.log('layout: beforeUpdate');
supabase = data.supabase;
session = data.session;
});
</script>
<div>
{#if session == null}
<button onclick="{()" ="">goto('/login')}>Login</button>
{:else}
</div>
$derived
는 $state
변수에 대한 반응성을 연결하는데 사용된다.$state
없이 $props
파생 변수를 처리할 수 없다.$derived
는 $effect
블록을 변수 단위로 적용한 것과 같다.-- 1. Create table
drop table if exists pokemons;
create table pokemons (
id bigint generated by default as identity primary key,
user_id uuid references auth.users on delete cascade,
email text,
comment text,
pokemon_ids integer[] -- '{1,2,3}'
);
-- 1-1. check user_id by email
do $$
declare
user_email text := '[email protected]';
user_id uuid;
begin
select id into user_id from auth.users where email = user_email;
raise notice 'ID of user "%" = "%"', user_email, user_id;
end; $$;
-- 1-2. insert sample data
insert into pokemons (user_id, email, comment, pokemon_ids) values(
(select id from auth.users where email = '[email protected]'),
'[email protected]',
'내가 좋아하는 개구리 포켓몬 3마리를 골랐어요.',
'{1,2,3}'
);
-- 1-3. select sample data
select * from pokemons;
-- 2. Enable RLS
alter table pokemons enable row level security;
-- 3. Create Policy : select
create policy "Public pokemons are visible to everyone."
on pokemons for select
to anon -- the Postgres Role (recommended)
using ( true );
-- 또는 자기 데이터만 보이기
-- create policy "User can see their own profile only."
-- on profiles for select
-- using ( (select auth.uid()) = user_id );
-- 3. Create Policy : insert
create policy "Users can create a profile."
on profiles for insert
to authenticated -- the Postgres Role (recommended)
with check ( (select auth.uid()) = user_id );
-- 3. Create Policy : update
create policy "Users can update their own profile."
on profiles for update
to authenticated -- the Postgres Role (recommended)
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
+page.svelte
$state
: myProfile, myPokemons 선언$effect
: getPokemonData 으로 myPokemons 갱신pokeapi.co 에서 제공하는 포켓몬 API 를 사용한다.
supabase 의 사용자 데이터를 다루기 위한 모든 기능을 포함한다.
자료구조
메소드
이런 식으로 할 수도 있다는 의미로 기록만 해둔다.
생성자에서 async 를 호출하면 지연된 초기화가 이루어지기 때문에 권장하지 않는다. 하지만 꼭 필요한 경우 (수명주기에 따른) 초기화 메소드를 따로 정의하여 외부에서 비동기로 호출하도록 한다.
참고 : The Proper Way to Write Async Constructors in JavaScript
포켓몬 프로파일 테이블을 읽어오는 코드 (생성자에서 Promise 사용)
export class PokemonProfile {
#supabase;
/**
* @type {{ userId: string, email: string, comment: string, pokemonIds: number[] } | null}
*/
#profile;
/**
* @param {import('@supabase/supabase-js').SupabaseClient<any, "public", any>} supabase
* @param {import('@supabase/supabase-js').Session} session
*/
constructor(supabase, session) {
this.supabase = supabase;
const userEmail = session.user?.email;
console.log(this.userEmail);
// 생성자에서 async 함수 호출하기 (되도록 안쓰는게 좋다)
return Promise.resolve(userEmail).then(email=>{
this.#profile = await this.#loadProfile();
return this;
});
}
/**
* [private] email 로 pokemon profile 을 읽어오기
*/
async #loadProfile() {
// Try to grab the current profile ([email protected])
const { data: profileData, error: profileError } = await this.#supabase
.from('pokemons')
.select('user_id, email, comment, pokemon_ids')
.eq('email', email);
console.log(profileData);
return profileData?.length > 0 ? profileData[0] : null;
}
/**
* [readonly] pokemon 사용자 프로파일
*/
get profile() { return this.#profile; }
}