
โ Supabase JS Client Library๋?
Supabase๊ฐ ์ ๊ณตํ๋ ์ ์ฉ Client Library.
๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉํ๋ ค๋ฉด ์ด๊ฑธ ์ฌ์ฉํด์ผ ํ๋ค.
๋ฟ๋ง ์๋๋ผ, ์ ์ ์ธ์ฆ(user authentication)์ ์ฌ์ฉํ๋ค๊ฑฐ๋ ์ค์๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ์ฌํญ์ ๋ฆฌ์ค๋ํ๊ฑฐ๋ Edge Function์ ํธ์ถํ๊ฑฐ๋ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๋ง๋ค ๋๋ ์ฌ์ฉํ๋ค.(์ ์ ์ ์ฟ ํค๋ฅผ ๊ฐ์ ธ์์ ์ ํจํ ์ธ์ ์ธ์ง ํ์ธ๋ ๊ฐ๋ฅ)
โ Installation
npm install @supabase/supabase-js
https://supabase.com/docs/reference/javascript/installing
JavaScript: Installing | Supabase Docs
supabase-js uses the Data API to query and mutate your Postgres data. You first need to grant Data API roles permissions to access your tables and functions. In Data API integrations settings, expose the specific tables and functions you want to access. To
supabase.com
โ Initialize
<app/supa-client.ts>
import { createClient } from "@supabase/supabase-js";
const client = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
);
export default client;
โ TypeScript Support
supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.ts
https://supabase.com/docs/reference/javascript/typescript-support
JavaScript: TypeScript support | Supabase Docs
Sometimes the generated types are not what you expect. For example, a view's column may show up as nullable when you expect it to be not null. Using type-fest, you can override the types like so:
supabase.com
package.json์ script๋ก ์ถ๊ฐํด์ค๋ค.
"db:typegen" : "supabase gen types typescript --project-id abcdefghijklmnopqrst > database.types.ts"
<app/supa-client.ts>
import { createClient } from "@supabase/supabase-js";
import type { Database } from "../database.types";
const client = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
);
export default client;
โ Usage
<community/queries.ts>
export const getTopics = async () => {
const { data, error } = await client.from("topics").select("name, slug");
console.log(data, error);
return data;
};
์๋ ์์ฑ์ผ๋ก query ์์ฑ์ด ํธํด์ง
export const getPosts = async () => {
const { data, error } = await client
.from("posts")
.select(
`post_id, title, created_at, topics (name), profiles (name, username, avatar)`,
);
};
topics์ profiles๋ฅผ ์๋์ผ๋ก joinํ ํ์์์ด ์ด๋ ๊ฒ ํ๋ฉด ๋๋ค.(๊ธฐ๋ณธ์ผ๋ก left join์ด ๋์ด์ topics์ name์ด๋ profiles์ name ๊ฐ์ ๊ฒ๋ค์ด nullable์ด๋ค.)
null์ด ์๋๋ก ํ๊ธฐ ์ํด์๋ inner join์ ํด์ค์ผ ํ๋ค.
Inner Join ๋ฐฉ๋ฒ
export const getPosts = async () => {
const { data, error } = await client.from("posts").select(
`post_id,
title,
created_at,
topics!inner (name),
profiles!inner (name, username, avatar)`,
);
};
Column ์ด๋ฆ ์ง์ ํ๋ ๋ฒ
export const getPosts = async () => {
const { data, error } = await client.from("posts").select(
`post_id,
title,
created_at,
topic:topics!inner (name),
author:profiles!inner (name, username, avatar)`,
);
};
Foreign Key Error
Error: Could not embed because more than one relationship was found for 'posts' and 'profiles'
details: [
{
cardinality: 'many-to-one',
embedding: 'posts with profiles',
relationship: 'posts_profile_id_profiles_profile_id_fk using posts(profile_id) and profiles(profile_id)'
},
{
cardinality: 'many-to-many',
embedding: 'posts with profiles',
relationship: 'post_upvotes using post_upvotes_post_id_posts_post_id_fk(post_id) and post_upvotes_profile_id_profiles_profile_id_fk(profile_id)'
}
],
ํ์ฌ posts์ profiles์ relationship์ ์ฐ๊ฒฐํ๋ ค๋๋ฐ relationship์ด 1๊ฐ ์ด์์ด๋ผ์ ์๊ธฐ๋ ์๋ฌ์ด๋ค.(๋ช ํํ๊ฒ ์ง์ ํด์ค์ผ ํจ)
(relationship์ด 1๊ฐ ์ด์์ธ ์ด์ ๋ posts์ profile_id๋ก profiles๊ฐ ์ฐ๊ฒฐ๋์ด ์๋ relationship๊ณผ post_upvotes์ post_id์ profile_id์ composite primary key๋ก ์ฐ๊ฒฐ๋์ด ์๋ relationship, ์ด๋ ๊ฒ 2๊ฐ๊ฐ ์๊ธฐ ๋๋ฌธ)
export const getPosts = async () => {
const { data, error } = await client.from("posts").select(
`post_id,
title,
created_at,
topic:topics!inner (name),
author:profiles!posts_profile_id_profiles_profile_id_fk!inner (name, username, avatar)`,
);
console.log(error);
if (error) throw new Error(error.message);
return data;
};
์ด๋ ๊ฒ ์ ์ด์ฃผ๋ฉด ๋๋ค.(Drizzle์ด constraint๋ฅผ ์์ฑํ ๋ ์๋ ์์ฑํ foreign key์ด๋ค.)
(๋ค๋ฅธ ๋ฐฉ๋ฒ๋ ์์)
count
export const getPosts = async () => {
const { data, error } = await client.from("posts").select(
`post_id,
title,
created_at,
topic:topics!inner (name),
author:profiles!posts_profile_id_profiles_profile_id_fk!inner (name, username, avatar),
upvotes:post_upvotes (count)
`,
);
console.log(error);
if (error) throw new Error(error.message);
return data;
};
count๋ง ์ ์ด์ฃผ๋ฉด ๋๋ค.
๊ทธ๋ฐ๋ฐ ์ด์ํ ํ์์ผ๋ก ๋ฐํ์ด ๋จ

์ ๊ตณ์ด count๋ฅผ ๋ฐํํ ๋ object์ ๋ด๊ณ array๋ก ๋ฐํํ ๊น?
๋ฐ์ดํฐ ํํ๊ฐ ๋ง์ ๋ค์ง ์๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฐ์ดํฐ๋ฒ ์ด์ค view๋ฅผ ์์ฑํ๋ค.
โ View
<app/sql/views/community-post-list-view.sql>
CREATE VIEW community_post_list_view AS
SELECT
posts.post_id,
posts.title,
posts.created_at,
topics.name AS topic,
profiles.name AS author,
profiles.avatar AS author_avatar,
profiles.username AS author_username,
COUNT(post_upvotes.post_id) AS upvotes
FROM posts
INNER JOIN topics USING (topic_id)
INNER JOIN profiles USING (profile_id)
LEFT JOIN post_upvotes USING (post_id)
GROUP BY posts.post_id, topics.name, profiles.name, profiles.avatar, profiles.username;

์์ ์ ์ SQL Query๋ฅผ Supabase์ views๋ผ๋ ํด๋๋ฅผ ์์ฑํ๊ณ ์์ ๋ฃ์ ๋ค ์คํ์์ผ ์ค๋ค.
๊ทธ๋ฆฌ๊ณ queries.ts๋ฅผ ์์ ํด์ค๋ค.
export const getPosts = async () => {
const { data, error } = await client
.from("community_post_list_view")
.select(`*`);
console.log(error);
if (error) throw new Error(error.message);
return data;
};
ํ์ง๋ง type์ด null์ด ์๋์ด์ผ ํ๋๋ฐ null๋ก ๋์ค๋ ๊ฒฝ์ฐ๊ฐ ์๋ค.
-> type-fest๋ฅผ ์ฌ์ฉํ๋ค.
โ type-fest
type-fest๋?
type์ ๋ฎ์ด ์ฐ๊ฑฐ๋ ์์ ํ๋๋ฐ ์ฌ์ฉํ ์ ์๋ TypeScript type์ ๋ชจ์
npm install type-fest
https://github.com/sindresorhus/type-fest
GitHub - sindresorhus/type-fest: A collection of essential TypeScript types
A collection of essential TypeScript types. Contribute to sindresorhus/type-fest development by creating an account on GitHub.
github.com
import { createClient } from "@supabase/supabase-js";
import type { MergeDeep, SetNonNullable, SetFieldType } from "type-fest";
import type { Database as SupabaseDatabase } from "../database.types";
type Database = MergeDeep<
SupabaseDatabase,
{
public: {
Views: {
community_post_list_view: {
Row: SetFieldType<
SetNonNullable<
SupabaseDatabase["public"]["Views"]["community_post_list_view"]["Row"]
>,
"author_avatar",
string | null
>;
};
};
};
}
>;
const client = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
);
export default client;
author_avatar๋ nullable์ด๊ธฐ ๋๋ฌธ์ string | null๋ก ๋ณ๊ฒฝํด์ค์ผ ํ๋ค.
'๐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ - React Router v7 - clientLoader, HydrateFallback (0) | 2026.05.29 |
|---|---|
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ - React Router v7 - prefetch (1) | 2026.05.29 |
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ - Magic UI (1) | 2026.05.21 |
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ - Simple Icons (0) | 2026.05.15 |
| ๋ผ์ด๋ธ๋ฌ๋ฆฌ - React Router v7 (0) | 2026.05.11 |