๐Ÿ“˜ Supabase ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ: ์„œ๋น„์Šค์™€ ์ฟผ๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•œ ๊ตฌ์กฐ ์„ค๊ณ„

์–‘์ •๊ทœยท2025๋…„ 4์›” 25์ผ
post-thumbnail

๐Ÿ’ก ๊ฐœ์š”

Supabase์˜ Storage๋ฅผ ์ด์šฉํ•ด ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ,
React Query์™€ Zustand Store, API ์„œ๋น„์Šค ๋ ˆ์ด์–ด๋ฅผ ์—ญํ• ๋ณ„๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•œ ๊ตฌ์กฐ๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.


๐Ÿงฉ ๋ฌธ์ œ ์ธ์‹

์ฒ˜์Œ์—๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ Supabase API๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ณ ,
์—…๋กœ๋“œ๋‚˜ ์ด๋ฏธ์ง€ ์กฐํšŒ ์ƒํƒœ๋„ ๋ชจ๋‘ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์ฃ :

  • โœ… ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ…์ž„์„ ๊ฐ€์ง (API ํ˜ธ์ถœ + ์ƒํƒœ ๊ด€๋ฆฌ + ๋กœ์ง)
  • โœ… ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๊ฐ™์€ ๋กœ์ง์„ ๋ฐ˜๋ณตํ•ด์•ผ ํ•จ
  • โœ… ํ…Œ์ŠคํŠธ์™€ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์–ด๋ ค์›€

๐Ÿ›  ๊ตฌ์กฐ ์„ค๊ณ„ ๋ฐฉํ–ฅ

์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ญํ• ์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฑ…์ž„์œ„์น˜์„ค๋ช…
API ํ˜ธ์ถœprofileServiceSupabase ํ˜ธ์ถœ๋งŒ ๋‹ด๋‹น (์ˆœ์ˆ˜ ํ•จ์ˆ˜)
React QueryuseProfileImageSuspenseQuery, useUploadProfileImageMutation์ƒํƒœยท์—๋Ÿฌ ์ฒ˜๋ฆฌยท์บ์‹œ ๋ฌดํšจํ™” ํฌํ•จ
UI ์ƒํƒœ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ํŒŒ์ผ ์„ ํƒ, ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋“ฑ UI ์ „์šฉ ์ƒํƒœ
์‚ฌ์šฉ์ž ์ •๋ณดuseUserStore์ƒํƒœ ์ „์—ญ ์ €์žฅ์†Œ์—์„œ ๊ด€๋ฆฌ

๐Ÿ“ฆ 1. ์„œ๋น„์Šค ๋ ˆ์ด์–ด (profileService.ts)

export const profileService = {
  async uploadProfileImage(file: File, userId: string) {
    const { data, error } = await supabase.storage
      .from('images')
      .upload(`profile/${userId}`, file, { upsert: true })
    if (error) throw error
    return data
  },

  async getProfileImageUrl(userId: string): Promise<string> {
    const { data } = supabase.storage.from('images').getPublicUrl(`profile/${userId}`)
    if (!data?.publicUrl) throw new Error('์ด๋ฏธ์ง€ URL์ด ์—†์Šต๋‹ˆ๋‹ค.')
    return data.publicUrl
  },
}
  • ๐Ÿ‘‰ Supabase ํ˜ธ์ถœ๋งŒ ๋‹ด๋‹น
  • ๐Ÿ‘‰ ์บ์‹œ ๋ฌดํšจํ™”๋‚˜ ํ† ์ŠคํŠธ๋Š” ํ•˜์ง€ ์•Š์Œ (ํ“จ์–ด ๋กœ์ง๋งŒ)

โš™๏ธ 2. ์ฟผ๋ฆฌ ํ›… (useProfileImageSuspenseQuery.ts, useUploadProfileImageMutation.ts)

export const useProfileImageSuspenseQuery = (profileId: string) =>
  useSuspenseQuery({
    queryKey: profileKeys.image(profileId),
    queryFn: () => profileService.getProfileImageUrl(profileId),
  })

export const useUploadProfileImageMutation = (profileId: string) => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: ({ file }) => profileService.uploadProfileImage(file, profileId),
    meta: {
      toastError: true,
      toastSuccess: true,
      toastSuccessMessage: 'ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค',
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: profileKeys.detail(profileId) })
      queryClient.invalidateQueries({ queryKey: profileKeys.image(profileId) })
    },
    retry: 0,
  })
}
  • ๐Ÿ‘‰ ์ฟผ๋ฆฌ ๋กœ์ง์€ ์—ฌ๊ธฐ์„œ ๋
  • ๐Ÿ‘‰ ์บ์‹œ ๋ฌดํšจํ™”, ํ† ์ŠคํŠธ ๋“ฑ ๋ถ€๊ฐ€ ์ฒ˜๋ฆฌ๋„ ํ›… ๋‚ด๋ถ€์—์„œ ๊ด€๋ฆฌ

๐ŸŽจ 3. ์‚ฌ์šฉ ์˜ˆ์‹œ (์ปดํฌ๋„ŒํŠธ)

const profileId = useUserStore((s) => s.profileId)
const { data: uploadedUrl } = useProfileImageSuspenseQuery(profileId)
const uploadMutation = useUploadProfileImageMutation(profileId)
  • โœ… ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ๋งŒ ํ•˜๋ฉด ๋จ
  • โœ… uploadedUrl๋งŒ ์žˆ์œผ๋ฉด ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆด ์ˆ˜ ์žˆ๊ณ 
  • โœ… uploadMutation.mutate({ file })๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋

โœ… ๊ตฌ์กฐ ๋ฆฌํŒฉํ„ฐ๋ง ํ›„์˜ ์ด์ 

ํ•ญ๋ชฉ๋ฆฌํŒฉํ„ฐ๋ง ์ „๋ฆฌํŒฉํ„ฐ๋ง ํ›„
API ํ˜ธ์ถœ ์œ„์น˜์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€profileService
์ƒํƒœ ๊ด€๋ฆฌ์ปดํฌ๋„ŒํŠธ์—์„œ ์ง์ ‘React Query๊ฐ€ ์บ์‹œ ๊ด€๋ฆฌ
๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌํ˜ผ์žฌ๋˜์–ด ์žˆ์Œ์„œ๋น„์Šค / ์ฟผ๋ฆฌ / UI ๋ช…ํ™• ๋ถ„๋ฆฌ
์žฌ์‚ฌ์šฉ์„ฑ๋‚ฎ์Œ๋†’์Œ (์–ด๋””์„œ๋“  ํ›…๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋จ)
์œ ์ง€๋ณด์ˆ˜์ค‘๋ณต ์ฝ”๋“œ ๋งŽ์Œ๋กœ์ง ํ•œ ๊ณณ์—์„œ ํ†ตํ•ฉ ๊ด€๋ฆฌ

โœ๏ธ ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ๊ตฌ์กฐ ๋ฆฌํŒฉํ„ฐ๋ง์„ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ๋Š” ์ˆœ์ˆ˜ UI๋งŒ ๋‹ด๋‹นํ•˜๋„๋ก ์ •๋ฆฌํ–ˆ๊ณ ,
๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ API ํ˜ธ์ถœ์€ ํ“จ์–ดํ•œ ์„œ๋น„์Šค ํ•จ์ˆ˜์™€ React Query ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ‘‡ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋„˜๊ธฐ๋ฉด ๋˜๋‹ˆ๊นŒโ€ฆ

const uploadMutation = useUploadProfileImageMutation(profileId)
uploadMutation.mutate({ file })

์„œ๋น„์Šค์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ์ด๋Ÿฐ ๊ตฌ์กฐ๊ฐ€ ํ˜‘์—…๊ณผ ์œ ์ง€๋ณด์ˆ˜์—์„œ ์ง„๊ฐ€๋ฅผ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค.
๋‹น์‹ ์˜ ํ”„๋กœ์ ํŠธ์—๋„ ์ด ๊ตฌ์กฐ๋ฅผ ํ•œ๋ฒˆ ์ ์šฉํ•ด๋ณด์„ธ์š”! ๐Ÿ˜Š

profile
๋กค๋ณด๋‹ค ๊ฐœ๋ฐœ์ด ์žฌ๋ฐŒ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค :D

0๊ฐœ์˜ ๋Œ“๊ธ€